JSON for Bend 2.
With Bend alone there is nothing to
install: import ezjson by its hub name and version, and bend fetches it
from the hub into ~/.bend/lib on the first
run; no install step. ezjson is built and checked on Bend 2.0.38.
import ezjson@1.4.0.0/main.bend as Ezjson
ezjson@1.4.0.0 is ezjson v1.4.0 and resolves to
0xdab377b48b4edd8247427c10e2120805; to pin by content, import
0xdab377b48b4edd8247427c10e2120805/main.bend instead.
Or with ez, which records the
package in ez.toml (ez init makes one):
ez add Emerging-Patterns/ezjson
parse reads JSON text into Maybe Json: null, booleans, numbers, strings,
arrays, and objects. print writes that value back as compact text, and
writes U+FFFD for a code point no UTF-8 text can hold (a lone surrogate).
pretty writes the same tokens indented two spaces a level, one element or
member a line and a space after each colon, as JavaScript's
JSON.stringify(v, null, 2) does, with no newline at the end. A number
keeps the spelling it was parsed with. num builds one from that text, and
builds null when the text is not a JSON number. The Json type is
src/value.bend's; import that file to name it in a signature, and give a
match on a result its own def, since Bend matches only a parameter.
import ezjson@1.4.0.0/main.bend as Ezjson
import ezjson@1.4.0.0/src/value.bend as Value
def shown(got: Maybe<&2, Value.Json>) -> String:
match got:
case None{}:
""
case Some{j}:
Ezjson.print(j)
def compact(s: String) -> String:
shown(Ezjson.parse(s))
def point() -> String:
Ezjson.print(Ezjson.obj([("x", Ezjson.num("1")), ("y", Ezjson.num("-2")),
("name", Ezjson.str("a")), ("ok", Ezjson.bool(True{})),
("extra", Ezjson.null())]))
null, bool, str, num, arr, and obj build values. get reads an
object key and returns the first value when a key is repeated. at reads an
array index. as_bool, as_str, as_num, as_u32, and as_f32 read a
value of that kind, or none when the kind differs or the number does not fit.
has tells a missing key from a key whose value is null, and len counts
the elements of an array or the members of an object. items lists an
array's elements in order in one walk (a loop over at walks the array again
for every index), and gives [] for anything that is not an array. is_arr
and is_obj tell whether a value is an array or an object.
num.fixed(s, n) writes a number's text with exactly n decimals, as
Python's '%.Nf' does, but rounding half to even on the decimal digits
rather than through a binary float: num.fixed("2.675", 2n) is
Some{"2.68"} where Python's '%.2f' % 2.675 is 2.67. It reads
exponents (1e-05 to 7 places is 0.0000100), keeps a minus on a value
that rounds to zero (-0.04 to 1 place is -0.0), and is None when the
text is not a JSON number. Pass it what as_num reads.
laws/FixedWritesValidNumbers.bend lists
where it and Python's float formatting differ.
import ezjson@1.4.0.0/main.bend as Ezjson
import ezjson@1.4.0.0/src/value.bend as Value
def names.go(users: List<&2, Value.Json>) -> List<&2, Maybe<&2, String>>:
match users:
case Nil{}:
[]
case uu <> rest:
Ezjson.as_str(Ezjson.get(uu, "name")) <> names.go(rest)
# the name of every user in doc's "users" array, in order
def names(doc: Value.Json) -> List<&2, Maybe<&2, String>>:
names.go(Ezjson.items(Ezjson.get(doc, "users")))
A multi-gigabyte text is read with a cursor. parse builds one tree;
cursor does not. next returns one event and the cursor after it. skip
drops the next value (one scalar, or one array or object and everything
inside it) without building that value. text copies the spelling of a
string, a key, or a number. Commas and colons are not events. Import
src/pull.bend to name the cursor and match the event constructors.
cursor holds the unread suffix of the source. A string or key span
(Pull.EStrS, Pull.EKeyS) is the first nn characters of a suffix, not
a copy. The event holds that suffix, so the span stays readable after
next advances, and it is gone when the event is dropped. A number
(Pull.ENum) keeps its own spelling, so one number in a big array does
not hold the rest of the array. Dropping the caller's own string variable
does not drop the cursor.
Walk a large object or array one event at a time. skip drops a subtree
you do not need. text copies an owned string when you need one.
import ezjson@1.4.0.0/main.bend as Ezjson
import ezjson@1.4.0.0/src/pull.bend as Pull
def owned.go(step: (Pull.Ev & Pull.Cur)) -> (String & Pull.Cur):
(ev, rest) = step
(Maybe.default(&2, String, Ezjson.text(ev), ""), Ezjson.skip(rest))
def owned(cur: Pull.Cur) -> (String & Pull.Cur):
owned.go(Ezjson.next(cur))
LAWS.bend lists every behavior ezjson guarantees: conformance
to RFC 8259 and the behavior of
each def in main.bend, one file under laws/ each, proved by the
file of the same name under proofs/ and checked by
bend PROOF.bend (the first line must be ALL PROOFS CHECK). What ezjson
trusts rather than proves is said on the code that assumes it. docs/rfc/ezjson-spec.md has the
reasoning and the rollout.
scale/ is a compiled size check that pull-walks large string-row and
nested-array documents without calling parse: 32000 string rows
(~563 KiB) and 35000 nested rows of [0.100,0.200,0.3] (~615 KiB).