Skip to content

v0.5.0 — RON spec v0.4.0: escapes, output modes, canonical RON

Latest

Choose a tag to compare

@mbolli mbolli released this 31 Aug 17:52
· 1 commit to master since this release

Tracks RON spec v0.4.0 and
ron-go v0.1.1. The pinned conformance corpus moves
from v0.1.0 to v0.4.0. This release is breaking in both the API and the output bytes.

Added

  • JSON escapes in every string form. Bare strings, '/" strings, repeated-delimiter
    strings, comma-prefixed tokens and object keys all accept \" \\ \/ \b \f \n \r \t \uXXXX,
    including surrogate pairs. An escape is a single scanner atom, so a\u0020b stays one token.
    Type classification happens before decoding, so tr\u0075e is the string "true".
    Unknown, truncated and non-hex escapes, unpaired surrogates and raw U+0000-U+001F in string
    content are now rejected. The renderer escapes backslash and C0 controls before deciding
    bare vs quoted, so "a\nb" renders as bare a\nb and a literal backslash is doubled.
  • RonMode enum (Pretty, Compact, Canonical) replacing the $pretty/$canonical flags.
  • Real canonical mode. RonMode::Canonical applies the RFC 8785 and RFC 7493 (I-JSON) contract:
    duplicate decoded member names, invalid Unicode, Unicode noncharacters and numbers whose
    IEEE 754 conversion is non-finite are rejected, and numbers are re-serialized with the
    ECMAScript algorithm.
  • Ron::format() re-renders RON source in a given mode (RON -> RON), which is how canonical RON
    and its hash are derived from RON rather than JSON input.
  • set vocabulary: #set (deduplicated and sorted by each element's RFC 8785 canonical JSON
    bytes) and #bits (uint32 indexes normalized to ascending, merged, inclusive ranges).
  • #topo (TopoJSON topology) in the geo vocabulary. Validated but never expanded: shared arcs
    stay indexed, negative indexes stay unreversed, quantized coordinates keep their encoding.
  • RonObject::has() and Utf8::encodeRune().

Changed

  • BREAKING: toJson(), fromJson(), encode() and RonRenderer::__construct() take a
    RonMode instead of bool $pretty, bool $canonical.
  • BREAKING: toJson() now defaults to pretty output, matching the spec's defaultMode and
    ron-go. Pass RonMode::Compact for the previous default.
  • BREAKING: pretty and compact output preserve source member order. Only RonMode::Canonical
    sorts keys.
  • BREAKING: canonical output no longer preserves number spelling (1E2 canonicalizes to
    100, 9007199254740993 to 9007199254740992). Pretty and compact still preserve it.
  • RFC 8785 canonicalization accepts a finite number that rounds during IEEE 754 conversion; only
    a non-finite conversion is an error. The former "integer is not exactly representable"
    rejection is gone, following the corpus.
  • N-quoted strings are delimiter-aware: a same-quote run shorter than the opening run and every
    occurrence of the other quote byte are content. The (n - 2) % 3 compatibility form is now
    apostrophe-only; a double-quote run never takes it.
  • A comma directly after an object key starts the value token (k ,foo is the string ",foo")
    instead of being skipped as a separator.
  • Bare tokens end at Unicode whitespace, not only at ASCII delimiters.
  • #rx payloads carry doubled backslashes on the wire now that every RON string decodes escapes:
    JavaScript \d is written as RON \\d.

Performance

Measured best-of-N on one ~31 KB document with OPcache + JIT, against v0.4.1 on the same
machine. Pretty and compact got faster because they no longer sort object keys, and the string
renderer now answers "needs escaping?" and "needs quoting?" with one strcspn over a combined
mask instead of two scans plus an mb_check_encoding. The token scanner keeps its old 11-byte
delimiter mask for sources that contain no escape, no raw control and no Unicode-whitespace lead
byte, decided by a single pass in Scanner::setSource() — without it the 45-byte escape-aware
mask cost ~20% on token-heavy input, because strcspn rebuilds a 256-byte table per call.

Conversion v0.4.1 v0.5.0
JSON -> RON (compact) ~26 MB/s ~29 MB/s
JSON -> RON (pretty) ~20 MB/s ~26 MB/s
RON -> JSON (compact) ~20 MB/s ~19 MB/s
canonical hash ~26 MB/s ~13 MB/s

The canonical hash is ~2x slower and stays that way by design: it used to be compact rendering
with sorted keys, and now validates the whole RFC 8785 / I-JSON contract and re-serializes every
number. Documents made of long quoted strings convert ~9% slower (~222 -> ~203 MB/s) because the
content scan is escape-aware; that path runs an order of magnitude above the token-bound one, so
it was left alone.

Removed

  • BREAKING: #lla (LngLatAlt) from the spatial vocabulary, removed upstream. Geographic
    positions belong in #geo, whose positions accept an optional third altitude element.

Fixed

  • Unicode noncharacters (U+FDD0-U+FDEF and every plane's U+xFFFE/U+xFFFF) are rejected in RFC 8785
    canonical JSON, whether written directly or via \uXXXX.

Upgrading from 0.4.x

// before
Ron::toJson($ron);                     // compact, sorted keys
Ron::toJson($ron, pretty: true);
Ron::fromJson($json, pretty: false);
Ron::encode($value, pretty: false);

// after
use Mbolli\Ron\RonMode;

Ron::toJson($ron, RonMode::Compact);   // compact, source order
Ron::toJson($ron);                     // pretty is now the default
Ron::fromJson($json, RonMode::Compact);
Ron::encode($value, RonMode::Compact);

Sorted keys now mean RonMode::Canonical, which also validates the RFC 8785 / I-JSON contract
and re-serializes numbers — it is not a drop-in for the old canonical: true flag on pretty or
compact output. Ron::canonicalRon(), Ron::canonicalHash() and Ron::canonicalJson() are
unchanged in signature.

Full changelog: CHANGELOG.md ·
v0.4.1...v0.5.0