diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 08:41:23 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 08:41:23 +0200 |
| commit | 063d4058346f7dc03eb0aa5ba394b9a872d3f1e3 (patch) | |
| tree | 93d1ff06c91affc6586c64dc0326460b675f9e9b /lib/render/emit_json.ml | |
| parent | 5828571fa31a0480838d1ede210002ab50768c64 (diff) | |
| download | colitur-063d4058346f7dc03eb0aa5ba394b9a872d3f1e3.tar.gz colitur-063d4058346f7dc03eb0aa5ba394b9a872d3f1e3.zip | |
feat(render): CSV and JSON emitters, and the published contract
Both consume the VIEW, not the kernel, so every emitter and every
template describe exactly the same fields -- there is one vocabulary,
not five.
CSV is RFC 4180: a field with a comma is quoted. That is live on real
data, not hypothetical -- 'St. Joseph, Spouse of the Bl. Virgin Mary'
would otherwise split into two columns.
JSON is hand-rolled because the dependency list is frozen and escaping
is the only subtlety. Control characters below 0x20 are \u-escaped per
RFC 8259 section 7.
There are no numbers in the view, deliberately: a consumer never has to
guess whether week is 2 or "2".
schema/day-v1.json pins the shape. Once a phone subscribes or a site
fetches this, it is a promise to strangers -- adding a field is minor,
renaming one means /v2/.
Diffstat (limited to 'lib/render/emit_json.ml')
| -rw-r--r-- | lib/render/emit_json.ml | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/lib/render/emit_json.ml b/lib/render/emit_json.ml new file mode 100644 index 0000000..272e4ca --- /dev/null +++ b/lib/render/emit_json.ml @@ -0,0 +1,47 @@ +(* emit_json.ml *) +module T = Template + +let escape_string s = + let b = Buffer.create (String.length s + 8) in + Buffer.add_char b '"'; + String.iter + (fun c -> + match c with + | '"' -> Buffer.add_string b "\\\"" + | '\\' -> Buffer.add_string b "\\\\" + | '\n' -> Buffer.add_string b "\\n" + | '\r' -> Buffer.add_string b "\\r" + | '\t' -> Buffer.add_string b "\\t" + | c when Char.code c < 0x20 -> Buffer.add_string b (Printf.sprintf "\\u%04x" (Char.code c)) + | c -> Buffer.add_char b c) + s; + Buffer.add_char b '"'; + Buffer.contents b + +(* Emit the view tree directly. Bools stay bools; everything else is a string, + an array or an object -- there are no numbers in the view, deliberately, so a + consumer never has to guess whether "week" is 2 or "2". *) +let rec write b (v : T.value) = + match v with + | T.Str s -> Buffer.add_string b (escape_string s) + | T.Bool x -> Buffer.add_string b (if x then "true" else "false") + | T.List l -> + Buffer.add_char b '['; + List.iteri (fun i x -> if i > 0 then Buffer.add_char b ','; write b x) l; + Buffer.add_char b ']' + | T.Obj kvs -> + Buffer.add_char b '{'; + List.iteri + (fun i (k, x) -> + if i > 0 then Buffer.add_char b ','; + Buffer.add_string b (escape_string k); + Buffer.add_char b ':'; + write b x) + kvs; + Buffer.add_char b '}' + +let year v = + let b = Buffer.create (64 * 1024) in + write b v; + Buffer.add_char b '\n'; + Buffer.contents b |
