···11+(** Test executable for verifying jsont location tracking
22+33+ Usage: test_location_errors <file> [field]
44+55+ Parses JSON feed files and outputs JSON with either:
66+ - Success: {"status":"ok", "field":"<field>", "value":"<value>"}
77+ - Error: {"status":"error", "message":"...", "location":{...}, "context":"..."}
88+*)
99+1010+open Jsonfeed
1111+1212+(* Helper to format path context *)
1313+let format_context (ctx : Jsont.Error.Context.t) =
1414+ if Jsont.Error.Context.is_empty ctx then
1515+ "$"
1616+ else
1717+ let indices = ctx in
1818+ let rec format_path acc = function
1919+ | [] -> if acc = "" then "$" else "$" ^ acc
2020+ | ((_kinded_sort, _meta), idx) :: rest ->
2121+ let segment = match idx with
2222+ | Jsont.Path.Mem (name, _meta) -> "." ^ name
2323+ | Jsont.Path.Nth (n, _meta) -> "[" ^ string_of_int n ^ "]"
2424+ in
2525+ format_path (acc ^ segment) rest
2626+ in
2727+ format_path "" indices
2828+2929+(* Extract field from successfully parsed feed *)
3030+let extract_field field feed =
3131+ match field with
3232+ | "title" -> Jsonfeed.title feed
3333+ | "version" -> Jsonfeed.version feed
3434+ | "item_count" -> string_of_int (List.length (Jsonfeed.items feed))
3535+ | "first_item_id" ->
3636+ (match Jsonfeed.items feed with
3737+ | [] -> "(no items)"
3838+ | item :: _ -> Item.id item)
3939+ | _ -> "(unknown field)"
4040+4141+(* Escape JSON strings *)
4242+let escape_json_string s =
4343+ let buf = Buffer.create (String.length s) in
4444+ String.iter (function
4545+ | '"' -> Buffer.add_string buf "\\\""
4646+ | '\\' -> Buffer.add_string buf "\\\\"
4747+ | '\n' -> Buffer.add_string buf "\\n"
4848+ | '\r' -> Buffer.add_string buf "\\r"
4949+ | '\t' -> Buffer.add_string buf "\\t"
5050+ | c when c < ' ' -> Printf.bprintf buf "\\u%04x" (Char.code c)
5151+ | c -> Buffer.add_char buf c
5252+ ) s;
5353+ Buffer.contents buf
5454+5555+(* Output success as JSON *)
5656+let output_success field value =
5757+ Printf.printf {|{"status":"ok","field":"%s","value":"%s"}|}
5858+ (escape_json_string field)
5959+ (escape_json_string value);
6060+ print_newline ()
6161+6262+(* Output error as JSON *)
6363+let output_error (ctx, meta, kind) =
6464+ let message = Jsont.Error.kind_to_string kind in
6565+ let textloc = Jsont.Meta.textloc meta in
6666+ let file = Jsont.Textloc.file textloc in
6767+ let first_byte = Jsont.Textloc.first_byte textloc in
6868+ let last_byte = Jsont.Textloc.last_byte textloc in
6969+ let (line_num, line_start_byte) = Jsont.Textloc.first_line textloc in
7070+ let column = first_byte - line_start_byte + 1 in
7171+ let context = format_context ctx in
7272+7373+ Printf.printf {|{"status":"error","message":"%s","location":{"file":"%s","line":%d,"column":%d,"byte_start":%d,"byte_end":%d},"context":"%s"}|}
7474+ (escape_json_string message)
7575+ (escape_json_string file)
7676+ line_num
7777+ column
7878+ first_byte
7979+ last_byte
8080+ (escape_json_string context);
8181+ print_newline ()
8282+8383+let main () =
8484+ (* Disable ANSI styling in error messages for consistent output *)
8585+ Jsont.Error.disable_ansi_styler ();
8686+8787+ if Array.length Sys.argv < 2 then (
8888+ Printf.eprintf "Usage: %s <file> [field]\n" Sys.argv.(0);
8989+ Printf.eprintf "Fields: title, version, item_count, first_item_id\n";
9090+ exit 1
9191+ );
9292+9393+ let file = Sys.argv.(1) in
9494+ let field = if Array.length Sys.argv > 2 then Sys.argv.(2) else "title" in
9595+9696+ (* Read file *)
9797+ let content =
9898+ try
9999+ In_channel.with_open_text file In_channel.input_all
100100+ with Sys_error msg ->
101101+ Printf.printf {|{"status":"error","message":"File error: %s"}|}
102102+ (escape_json_string msg);
103103+ print_newline ();
104104+ exit 1
105105+ in
106106+107107+ (* Parse with location tracking *)
108108+ match Jsonfeed.decode_string ~locs:true ~file content with
109109+ | Ok feed ->
110110+ let value = extract_field field feed in
111111+ output_success field value
112112+ | Error err ->
113113+ output_error err;
114114+ exit 1
115115+116116+let () = main ()
+127
test/test_locations.t
···11+Location tracking tests for JSON Feed parser
22+===========================================
33+44+This test suite verifies that jsont combinators correctly track location
55+information for both valid and invalid JSON feeds.
66+77+Valid Feeds
88+-----------
99+1010+Test minimal valid feed:
1111+ $ ./test_location_errors.exe data/minimal_valid.json title
1212+ {"status":"ok","field":"title","value":"Minimal Feed"}
1313+1414+ $ ./test_location_errors.exe data/minimal_valid.json version
1515+ {"status":"ok","field":"version","value":"https://jsonfeed.org/version/1.1"}
1616+1717+ $ ./test_location_errors.exe data/minimal_valid.json item_count
1818+ {"status":"ok","field":"item_count","value":"0"}
1919+2020+Test complete feed with all fields:
2121+ $ ./test_location_errors.exe data/complete_valid.json title
2222+ {"status":"ok","field":"title","value":"Complete Feed"}
2323+2424+ $ ./test_location_errors.exe data/complete_valid.json item_count
2525+ {"status":"ok","field":"item_count","value":"1"}
2626+2727+ $ ./test_location_errors.exe data/complete_valid.json first_item_id
2828+ {"status":"ok","field":"first_item_id","value":"https://example.com/item1"}
2929+3030+Test mixed content types:
3131+ $ ./test_location_errors.exe data/mixed_content.json item_count
3232+ {"status":"ok","field":"item_count","value":"3"}
3333+3434+Test feed with extensions:
3535+ $ ./test_location_errors.exe data/with_extensions.json title
3636+ {"status":"ok","field":"title","value":"Feed with Extensions"}
3737+3838+3939+Missing Required Fields
4040+------------------------
4141+4242+Test missing title field:
4343+ $ ./test_location_errors.exe data/missing_title.json title
4444+ {"status":"error","message":"Missing member title in JSON Feed object","location":{"file":"data/missing_title.json","line":1,"column":1,"byte_start":0,"byte_end":65},"context":"$"}
4545+ [1]
4646+4747+Test missing version field:
4848+ $ ./test_location_errors.exe data/missing_version.json title
4949+ {"status":"error","message":"Missing member version in JSON Feed object","location":{"file":"data/missing_version.json","line":1,"column":1,"byte_start":0,"byte_end":51},"context":"$"}
5050+ [1]
5151+5252+Test missing items field:
5353+ $ ./test_location_errors.exe data/missing_items.json title
5454+ {"status":"error","message":"Missing member items in JSON Feed object","location":{"file":"data/missing_items.json","line":1,"column":1,"byte_start":0,"byte_end":83},"context":"$"}
5555+ [1]
5656+5757+Test missing item id:
5858+ $ ./test_location_errors.exe data/missing_item_id.json first_item_id
5959+ {"status":"error","message":"Missing member id in Item object","location":{"file":"data/missing_item_id.json","line":5,"column":5,"byte_start":108,"byte_end":161},"context":"$.items[0]"}
6060+ [1]
6161+6262+Test missing item content:
6363+ $ ./test_location_errors.exe data/missing_item_content.json first_item_id
6464+ {"status":"error","message":"Item must have at least one of content_html or content_text","location":{"file":"-","line":-1,"column":1,"byte_start":-1,"byte_end":-1},"context":"$.items[0]"}
6565+ [1]
6666+6767+6868+Type Errors
6969+-----------
7070+7171+Test wrong type for version (number instead of string):
7272+ $ ./test_location_errors.exe data/wrong_type_version.json title
7373+ {"status":"error","message":"Expected string but found number","location":{"file":"data/wrong_type_version.json","line":2,"column":14,"byte_start":15,"byte_end":15},"context":"$.version"}
7474+ [1]
7575+7676+Test wrong type for items (object instead of array):
7777+ $ ./test_location_errors.exe data/wrong_type_items.json item_count
7878+ {"status":"error","message":"Expected array<Item object> but found object","location":{"file":"data/wrong_type_items.json","line":4,"column":12,"byte_start":102,"byte_end":102},"context":"$.items"}
7979+ [1]
8080+8181+Test wrong type for title (boolean instead of string):
8282+ $ ./test_location_errors.exe data/wrong_type_title.json title
8383+ {"status":"error","message":"Expected string but found bool","location":{"file":"data/wrong_type_title.json","line":3,"column":12,"byte_start":62,"byte_end":62},"context":"$.title"}
8484+ [1]
8585+8686+Test wrong type for expired (string instead of boolean):
8787+ $ ./test_location_errors.exe data/wrong_type_expired.json title
8888+ {"status":"error","message":"Expected bool but found string","location":{"file":"data/wrong_type_expired.json","line":4,"column":14,"byte_start":111,"byte_end":111},"context":"$.expired"}
8989+ [1]
9090+9191+9292+Nested Errors
9393+-------------
9494+9595+Test invalid date format in item:
9696+ $ ./test_location_errors.exe data/invalid_date_format.json first_item_id
9797+ {"status":"error","message":"RFC 3339 timestamp: invalid RFC 3339 timestamp: \"not-a-valid-date\"","location":{"file":"-","line":-1,"column":1,"byte_start":-1,"byte_end":-1},"context":"$.items[0].date_published"}
9898+ [1]
9999+100100+Test invalid author type (string instead of object):
101101+ $ ./test_location_errors.exe data/invalid_author_type.json title
102102+ {"status":"error","message":"Expected Author object but found string","location":{"file":"data/invalid_author_type.json","line":5,"column":5,"byte_start":109,"byte_end":109},"context":"$.authors[0]"}
103103+ [1]
104104+105105+Test invalid attachment field type (deeply nested):
106106+ $ ./test_location_errors.exe data/invalid_nested_attachment.json first_item_id
107107+ {"status":"error","message":"Expected string but found number","location":{"file":"data/invalid_nested_attachment.json","line":11,"column":24,"byte_start":296,"byte_end":296},"context":"$.items[0].attachments[0].mime_type"}
108108+ [1]
109109+110110+Test missing required field in hub:
111111+ $ ./test_location_errors.exe data/invalid_hub_type.json title
112112+ {"status":"error","message":"Missing member url in Hub object","location":{"file":"data/invalid_hub_type.json","line":5,"column":5,"byte_start":103,"byte_end":132},"context":"$.hubs[0]"}
113113+ [1]
114114+115115+116116+JSON Syntax Errors
117117+------------------
118118+119119+Test trailing comma:
120120+ $ ./test_location_errors.exe data/extra_comma.json title
121121+ {"status":"error","message":"Expected object member but found }","location":{"file":"data/extra_comma.json","line":5,"column":1,"byte_start":105,"byte_end":105},"context":"$"}
122122+ [1]
123123+124124+Test malformed JSON (missing comma):
125125+ $ ./test_location_errors.exe data/malformed_json.json title
126126+ {"status":"error","message":"Expected , or } after object member but found: \"","location":{"file":"data/malformed_json.json","line":3,"column":3,"byte_start":52,"byte_end":52},"context":"$"}
127127+ [1]