Voxgig.Struct 0.1.0

dotnet add package Voxgig.Struct --version 0.1.0
                    
NuGet\Install-Package Voxgig.Struct -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Voxgig.Struct" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Voxgig.Struct" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Voxgig.Struct" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Voxgig.Struct --version 0.1.0
                    
#r "nuget: Voxgig.Struct, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Voxgig.Struct@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Voxgig.Struct&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Voxgig.Struct&version=0.1.0
                    
Install as a Cake Tool

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/DELETE sentinels, and the InjectState machinery. Inject/Transform/Validate/Select all 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; the CorpusScoreboard test mirrors the Java/C++ runners and writes corpus-scoreboard.json after each run with per-.jsonic-file pass counts. The committed baseline lives at test-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: VoxgigStruct targeting net8.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.Str instead of T.String (collides with System.String).
  • T.Func instead of T.Function (collides with Func<>).

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 via RegexOptions.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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