Skip to content
Emerging-PatternsPublic

About

JSON for Bend 2

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ezjson

JSON for Bend 2.

Install

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

Usage

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))

Specification

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).

About

JSON for Bend 2

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages