# tabnas-json (Rust)
Standard JSON grammar plugin for the
[`tabnas`](https://github.com/tabnas/parser) 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`](../ts); the TypeScript version is authoritative and this crate
tracks it.
## Documentation
Full [Diátaxis](https://diataxis.fr) docs:
- [`doc/tutorial.md`](doc/tutorial.md). Learn it step by step.
- [`doc/guide.md`](doc/guide.md). Task-focused recipes.
- [`doc/reference.md`](doc/reference.md). The exact API surface.
- [`doc/concepts.md`](doc/concepts.md). How it works, including the
differences from the TypeScript version.
TypeScript is canonical; its docs are in [`../ts/doc/`](../ts/doc/), and
the Go port has the equivalent set in [`../go/doc/`](../go/doc/).
## Use
```rust
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:
```rust
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`:
```rust
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:
```toml
[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:
```bash
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](https://github.com/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.