summaryrefslogtreecommitdiff
path: root/test
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 /test
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 'test')
-rw-r--r--test/test_colitur.ml3
-rw-r--r--test/test_emit.ml77
2 files changed, 79 insertions, 1 deletions
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index dadc77d..b61eed6 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -10,4 +10,5 @@ let () =
Test_escape.suite;
Test_template.suite;
Test_template.render_suite;
- Test_view.suite ]
+ Test_view.suite;
+ Test_emit.suite ]
diff --git a/test/test_emit.ml b/test/test_emit.ml
new file mode 100644
index 0000000..a8fd45c
--- /dev/null
+++ b/test/test_emit.ml
@@ -0,0 +1,77 @@
+module V = Colitur_render.View
+module Csv = Colitur_render.Emit_csv
+module Json = Colitur_render.Emit_json
+
+let view_2027 () = Test_view.view_of 2027
+
+let test_csv_header_and_rows () =
+ let out = Csv.year (view_2027 ()) in
+ let lines = String.split_on_char '\n' out |> List.filter (fun l -> l <> "") in
+ Alcotest.(check int) "366 lines: header + 365 days" 366 (List.length lines);
+ Alcotest.(check string) "header"
+ "date,rite,season,week,slug,rank,colour,subject,name_la,name_en,first,gospel,comms"
+ (List.hd lines);
+ Alcotest.(check bool) "first row is 1 January" true
+ (String.length (List.nth lines 1) > 10 && String.sub (List.nth lines 1) 0 10 = "2027-01-01")
+
+(* RFC 4180: a field containing a comma, quote or newline is quoted, and an
+ embedded quote is doubled. Feast names contain commas ("St. Joseph, Spouse
+ of the Bl. Virgin Mary"), so this is live on real data, not hypothetical. *)
+let test_csv_quotes_commas () =
+ Alcotest.(check string) "comma quoted" "\"a,b\"" (Csv.escape_field "a,b");
+ Alcotest.(check string) "quote doubled" "\"a\"\"b\"" (Csv.escape_field "a\"b");
+ Alcotest.(check string) "plain unquoted" "ab" (Csv.escape_field "ab");
+ let out = Csv.year (view_2027 ()) in
+ let joseph =
+ List.find
+ (fun l -> String.length l > 10 && String.sub l 0 10 = "2027-03-19")
+ (String.split_on_char '\n' out)
+ in
+ Alcotest.(check bool) "the comma in Joseph's name is quoted, not a field break" true
+ (String.length (String.split_on_char ',' joseph |> List.hd) = 10)
+
+let test_json_parses_back () =
+ let out = Json.year (view_2027 ()) in
+ Alcotest.(check bool) "starts as an object" true (out.[0] = '{');
+ let count sub =
+ let n = String.length sub in
+ let rec go i acc =
+ if i + n > String.length out then acc
+ else go (i + 1) (if String.sub out i n = sub then acc + 1 else acc)
+ in
+ go 0 0
+ in
+ Alcotest.(check bool) "has a days array" true (count "\"days\":[" >= 1);
+ (* Every ISO date the view produced appears in the JSON -- that is the
+ property under test, not a specific occurrence count. The count is NOT
+ 1: the view deliberately offers both artefacts at every level (view.mli)
+ -- the flat top-level [days], each month's own [days], and that month's
+ [weeks] grid cell -- so an ordinary date's "iso" field appears 3 times.
+ Verified against the real 2027 engine output (all 365 dates checked);
+ the one exception is legitimate, not a bug: 2027-04-05 appears 6 times
+ because the Annunciation (25 March, impeded by Holy Week) transfers to
+ it under RG 96/98, and its own "to" target string is the identical
+ literal, tripled by the same structural redundancy. *)
+ Alcotest.(check int) "1 January appears (flat days + month days + week grid)" 3
+ (count "\"2027-01-01\"");
+ Alcotest.(check int) "31 December appears (flat days + month days + week grid)" 3
+ (count "\"2027-12-31\"")
+
+let test_json_escapes () =
+ Alcotest.(check string) "quote" "\"a\\\"b\"" (Json.escape_string "a\"b");
+ Alcotest.(check string) "backslash" "\"a\\\\b\"" (Json.escape_string "a\\b");
+ Alcotest.(check string) "newline" "\"a\\nb\"" (Json.escape_string "a\nb");
+ Alcotest.(check string) "tab" "\"a\\tb\"" (Json.escape_string "a\tb");
+ (* Control characters below 0x20 must be \u-escaped (RFC 8259 section 7). *)
+ Alcotest.(check string) "control" "\"a\\u0001b\"" (Json.escape_string "a\001b")
+
+let test_utf8_passes_through_json () =
+ Alcotest.(check string) "polish" "\"\xc5\x9awi\xc4\x99tej\"" (Json.escape_string "\xc5\x9awi\xc4\x99tej")
+
+let suite =
+ ( "Emit/csv+json",
+ [ Alcotest.test_case "csv header and rows" `Quick test_csv_header_and_rows;
+ Alcotest.test_case "csv quotes commas" `Quick test_csv_quotes_commas;
+ Alcotest.test_case "json parses back" `Quick test_json_parses_back;
+ Alcotest.test_case "json escapes" `Quick test_json_escapes;
+ Alcotest.test_case "json passes utf8 through" `Quick test_utf8_passes_through_json ] )