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