json

A JSON (RFC 8259) parser and renderer in pure Dawn that keeps integers exact and reports errors a caller can branch on.

use json/parser.{parse}
use json/render.{render}
use json/value.{error_text}

fn reformat(text: String) -> String =
  match parse(text) {
    Ok(j) -> render(j)
    Err(e) -> error_text(e)
  }

Contract

Changes between versions: CHANGELOG.md.

API

json/lexer

lexer.peek

fn peek(src: String, i: Cursor) -> Int

The code point at i, or -1 at the end of the document.

An Int and not a Char: the end-of-input answer is a sentinel, and a sentinel that is not a character cannot live in a type whose every value is one (spec §4.8). So this scanner compares code points, and spells the constants it compares against as char.code('x') -- the character is what is readable, the Int is what is being compared.

lexer.offset

fn offset(src: String, i: Cursor) -> Int

The code-point offset of i in src, as JsonError.offset reports it. O(n) in the offset: meant for the failure path, not for every token.

lexer.err_at

fn err_at(src: String, i: Cursor, msg: String) -> JsonError

A JsonError at i carrying msg, of kind Truncated when i is at the end of src and Syntax otherwise. err_kind is the same with the kind stated.

lexer.err_kind

fn err_kind(src: String, i: Cursor, kind: JsonErrorKind, msg: String) -> JsonError

An error of a stated kind at i.

lexer.is_digit

fn is_digit(c: Int) -> Bool

Whether the code point c is an ASCII digit 0-9; -1 (the end of the document, as peek reports it) is not.

lexer.skip_ws

fn skip_ws(src: String, i: Cursor) -> Cursor

The first cursor at or after i that is not JSON whitespace (space, tab, line feed, carriage return). At the end of the document it is the end.

lexer.scan_string

fn scan_string(src: String, start: Cursor) -> Result[(String, Cursor), JsonError]

The JSON string literal whose opening quote is at start, escapes decoded, with the cursor just past its closing quote; an Err for an unterminated literal, a raw control character, or a malformed escape.

lexer.scan_number

fn scan_number(src: String, start: Cursor) -> Result[(String, Cursor), JsonError]

The JSON number literal starting at start, as its source text, with the cursor just past it; an Err when the text breaks the JSON number grammar. The text and not a value, so the caller picks a lossless conversion (Int or Float).

json/parser

parser.MAX_DEPTH

const MAX_DEPTH: Int

How deeply arrays and objects may nest before parse gives up. Deeper input is a plain error, not a blown stack.

parser.parse

fn parse(input: String) -> Result[Json, JsonError]

Parse one complete JSON document (RFC 8259). Err is a value.JsonError: the kind of failure, the code-point offset it was noticed at, and a sentence; value.error_text renders the last two as the one-line message this used to return in place of the whole thing. Anything but whitespace after the value is an error. Never panics. An integer literal becomes JInt, any other number JNum. Integers must fit Int; fraction/exponent literals use IEEE-754 binary64 ties-to-even rounding, accept finite underflow as signed zero, and reject overflow when the result is non-finite.

json/render

render.render

fn render(j: Json) -> String

Serialize a Json value to compact JSON text — no spaces, object keys in the Map's insertion order. Total, and the output is always legal JSON: every code point below U+0020 is escaped and a non-finite JNum renders as null.

json/value

impls registered by this module (visible program-wide, no use needed): Show[Json], Show[JsonErrorKind], Show[JsonError]

Json

type Json = JNull | JBool(value: Bool) | JNum(value: Float) | JInt(value: Int) | JStr(value: String) | JArr(items: List[Json]) | JObj(entries: Map[String, Json])

The JSON data model (RFC 8259): null, booleans, numbers, strings, arrays and objects, which preserve insertion order via the builtin Map. A number is JInt when its literal carries no '.', 'e' or 'E', and JNum otherwise.

JsonErrorKind

type JsonErrorKind = Syntax | Truncated | Trailing | Depth | NumberRange(literal: String)

Why a parse failed, in the terms a caller can act on: input that ran out, a value followed by more input, nesting past MAX_DEPTH, a number outside Dawn's model (carrying its literal), or a plain syntax error.

JsonError

type JsonError = { kind: JsonErrorKind, offset: Int, message: String }

What parse returns on failure: the kind, the code-point offset it was noticed at, and the sentence a human reads. error_text renders the last two in the shape the old String error had.

value.error_text

fn error_text(e: JsonError) -> String

A JsonError as the one-line message the String error used to be: "unexpected character at offset 6". The migration from that error type is this call.