Voxgig.Struct
0.1.0
dotnet add package Voxgig.Struct --version 0.1.0
NuGet\Install-Package Voxgig.Struct -Version 0.1.0
<PackageReference Include="Voxgig.Struct" Version="0.1.0" />
<PackageVersion Include="Voxgig.Struct" Version="0.1.0" />
<PackageReference Include="Voxgig.Struct" />
paket add Voxgig.Struct --version 0.1.0
#r "nuget: Voxgig.Struct, 0.1.0"
#:package Voxgig.Struct@0.1.0
#addin nuget:?package=Voxgig.Struct&version=0.1.0
#tool nuget:?package=Voxgig.Struct&version=0.1.0
Struct for C#
C# / .NET port of the canonical TypeScript implementation.
Status: complete. Full TS-canonical parity: all 48 functions, 15 type bit-flags, 3 mode constants (
M_KEYPRE/M_KEYPOST/M_VAL),SKIP/DELETEsentinels, and theInjectStatemachinery.Inject/Transform/Validate/Selectall dispatch through the canonical injector machinery: 11 transform commands, 6 validate checkers, 4 select operators.Passes the full shared corpus (1178/1178). The xUnit suite (
StructTest.cs, 74 tests) is the green-bar regression baseline; theCorpusScoreboardtest mirrors the Java/C++ runners and writescorpus-scoreboard.jsonafter each run with per-.jsonic-file pass counts. The committed baseline lives attest-baseline.json.
For motivation, language-neutral concepts, and the cross-language parity matrix, see the top-level README.
Install
Inside the monorepo:
cd csharp
dotnet restore
dotnet build
- Project:
VoxgigStructtargetingnet8.0. - Namespace:
Voxgig.Struct. - Static class:
StructUtils(all functions are static methods).
Quick start
using Voxgig.Struct;
var store = new Dictionary<string, object?> {
["db"] = new Dictionary<string, object?> { ["host"] = "localhost" },
["user"] = new Dictionary<string, object?> {
["first"] = "Ada", ["last"] = "Lovelace",
},
["age"] = 36,
};
var host = StructUtils.GetPath(store, "db.host");
// host == "localhost"
var named = StructUtils.Transform(store, new Dictionary<string, object?> {
["name"] = "`user.first`",
["surname"] = "`user.last`",
["years"] = "`age`",
});
StructUtils.Validate(named, new Dictionary<string, object?> {
["name"] = "`$STRING`",
["surname"] = "`$STRING`",
["years"] = "`$INTEGER`",
});
Naming convention
C# uses PascalCase for all public members:
| Canonical | C# |
|---|---|
getpath |
GetPath |
setpath |
SetPath |
getprop |
GetProp |
setprop |
SetProp |
isnode |
IsNode |
keysof |
KeysOf |
escre |
EscRe |
escurl |
EscUrl |
Function reference
Source: Struct.cs.
Predicates
StructUtils.IsNode(object? val) // bool
StructUtils.IsMap(object? val) // bool
StructUtils.IsList(object? val) // bool
StructUtils.IsKey(object? val) // bool
StructUtils.IsEmpty(object? val) // bool
StructUtils.IsFunc(object? val) // bool
StructUtils.IsNode(StructUtils.Jm("a", 1)); // true
StructUtils.IsMap(StructUtils.Jm("a", 1)); // true
StructUtils.IsList(StructUtils.Jt(1, 2)); // true
StructUtils.IsKey("name"); // true
StructUtils.IsEmpty(new List<object?>()); // true
Type inspection
StructUtils.Typify(object? value) // int — bit-field
StructUtils.TypeName(int t) // string
StructUtils.Typify(1); // T.Scalar | T.Number | T.Integer (201326720)
StructUtils.Typify(42); // T.Scalar | T.Number | T.Integer
StructUtils.TypeName(8192); // "map" (8192 == T.Map)
StructUtils.TypeName(StructUtils.Typify("hi")); // "string"
Size, slice, pad
StructUtils.Size(object? val)
StructUtils.Slice(object? val, int? start = null, int? end = null,
bool mutate = false)
StructUtils.Pad(object? str, int padding = 44, string? padchar = null)
StructUtils.Size(StructUtils.Jt(1, 2, 3)); // 3
Slice keeps the first N; a negative start drops the last |start|
items, and end is exclusive:
StructUtils.Slice(StructUtils.Jt(1, 2, 3, 4, 5), 1, 4); // [2, 3, 4]
StructUtils.Slice("abcdef", -3); // "abc" (drops the last 3)
StructUtils.Pad("a", 3); // "a "
Property access
StructUtils.GetProp(object? val, object? key, object? alt = null)
StructUtils.SetProp(object? parent, object? key, object? val)
StructUtils.DelProp(object? parent, object? key)
StructUtils.GetElem(object? val, object? key, object? alt = null)
StructUtils.GetDef(object? val, object? alt)
StructUtils.HasKey(object? val, object? key)
StructUtils.KeysOf(object? val)
StructUtils.Items(object? val)
StructUtils.StrKey(object? key)
StructUtils.GetProp(StructUtils.Jm("x", 1), "x"); // 1
StructUtils.SetProp(StructUtils.Jm("a", 1), "b", 2); // { a = 1, b = 2 }
StructUtils.DelProp(StructUtils.Jm("a", 1, "b", 2), "a"); // { b = 2 }
StructUtils.GetElem(StructUtils.Jt(10, 20, 30), -1); // 30
StructUtils.HasKey(StructUtils.Jm("a", 1), "a"); // true
KeysOf returns map keys in sorted order:
StructUtils.KeysOf(StructUtils.Jm("b", 4, "a", 5)); // ["a", "b"] (sorted)
StructUtils.Items(StructUtils.Jm("a", 1, "b", 2)); // [["a", 1], ["b", 2]]
StructUtils.StrKey(2.2); // "2"
Path operations
StructUtils.GetPath(object? store, object? path,
object? current = null, InjectState? state = null)
StructUtils.SetPath(object? store, object? path, object? val)
StructUtils.Pathify(object? val, int startIn = 0, int endIn = 0)
var store = new Dictionary<string, object?> {
["a"] = new Dictionary<string, object?> {
["b"] = new Dictionary<string, object?> { ["c"] = 42 }
}
};
StructUtils.GetPath(store, "a.b.c"); // 42
var fresh = new Dictionary<string, object?>();
StructUtils.SetPath(fresh, "db.host", "localhost");
StructUtils.SetPath(StructUtils.Jm("a", 1, "b", 2), "b", 22); // { a = 1, b = 22 }
StructUtils.Pathify(StructUtils.Jt("a", "b", "c")); // "a.b.c"
Tree operations
StructUtils.Walk(object? val, WalkApply? before = null,
WalkApply? after = null, int? maxdepth = null)
StructUtils.Merge(object? val, int? maxdepth = null)
StructUtils.Clone(object? val)
StructUtils.Flatten(List<object?> list, int depth = 1)
StructUtils.Filter(object? val, Func<List<object?>, bool> check)
public delegate object? WalkApply(
object? key, object? val, object? parent, List<object?> path);
Last input wins; maps deep-merge; lists merge by index:
StructUtils.Merge(StructUtils.Jt(
StructUtils.Jm("a", 1, "b", 2, "k", StructUtils.Jt(10, 20), "x", StructUtils.Jm("y", 5, "z", 6)),
StructUtils.Jm("b", 3, "d", 4, "e", 8, "k", StructUtils.Jt(11), "x", StructUtils.Jm("y", 7))));
// { a = 1, b = 3, d = 4, e = 8, k = [11, 20], x = { y = 7, z = 6 } }
StructUtils.Clone(StructUtils.Jm("a", StructUtils.Jm("b", StructUtils.Jt(1, 2))));
// { a = { b = [1, 2] } } (a deep copy)
StructUtils.Flatten(StructUtils.Jt(1, StructUtils.Jt(2, StructUtils.Jt(3))));
// [1, 2, [3]] (one level by default)
Filter passes each [key, value] pair to the check and returns the
matching values (not the pairs):
StructUtils.Filter(StructUtils.Jt(1, 2, 3, 4, 5), kv => Convert.ToInt32(kv[1]) > 3);
// [4, 5]
String / URL / JSON
StructUtils.EscRe(string s)
StructUtils.EscUrl(string s)
StructUtils.Join(IList<object?> arr, string? sep = null, bool? url = null)
StructUtils.Jsonify(object? val, int indent = 2, int offset = 0)
StructUtils.Stringify(object? val, int? maxlen = null)
StructUtils.EscRe("a.b+c"); // "a\\.b\\+c"
StructUtils.EscUrl("hello world?"); // "hello%20world%3F"
StructUtils.Join(StructUtils.Jt("a", "b", "c"), "/"); // "a/b/c"
Jsonify pretty-prints by default (indent 2); pass indent: 0 for the
compact form:
StructUtils.Jsonify(StructUtils.Jm("a", 1));
// {
// "a": 1
// }
StructUtils.Jsonify(StructUtils.Jm("a", 1, "b", 2), indent: 0); // {"a":1,"b":2}
Stringify is the compact, quote-light form — keys are sorted and object
braces are kept; the second argument caps the length (the ... counts):
StructUtils.Stringify(StructUtils.Jm("a", 1, "b", StructUtils.Jt(2, 3))); // {a:1,b:[2,3]}
StructUtils.Stringify("verylongstring", 5); // ve...
Inject / transform / validate / select
StructUtils.Inject(object? val, object? store, InjectState? state = null)
StructUtils.Transform(object? data, object? spec, InjectState? state = null)
StructUtils.Validate(object? data, object? spec, InjectState? state = null)
StructUtils.Select(object? children, object? query)
// Backtick refs in strings are replaced by store values.
StructUtils.Inject(StructUtils.Jm("x", "`a`", "y", 2), StructUtils.Jm("a", 1)); // { x = 1, y = 2 }
// Validate against a shape (throws on mismatch).
StructUtils.Validate(
StructUtils.Jm("name", "Ada", "age", 36),
StructUtils.Jm("name", "`$STRING`", "age", "`$INTEGER`"));
// { name = "Ada", age = 36 }
// Find children matching a query.
StructUtils.Select(
StructUtils.Jm("a", StructUtils.Jm("name", "Alice", "age", 30),
"b", StructUtils.Jm("name", "Bob", "age", 25)),
StructUtils.Jm("age", 30));
// [{ name = "Alice", age = 30, $KEY = "a" }]
Builders
StructUtils.Jm(params object?[] kv) // dictionary
StructUtils.Jt(params object?[] v) // list
Constants
Sentinels
StructUtils.SKIP // emit nothing
StructUtils.DELETE // remove from parent
Type bit-flags (Voxgig.StructUtils.T)
T.Any T.NoVal T.Boolean T.Decimal
T.Integer T.Number T.Str T.Func
T.Symbol T.Null T.List T.Map
T.Instance T.Scalar T.Node
T.Str and T.Func use shortened names because String and
Function collide with BCL types.
Walk / inject phase flags
M_KEYPRE M_KEYPOST M_VAL
Transform commands
$DELETE $COPY $KEY $META $ANNO
$MERGE $EACH $PACK $REF $FORMAT $APPLY
Validate checkers
$MAP $LIST $STRING $NUMBER $INTEGER $DECIMAL $BOOLEAN
$NULL $NIL $FUNCTION $INSTANCE $ANY $CHILD $ONE $EXACT
Notes
null and object?
Nullable reference types are enabled
(<Nullable>enable</Nullable>). The API exposes nullable
references throughout. As in Go and Java, null covers both JSON
null and "absent".
Identifier collisions
Some canonical names collide with C# reserved or stdlib identifiers:
T.Strinstead ofT.String(collides withSystem.String).T.Funcinstead ofT.Function(collides withFunc<>).
Behaviour is unchanged.
Status
Complete: the full canonical API is present and the parity check
(../tools/check_parity.py) reports C# ok.
See ../REPORT.md for the cross-port matrix.
Regex
Uniform six-function regex API (see /design/REGEX_API.md). The C# port
wraps System.Text.RegularExpressions.Regex.
API
| Function | Maps to |
|---|---|
ReCompile(pattern) |
new Regex(pattern) (throws RegexParseException on bad pattern) |
ReTest(pattern, input) |
Regex.IsMatch(input, pattern) |
ReFind(pattern, input) |
first match as string[] of [whole, group1, …] or null |
ReFindAll(pattern, input) |
List<string[]> |
ReReplace(pattern, input, rep) |
Regex.Replace(input, pattern, rep) |
ReEscape(s) |
Regex.Escape(s) |
Dialect
Patterns must stay inside the RE2 subset documented in /design/REGEX.md.
.NET regex supports backreferences and lookaround; using them will not
be portable.
Sharp edges
- Catastrophic backtracking. .NET's regex is backtracking; the
discovery panel sees P1 (
^(a+)+$over 22 a's plus!) in ~390 ms here. .NET 7+ ships a non-backtracking engine you can opt into viaRegexOptions.NonBacktracking— consider it for untrusted patterns. Stay inside the RE2 subset and prefer flat patterns. - Zero-width
replace.ReReplace("a*", "abc", "X")returns"XXbXcX"— the ECMA convention shared by all PCRE/ECMA/.NET/Java/Onigmo engines plus the in-tree Thompson ports. Go (RE2) returns"XbXcX"instead; see/design/REGEX_PATHOLOGICAL.md.
See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel.
Build and test
cd csharp
dotnet restore
dotnet test
Tests in tests/ consume fixtures from
../build/test/.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 114 | 6/29/2026 |