aboutsummaryrefslogtreecommitdiff
path: root/lib/render/emit_json.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 08:41:23 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 08:41:23 +0200
commit063d4058346f7dc03eb0aa5ba394b9a872d3f1e3 (patch)
tree93d1ff06c91affc6586c64dc0326460b675f9e9b /lib/render/emit_json.ml
parent5828571fa31a0480838d1ede210002ab50768c64 (diff)
downloadcolitur-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.ml47
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