json/src/main.nika

// Parses a JSON file into a tree and prints it back, two spaces per level.
//
//     nikaia run -- tests/document.in/data.json
//     {
//       "name": "a\nb",
//       …
//     }
//     longest string: 3 characters
//
// Any JSON file will do; a one-line one is enough:
// `{"name": "a\nb", "tags": [1, 2.5, true, null], "empty": {}}`.
//
// Shows: an `enum` as a recursive tree, a recursive `grammar`, strings kept as
// views of the input and decoded only when asked, and a `match` the compiler
// checks for missing cases.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints against `tests/` (ADR-269 D13).

use std::fs
use std::cli

// A JSON value is one of six things, which is what an enum says. `Array` and
// `Object` hold more `Json`, so the type is a tree of any depth.
//
// `Text` holds a view into the input, so `Json` and `Member` are tied to the
// text they were parsed from. Neither declaration has to say so: the compiler
// works it out, through the cycle.
enum Json {
    Null,
    Bool(bool),
    Number(f64),
    Text(String),
    Array(Vec[Json]),
    Object(Vec[Member]),
}

pub struct Member {
    key: String,
    value: Json,
}

grammar Json_ {

    // A lowercase rule skips whitespace between its parts, so nothing below
    // mentions blanks, and `[1, 2]` and a document spread over forty lines
    // parse the same. `value` reaches `value` again through `[` and `{`.
    entry rule value -> Json =
          "null" -> { Json::Null }
        | "true" -> { Json::Bool(true) }
        | "false" -> { Json::Bool(false) }
        | n:NUMBER -> { Json::Number(n) }
        | s:TEXT -> { Json::Text(s) }
        | "[" "]" -> { Json::Array(Vec()) }
        | "[" items:values "]" -> { Json::Array(items) }
        | "{" "}" -> { Json::Object(Vec()) }
        | "{" members:members "}" -> { Json::Object(members) }

    // A comma-separated list: a head and a repetition, collected in the
    // action. `values = values "," value` would never terminate.
    rule values -> Vec[Json] =
        head:value tail:value_tail* -> {
            let mut all = Vec()
            all.push(head)
            for v in tail.drain() { all.push(v) }
            all
        }

    rule value_tail -> Json = "," v:value -> { v }

    rule members -> Vec[Member] =
        head:member tail:member_tail* -> {
            let mut all = Vec()
            all.push(head)
            for m in tail.drain() { all.push(m) }
            all
        }

    rule member_tail -> Member = "," m:member -> { m }

    rule member -> Member = k:TEXT ":" v:value -> { Member { key: k, value: v } }

    // An uppercase rule skips nothing, so the sign, the digits and the fraction
    // are one run with nothing between them. `dec[f64](…)` converts the text
    // the pattern matched.
    rule NUMBER -> f64 =
        n:dec[f64](text("-"? digit+ ("." digit+)? )) -> { n }

    // The body between the quotes, raw: escapes are kept as written. The body
    // `a\nb` is four characters in the file and three once decoded, so a JSON
    // string cannot simply be a slice of the input; the raw body can.
    //
    // An escape is a backslash and whatever follows it; anything else is a
    // character that is neither a quote nor a backslash. `text(…)` hands back
    // the input the repetition covered, as a view: one slice, no copy.
    rule TEXT -> String = "\"" s:text((ESC | PLAIN)*) "\"" -> { s }

    rule ESC = "\\" any -> { }
    rule PLAIN = not("\"") not("\\") any -> { }
}

// The document, printed back with two spaces per level.
//
// Six arms and no `else`: a variant added to `Json` makes this `match` a
// compile error until it is handled.
//
// Strings are printed back raw, as they were in the file, so printing never
// decodes and never has to re-escape.
fn show(value: Json, indent: i64) {
    let pad = "  ".repeat(indent)
    let inner = "  ".repeat(indent + 1)

    match value {
        Json::Null => { print("null") }
        Json::Bool(b) => { if b { print("true") } else { print("false") } }
        Json::Number(n) => { print(f"{n}") }
        Json::Text(raw) => { print(f"\"{raw}\"") }

        Json::Array(items) => {
            if items.len() == 0 {
                print("[]")
            } else {
                print("[\n")
                let mut first = true
                for item in items.drain() {
                    if !first { print(",\n") }
                    first = false
                    print(f"{inner}")
                    show(item, indent + 1)
                }
                print(f"\n{pad}]")
            }
        }

        // A brace in a plain string is just a brace: `print("{}")` prints `{}`.
        // Only an f-string reads `{…}`, and there `}}` stands for one `}`.
        Json::Object(fields) => {
            if fields.len() == 0 {
                print("{}")
            } else {
                print("{\n")
                let mut first = true
                for field in fields.drain() {
                    if !first { print(",\n") }
                    first = false
                    print(f"{inner}\"{field.key}\": ")
                    show(field.value, indent + 1)
                }
                print(f"\n{pad}}}")
            }
        }
    }
}

// What a JSON escape means. This is the only place a string is decoded.
//
// `\u` is not handled: `\u0041` comes out as `u0041`, not `A`.
fn unescape(raw: String) -> String {
    let mut out = String()
    let mut escaped = false

    for c in raw.chars() {
        if escaped {
            match c {
                'n' => { out.push('\n') }
                't' => { out.push('\t') }
                'r' => { out.push('\r') }
                'b' => { out.push_str("\u{0008}") }
                'f' => { out.push_str("\u{000C}") }
                // `\"`, `\\` and `\/` stand for themselves.
                else => { out.push(c) }
            }
            escaped = false
        } else {
            if c == '\\' { escaped = true } else { out.push(c) }
        }
    }

    return out
}

// The longest decoded string in the document, in characters. It takes the
// tree by `ref` because `main` still prints it afterwards; `show` takes it by
// value, being the last to look.
fn longest(value: ref Json) -> i64 {
    match value {
        Json::Text(raw) => return unescape(raw).chars().count() as i64

        Json::Array(items) => {
            let mut best = 0
            for item in items {
                let n = longest(item)
                if n > best { best = n }
            }
            return best
        }

        Json::Object(fields) => {
            let mut best = 0
            for field in fields {
                let n = longest(field.value)
                if n > best { best = n }
            }
            return best
        }

        else => return 0
    }
}

fn main() throws {
    let path = cli::args().nth(1) ?? "data.json"

    let text = fs::read_to_string(ref path, fs::Root::Anywhere) catch {
        eprintln(f"cannot read {path}: {error}")
        return
    }

    let document = Json_::value(text) catch {
        eprintln(f"{path}: {error}")
        return
    }

    let widest = longest(document)

    show(document, 0)
    println("")
    println(f"longest string: {widest} characters")
}

The file itself · all the examples

This site is open source. Improve this page.