tabnas-json 0.5.13

Standard JSON grammar plugin for the tabnas parsing engine
Documentation

tabnas-json (Rust)

Standard JSON grammar plugin for the tabnas parsing engine, crate tabnas_json.

The engine ships no grammar of its own; this crate supplies the strict, standard-JSON one. The rule set (val / map / list / pair / elem) is jsonic's "Plain JSON" grammar, installed on its own with the lexer restricted to strict JSON: double-quoted strings, plain decimal numbers, quoted keys, no comments, no trailing commas.

This is the Rust port of the canonical TypeScript implementation in ../ts; the TypeScript version is authoritative and this crate tracks it.

Documentation

Full Diátaxis docs:

TypeScript is canonical; its docs are in ../ts/doc/, and the Go port has the equivalent set in ../go/doc/.

Use

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let value = tabnas_json::parse(r#"{"a":[1,2]}"#)?;
    println!("{value}");
    Ok(())
}

Or build an instance and reuse it:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let parser = tabnas_json::make();
    let value = parser.parse("[1,2,3]")?;
    println!("{value}");
    Ok(())
}

To layer another grammar on the JSON core, install the plugin on your own instance and add rules on top of the shared val / map / list / pair / elem:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut parser = tabnas::Tabnas::new();
    tabnas_json::json(&mut parser)?;
    Ok(())
}

Install

The tabnas crate is not published to a registry, so the engine is consumed as a sibling checkout, the standard tabnas development model. Clone https://github.com/tabnas/parser next to this repository and point at it:

[dependencies]
tabnas-json = { path = "../json/rs" }
tabnas = { package = "tabnas-parser", path = "../parser/rs" }

Both entries are needed. A crate's dependencies are not passed on to its dependents, so tabnas-json alone does not put tabnas in your extern prelude, and the examples above that name tabnas::Tabnas would not resolve. Only JsonError is re-exported.

Differences from the canonical TypeScript

Four, all deliberate:

  • Out-of-range exponents are rejected. 1e999 is syntactically valid JSON, and the platform parsers disagree about it: JSON.parse saturates to Infinity, while serde_json and encoding/json both error. This package is held to per-runtime parity, so Rust rejects with Go rather than accepting with TypeScript. Underflow (1e-999 to 0) is accepted, as it is in Go.
  • Nesting past 127 levels is rejected. serde_json accepts 127 and refuses one level deeper, while JSON.parse and encoding/json both go much further, so this port follows its own platform again. It is also what keeps a deeply nested source from ending the process: without the limit, a kilobyte of open brackets overflows the stack instead of returning an error.
  • Integer-like object keys keep document order. A JavaScript object enumerates them first, in ascending numeric order, so TypeScript reads {"2":"a","1":"b"} back as 1, 2 where this crate gives 2, 1. An IndexMap does not reorder, and neither does serde_json. Non-index keys keep insertion order in both.
  • Strictness is a check hook, not number.exclude. The TypeScript exclude is a negative lookahead, and the regex crate has no lookaround, so the positive pattern plus an inversion is the only way to express it here. That is the shape the Go port already uses.

Build and test

The engine is a path dependency on the sibling checkout, so there is nothing to fetch:

cargo test --all-targets

Or, from the repository root, make test-rs. For what CI would say, including formatting and the lockfile check, run ci/rust/run.sh.

The suite runs the shared ../test/spec/*.tsv conformance fixtures, the same files the TypeScript and Go suites run, and additionally cross-checks every valid row against serde_json, this runtime's platform oracle, the way the Go runner checks against encoding/json.

It also grades the external nst/JSONTestSuite corpus at a pinned commit, fetching it on first use. Every one of the 283 cases RFC 8259 makes mandatory passes. Eleven of the 35 implementation-defined cases differ from serde_json, each named with its reason in the test; ten are lone surrogates in a \u escape, which this parser turns into U+FFFD rather than rejecting, and one is a 48-digit integer where serde_json is a unit in the last place away from what the Rust standard library returns.

License

MIT.