Skip to content

Latest commit

 

History

History
511 lines (368 loc) · 13.5 KB

File metadata and controls

511 lines (368 loc) · 13.5 KB

Struct for Java

Java 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 sentinel marker maps, and the Injection state machine. inject()/transform()/validate()/select() all dispatch through the canonical injector machinery (the 11 transform commands, the validate checkers, and the 4 select operators).

Passes the full shared corpus. Run locally with mvn test from java/. Per-file pass counts are written to target/corpus-scoreboard.json; the committed baseline lives at test-baseline.json.

For motivation, language-neutral concepts, and the cross-language parity matrix, see the top-level README and REPORT.md. For the in-depth guide (tutorial, recipes, explanation), see DOCS.md.

Status: complete. The full canonical surface is present, the parity check reports Java ok, and the shared build/test/ corpus passes in full (1300/1300, make test-java).

Install

In the monorepo:

cd java
mvn package        # or `make build`

Group / artifact: com.voxgig:struct-java. Single class: voxgig.struct.Struct (all functions are static methods).

import voxgig.struct.Struct;

Quick start

import voxgig.struct.Struct;
import java.util.Map;

Map<String, Object> store = Map.of(
    "db", Map.of("host", "localhost", "port", 5432)
);

Object host = Struct.getpath(store, "db.host");   // "localhost"

// Reshape by example.
Object out = Struct.transform(
    Map.of("user", Map.of("first", "Ada"), "age", 36),
    Map.of("name", "`user.first`", "years", "`age`")
);
// { name=Ada, years=36 }

Naming convention

The core functions keep the canonical lowercase names exactly (getpath, setpath, getprop, setprop, isnode, ismap, escre, escurl, keysof, …) — same as most ports, not Java-style camelCase. Only the regex layer (reCompile/reTest/reFind/reFindAll/ reReplace/reEscape) and the three injection helpers (checkPlacement/injectorArgs/injectChild) use camelCase.

Function reference

All functions are public static on voxgig.struct.Struct. The full, example-by-example reference is in DOCS.md; the canonical semantics for every function are in the top-level reference.

Struct.StructUtility is a nested instance facade (every function as an instance method), useful for injecting the API as an object; walk has (val, apply), (val, before, after), and (val, before, after, maxdepth) overloads:

public interface WalkApply {
    Object apply(String key, Object val, Object parent, List<String> path);
}
public static Object walk(Object val, WalkApply before, WalkApply after, int maxdepth)

Minor utility examples

Each call uses the mutable jm / jt builders so output ordering is deterministic. The result of every example is also a tested corpus entry.

Predicates — isnode is true for maps and lists:

Struct.isnode(Struct.jm("a", 1));        // true

ismap is true only for maps; islist only for lists:

Struct.ismap(Struct.jm("a", 1));         // true
Struct.islist(Struct.jt(1, 2));          // true

iskey is true for non-empty strings and numbers (usable as keys):

Struct.iskey("name");                    // true

isempty is true for null, empty strings, and empty nodes:

Struct.isempty(Struct.jt());             // true

Size of a node is its element count:

Struct.size(Struct.jt(1, 2, 3));         // 3

slice keeps the first N; a negative start drops the last |start| items, and end is exclusive:

Struct.slice(Struct.jt(1, 2, 3, 4, 5), 1, 4);   // [2, 3, 4]
Struct.slice("abcdef", -3, null);        // "abc"  (drops the last 3)

pad extends to the right (a negative width pads on the left):

Struct.pad("a", 3, null);                // "a  "

getprop reads a key from a map or list:

Struct.getprop(Struct.jm("x", 1), "x");  // 1

getelem is list-specific and supports negative-from-the-end indexing:

Struct.getelem(Struct.jt(10, 20, 30), -1);   // 30

setprop returns the parent with the key set:

Struct.setprop(Struct.jm("a", 1), "b", 2);   // {a=1, b=2}

delprop returns the parent with the key removed:

Struct.delprop(Struct.jm("a", 1, "b", 2), "a");   // {b=2}

haskey is true when the key holds a value:

Struct.haskey(Struct.jm("a", 1), "a");   // true

items returns the [key, value] pairs of a map or list:

Struct.items(Struct.jm("a", 1, "b", 2));   // [[a, 1], [b, 2]]

strkey coerces a key to its canonical string form (numbers truncate):

Struct.strkey(2.2);                      // "2"

keysof returns map keys sorted:

Struct.keysof(Struct.jm("b", 4, "a", 5));   // ["a", "b"]  (sorted)

filter passes each [key, value] pair to the check and returns the matching values (not the pairs):

Struct.filter(Struct.jt(1, 2, 3, 4, 5),
    item -> ((Number) item.get(1)).intValue() > 3);   // [4, 5]

jsonify pretty-prints by default (indent 2); pass { "indent": 0 } for the compact form:

