vyrn

std/jsondec

42 exports, from std/jsondec.vyrn.

The untyped half of fromJson, and the generator of the typed half.

jsonDecoders writes the typed half per target type. What it writes reads the source once through a JsonReader and decodes each value into its slot; it builds no Json tree. It calls this module for the reader, Issues, paths and scalar decoders. Decoding accumulates: a decoder records its failure and returns, and a composite constructs only when every part succeeded. So every decoder returns an Array<T> of zero or one element; empty means the issue is already recorded. Option<T> cannot serve: with T = Option<U> it has no wire form. Every message is the canonical wording, byte for byte. A refused document yields its json.parse Issue alone, and a record's Issues follow its members' declaration order, whatever the document's key order.

The tree accessors (fieldsOf, fieldAt and the rest) serve programs that read a Json from parseJson.

Dependencies

The import graph shows the whole library.

fn tagOf(v: Json) -> String

A bare string's content (a nullary enum variant's wire form), or "". No variant can be named "", so the sentinel cannot collide with a real tag.

fn pushType(iss: modify Array<Issue>, path: String, expected: String, found: String) -> Unit

json.type: expected <what>, found <kind>.

fn pushValidate(iss: modify Array<Issue>, path: String, message: String) -> Unit

validate: a refined type's where clause did not hold. The caller passes the compiler's validation message, so the trap and this path word it the same.

fn pathAt(path: String, seg: String, idx: Int64) -> String

The path of a value a decoder reads: path itself when idx is -2, path[idx] for an element, and path.seg for a member when idx is -1. A decoder builds it only for an Issue or for its own members, so a value that decodes builds no path.

fn expect(
    p: modify JsonReader,
    path: String,
    seg: String,
    idx: Int64,
    iss: modify Array<Issue>,
    kind: Int64,
    what: String,
) -> Bool

Whether the value at the cursor is of kind, a kindAt kind. If it is not, records expected <what>, found <kind> at the value's path (see pathAt) and reads the value past.

fn readNull(p: modify JsonReader) -> Bool

Reads a null at the cursor and answers whether there was one: the wire form of an absent Option.

fn member(p: modify JsonReader, seen: Bool, key: String) -> Unit

Reads past the : after a record member's key, or refuses the key as a duplicate when the member was seen.

fn mapMember(p: modify JsonReader, keys: modify JsonKeySet, key: String) -> Bool

Records key in keys and reads past the :, answering true; or refuses the key as a duplicate and answers false.

fn skipMember(p: modify JsonReader, keys: modify JsonKeySet, key: String) -> Unit

Reads a member no field claims: an unknown field is ignored, but its key may not repeat and its value must be JSON.

fn takeIssues(iss: modify Array<Issue>, base: Int64) -> Array<Issue>

Moves iss[base..] out, in order: a record puts its members' Issues back in declaration order with putIssues.

fn dInt64(p: modify JsonReader, path: String, seg: String, idx: Int64, iss: modify Array<Issue>) -> Array<Int64>

A JSON integer-syntax number as an exact Int64. A fraction, an exponent or a magnitude outside Int64 is expected integer, never a rounded value.

fn dIntKey(key: String, path: String, iss: modify Array<Issue>) -> Array<Int64>

An object key read as an Int64, the wire form of Map<Int64, V>, in the map at path. Only the canonical text (n.toString()) passes: "007" and "+7" would otherwise collapse into the key "7" with no Issue.

fn dIntRange(
    p: modify JsonReader,
    path: String,
    seg: String,
    idx: Int64,
    iss: modify Array<Issue>,
    lo: Int64,
    hi: Int64,
) -> Array<Int64>

dInt64 restricted to [lo, hi], for the sized signed integers.

fn dUIntMax(
    p: modify JsonReader,
    path: String,
    seg: String,
    idx: Int64,
    iss: modify Array<Issue>,
    hi: UInt64,
) -> Array<UInt64>

A JSON integer-syntax number as an exact UInt64 in [0, hi]. parseInt64 cannot serve: it has no room above Int64.max.

fn dFloat64(p: modify JsonReader, path: String, seg: String, idx: Int64, iss: modify Array<Issue>) -> Array<Float64>

A JSON number as a Float64, correctly rounded (std/num). Integer syntax is accepted: 1 decodes into a Float64 target as 1.0.

fn dFloat32(p: modify JsonReader, path: String, seg: String, idx: Int64, iss: modify Array<Issue>) -> Array<Float32>

A JSON number as a Float32, rounded once: decimal -> Float64 -> Float32 rounds twice and is wrong near a Float32 tie.

gen fn jsonDecoders(t: TypeArg) -> String

Writes the decoders fromJson calls: for each root, the entry that reads a document into Validation<T>, and for each node, the function that decodes the value at a reader's cursor into zero or one value. Generated source cannot spell reserved names, so it writes VyrnRt_ for std/json's, VyrnRd_ for this module's and VyrnWp_ for a where type's predicate.

Tests

7 test blocks in std/jsondec.vyrn. CI runs all of them.

Test names
  • a decode result is zero or one element
  • integers decode exactly, never through a Float64
  • a sized integer out of range is a type issue, not a wrap
  • a wrong kind is a type issue, and the value is read past
  • paths compose the canonical way
  • a parse error is one issue, keyed json.parse, worded line/col
  • a string's content is its tag

Examples

1 program in examples/ import this module. Each runs as WebAssembly and as a native binary.