Struct.jsonify(Struct.jm("a", 1));
// {
//   "a": 1
// }
Struct.jsonify(Struct.jm("a", 1, "b", 2), Map.of("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):

Struct.stringify(Struct.jm("a", 1, "b", Struct.jt(2, 3)));   // {a:1,b:[2,3]}
Struct.stringify("verylongstring", 5);   // ve...

getpath reads a deep value by dot-path (arg order: getpath(store, path)):

Struct.getpath(Struct.jm("a", Struct.jm("b", Struct.jm("c", 42))), "a.b.c");   // 42

setpath writes a deep value, returning the (mutated) store:

Struct.setpath(Struct.jm("a", 1, "b", 2), "b", 22);   // {a=1, b=22}

pathify renders a path list as a dot-string:

Struct.pathify(Struct.jt("a", "b", "c"));   // "a.b.c"

typify returns a bit-field combining a kind flag with a specific type; typename looks up the human-friendly name:

Struct.typify(1);                        // T_scalar | T_number | T_integer  (201326720)
Struct.typename(8192);                    // "map"  (8192 == T_map)

escre escapes regex metacharacters; escurl percent-encodes for URLs:

Struct.escre("a.b+c");                    // "a\\.b\\+c"
Struct.escurl("hello world?");            // "hello%20world%3F"

join concatenates a list with a separator (the third arg is URL-collapse mode):

Struct.join(Struct.jt("a", "b", "c"), "/", false);   // "a/b/c"

clone deep-copies a node:

Struct.clone(Struct.jm("a", Struct.jm("b", Struct.jt(1, 2))));   // {a={b=[1, 2]}}  (a deep copy)

flatten collapses nested lists one level by default:

Struct.flatten(Struct.jt(1, Struct.jt(2, Struct.jt(3))));   // [1, 2, [3]]  (one level by default)

merge deep-merges a list of nodes — last input wins, maps deep-merge, lists merge by index:

Struct.merge(Struct.jt(
    Struct.jm("a", 1, "b", 2, "k", Struct.jt(10, 20), "x", Struct.jm("y", 5, "z", 6)),
    Struct.jm("b", 3, "d", 4, "e", 8, "k", Struct.jt(11), "x", Struct.jm("y", 7))));
// {a=1, b=3, d=4, e=8, k=[11, 20], x={y=7, z=6}}

inject replaces backtick refs in strings with values from the store:

Struct.inject(Struct.jm("x", "`a`", "y", 2), Struct.jm("a", 1));   // {x=1, y=2}

validate checks data against a shape spec (throws on mismatch):

Struct.validate(Struct.jm("name", "Ada", "age", 36),
    Struct.jm("name", "`$STRING`", "age", "`$INTEGER`"));   // {name=Ada, age=36}

select finds children matching a query, tagging each with its $KEY:

Struct.select(
    Struct.jm("a", Struct.jm("name", "Alice", "age", 30),
              "b", Struct.jm("name", "Bob", "age", 25)),
    Struct.jm("age", 30));
// [{name=Alice, age=30, $KEY=a}]

Constants

Type bit-flags

All 15 are present as int constants on Struct:

Struct.T_any        Struct.T_noval     Struct.T_boolean
Struct.T_decimal    Struct.T_integer   Struct.T_number
Struct.T_string     Struct.T_function  Struct.T_symbol
Struct.T_null       Struct.T_list      Struct.T_map
Struct.T_instance   Struct.T_scalar    Struct.T_node

Sentinels & mode flags

Struct.SKIP / Struct.DELETE (marker maps); Struct.M_KEYPRE / M_KEYPOST / M_VAL and MODENAME.

Notes

Object model

Maps are LinkedHashMap<String,Object> (insertion-ordered, matching the canonical key order) and lists are ArrayList<Object> — both reference-stable, so the "lists are mutable in place" property holds with no wrapper.

null conventions

Java has only null. As in Go, null stands in for both JSON null and "absent"; the shared Group A/B rule (see ../UNDEF_SPEC.md) decides which a given function means, and the corpus uses the __NULL__ sentinel where it must disambiguate.

Tests

A Gson-based corpus runner drives the shared .jsonic fixtures (src/test/), with a committed test-baseline.json. Gson is a test-scope dependency only; the library proper has no third-party runtime dependency (it hand-rolls its JSON printer).

Regex

Uniform six-function regex API (see /design/REGEX_API.md). The Java port wraps java.util.regex.Pattern.

API

Function Maps to
reCompile(pattern) Pattern.compile(pattern) (throws PatternSyntaxException on bad pattern)
reTest(pattern, input) Pattern.compile(pattern).matcher(input).find()
reFind(pattern, input) first match as String[] of [whole, group1, …] or null
reFindAll(pattern, input) List<String[]>
reReplace(pattern, input, repl) matcher.replaceAll(repl)
reEscape(s) escape regex metacharacters

Dialect

Patterns must stay inside the RE2 subset documented in /design/REGEX.md. Java's regex supports backreferences and lookaround; using them will not be portable.

Sharp edges

  • Catastrophic backtracking. java.util.regex is backtracking; the discovery panel sees P1 (^(a+)+$ over 22 a's plus !) in ~13 ms here. Other shapes can be worse. 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.
  • System.out encoding. When printing match results that contain non-ASCII characters, System.out's default PrintStream uses the platform's default charset, not UTF-8. The discovery panel sees caf? in stdout though the in-memory String is correct UTF-16. Pass -Dfile.encoding=UTF-8 (or use PrintStream(System.out, true, StandardCharsets.UTF_8)) when this matters. Orthogonal to the regex itself.

See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel.

Build and test

cd java
mvn package
make test           # mvn test — runs the shared .jsonic corpus

Tests live in src/test/.