OCaml library for JSONfeed parsing and creation

ocamlformat

+10 -15
example/feed_echo.ml
···
(** Example: JSON Feed Echo
-
Reads a JSON Feed from stdin, parses it, and outputs it to stdout.
-
Useful for testing round-trip parsing and identifying any changes
-
during serialization/deserialization.
+
Reads a JSON Feed from stdin, parses it, and outputs it to stdout. Useful
+
for testing round-trip parsing and identifying any changes during
+
serialization/deserialization.
-
Usage:
-
feed_echo < feed.json
-
cat feed.json | feed_echo > output.json
-
diff <(cat feed.json | feed_echo) feed.json
+
Usage: feed_echo < feed.json cat feed.json | feed_echo > output.json diff
+
<(cat feed.json | feed_echo) feed.json
-
Exit codes:
-
0 - Success
-
1 - Parsing or encoding failed *)
+
Exit codes: 0 - Success 1 - Parsing or encoding failed *)
let echo_feed () =
(* Create a bytesrw reader from stdin *)
···
| Error err ->
Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err);
exit 1
-
-
| Ok feed ->
+
| Ok feed -> (
(* Encode the feed back to stdout *)
match Jsonfeed.to_string ~minify:false feed with
| Error err ->
-
Format.eprintf "Encoding failed:\n %s\n%!" (Jsont.Error.to_string err);
+
Format.eprintf "Encoding failed:\n %s\n%!"
+
(Jsont.Error.to_string err);
exit 1
-
| Ok json ->
print_string json;
print_newline ();
-
exit 0
+
exit 0)
let () = echo_feed ()
+95 -105
example/feed_example.ml
···
match Jsonfeed.to_string feed with
| Ok s ->
Out_channel.with_open_gen
-
[Open_wronly; Open_creat; Open_trunc; Open_text]
-
0o644
-
filename
+
[ Open_wronly; Open_creat; Open_trunc; Open_text ] 0o644 filename
(fun oc -> Out_channel.output_string oc s)
| Error e ->
Printf.eprintf "Error encoding feed: %s\n" (Jsont.Error.to_string e);
···
let create_blog_feed () =
(* Create some authors *)
-
let jane = Author.create
-
~name:"Jane Doe"
-
~url:"https://example.com/authors/jane"
-
~avatar:"https://example.com/avatars/jane.png"
-
() in
+
let jane =
+
Author.create ~name:"Jane Doe" ~url:"https://example.com/authors/jane"
+
~avatar:"https://example.com/avatars/jane.png" ()
+
in
-
let john = Author.create
-
~name:"John Smith"
-
~url:"https://example.com/authors/john"
-
() in
+
let john =
+
Author.create ~name:"John Smith" ~url:"https://example.com/authors/john" ()
+
in
(* Create items with different content types *)
-
let item1 = Item.create
-
~id:"https://example.com/posts/1"
-
~url:"https://example.com/posts/1"
-
~title:"Introduction to OCaml"
-
~content:(`Both (
-
"<p>OCaml is a powerful functional programming language.</p>",
-
"OCaml is a powerful functional programming language."
-
))
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
-
~date_modified:(Jsonfeed.Rfc3339.parse "2024-11-01T15:30:00Z" |> Option.get)
-
~authors:[jane]
-
~tags:["ocaml"; "programming"; "functional"]
-
~summary:"A beginner's guide to OCaml programming"
-
() in
+
let item1 =
+
Item.create ~id:"https://example.com/posts/1"
+
~url:"https://example.com/posts/1" ~title:"Introduction to OCaml"
+
~content:
+
(`Both
+
( "<p>OCaml is a powerful functional programming language.</p>",
+
"OCaml is a powerful functional programming language." ))
+
~date_published:
+
(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
+
~date_modified:
+
(Jsonfeed.Rfc3339.parse "2024-11-01T15:30:00Z" |> Option.get)
+
~authors:[ jane ]
+
~tags:[ "ocaml"; "programming"; "functional" ]
+
~summary:"A beginner's guide to OCaml programming" ()
+
in
-
let item2 = Item.create
-
~id:"https://example.com/posts/2"
-
~url:"https://example.com/posts/2"
-
~title:"JSON Feed for Syndication"
-
~content:(`Html "<p>JSON Feed is a modern alternative to RSS and Atom.</p>")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-02T09:00:00Z" |> Option.get)
-
~authors:[jane; john]
-
~tags:["json"; "syndication"; "web"]
-
~image:"https://example.com/images/jsonfeed.png"
-
() in
+
let item2 =
+
Item.create ~id:"https://example.com/posts/2"
+
~url:"https://example.com/posts/2" ~title:"JSON Feed for Syndication"
+
~content:
+
(`Html "<p>JSON Feed is a modern alternative to RSS and Atom.</p>")
+
~date_published:
+
(Jsonfeed.Rfc3339.parse "2024-11-02T09:00:00Z" |> Option.get)
+
~authors:[ jane; john ]
+
~tags:[ "json"; "syndication"; "web" ]
+
~image:"https://example.com/images/jsonfeed.png" ()
+
in
(* Microblog-style item (text only, no title) *)
-
let item3 = Item.create
-
~id:"https://example.com/micro/42"
-
~content:(`Text "Just shipped a new feature! 🚀")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-03T08:15:00Z" |> Option.get)
-
~tags:["microblog"]
-
() in
+
let item3 =
+
Item.create ~id:"https://example.com/micro/42"
+
~content:(`Text "Just shipped a new feature! 🚀")
+
~date_published:
+
(Jsonfeed.Rfc3339.parse "2024-11-03T08:15:00Z" |> Option.get)
+
~tags:[ "microblog" ] ()
+
in
(* Create the complete feed *)
-
let feed = Jsonfeed.create
-
~title:"Example Blog"
-
~home_page_url:"https://example.com"
-
~feed_url:"https://example.com/feed.json"
-
~description:"A blog about programming, web development, and technology"
-
~icon:"https://example.com/icon-512.png"
-
~favicon:"https://example.com/favicon-64.png"
-
~authors:[jane; john]
-
~language:"en-US"
-
~items:[item1; item2; item3]
-
() in
+
let feed =
+
Jsonfeed.create ~title:"Example Blog" ~home_page_url:"https://example.com"
+
~feed_url:"https://example.com/feed.json"
+
~description:"A blog about programming, web development, and technology"
+
~icon:"https://example.com/icon-512.png"
+
~favicon:"https://example.com/favicon-64.png" ~authors:[ jane; john ]
+
~language:"en-US" ~items:[ item1; item2; item3 ] ()
+
in
feed
let create_podcast_feed () =
(* Create podcast author *)
-
let host = Author.create
-
~name:"Podcast Host"
-
~url:"https://podcast.example.com/host"
-
~avatar:"https://podcast.example.com/host-avatar.jpg"
-
() in
+
let host =
+
Author.create ~name:"Podcast Host" ~url:"https://podcast.example.com/host"
+
~avatar:"https://podcast.example.com/host-avatar.jpg" ()
+
in
(* Create episode with audio attachment *)
-
let attachment = Attachment.create
-
~url:"https://podcast.example.com/episodes/ep1.mp3"
-
~mime_type:"audio/mpeg"
-
~title:"Episode 1: Introduction"
-
~size_in_bytes:15_728_640L
-
~duration_in_seconds:1800
-
() in
+
let attachment =
+
Attachment.create ~url:"https://podcast.example.com/episodes/ep1.mp3"
+
~mime_type:"audio/mpeg" ~title:"Episode 1: Introduction"
+
~size_in_bytes:15_728_640L ~duration_in_seconds:1800 ()
+
in
-
let episode = Item.create
-
~id:"https://podcast.example.com/episodes/1"
-
~url:"https://podcast.example.com/episodes/1"
-
~title:"Episode 1: Introduction"
-
~content:(`Html "<p>Welcome to our first episode!</p>")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T12:00:00Z" |> Option.get)
-
~attachments:[attachment]
-
~authors:[host]
-
~image:"https://podcast.example.com/episodes/ep1-cover.jpg"
-
() in
+
let episode =
+
Item.create ~id:"https://podcast.example.com/episodes/1"
+
~url:"https://podcast.example.com/episodes/1"
+
~title:"Episode 1: Introduction"
+
~content:(`Html "<p>Welcome to our first episode!</p>")
+
~date_published:
+
(Jsonfeed.Rfc3339.parse "2024-11-01T12:00:00Z" |> Option.get)
+
~attachments:[ attachment ] ~authors:[ host ]
+
~image:"https://podcast.example.com/episodes/ep1-cover.jpg" ()
+
in
(* Create podcast feed with hub for real-time updates *)
-
let hub = Hub.create
-
~type_:"WebSub"
-
~url:"https://pubsubhubbub.appspot.com/"
-
() in
+
let hub =
+
Hub.create ~type_:"WebSub" ~url:"https://pubsubhubbub.appspot.com/" ()
+
in
-
let feed = Jsonfeed.create
-
~title:"Example Podcast"
-
~home_page_url:"https://podcast.example.com"
-
~feed_url:"https://podcast.example.com/feed.json"
-
~description:"A podcast about interesting topics"
-
~icon:"https://podcast.example.com/icon.png"
-
~authors:[host]
-
~language:"en-US"
-
~hubs:[hub]
-
~items:[episode]
-
() in
+
let feed =
+
Jsonfeed.create ~title:"Example Podcast"
+
~home_page_url:"https://podcast.example.com"
+
~feed_url:"https://podcast.example.com/feed.json"
+
~description:"A podcast about interesting topics"
+
~icon:"https://podcast.example.com/icon.png" ~authors:[ host ]
+
~language:"en-US" ~hubs:[ hub ] ~items:[ episode ] ()
+
in
feed
···
(* Serialize to string *)
(match Jsonfeed.to_string blog_feed with
-
| Ok json_string ->
-
Format.printf "JSON (first 200 chars): %s...\n\n"
-
(String.sub json_string 0 (min 200 (String.length json_string)))
-
| Error e ->
-
Printf.eprintf "Error serializing to string: %s\n" (Jsont.Error.to_string e);
-
exit 1);
+
| Ok json_string ->
+
Format.printf "JSON (first 200 chars): %s...\n\n"
+
(String.sub json_string 0 (min 200 (String.length json_string)))
+
| Error e ->
+
Printf.eprintf "Error serializing to string: %s\n"
+
(Jsont.Error.to_string e);
+
exit 1);
(* Serialize to file *)
to_file "blog-feed.json" blog_feed;
···
(* Validate feeds *)
(match Jsonfeed.validate blog_feed with
-
| Ok () -> Format.printf "✓ Blog feed is valid\n"
-
| Error errors ->
-
Format.printf "✗ Blog feed validation errors:\n";
-
List.iter (Format.printf " - %s\n") errors);
+
| Ok () -> Format.printf "✓ Blog feed is valid\n"
+
| Error errors ->
+
Format.printf "✗ Blog feed validation errors:\n";
+
List.iter (Format.printf " - %s\n") errors);
-
(match Jsonfeed.validate podcast_feed with
-
| Ok () -> Format.printf "✓ Podcast feed is valid\n"
-
| Error errors ->
-
Format.printf "✗ Podcast feed validation errors:\n";
-
List.iter (Format.printf " - %s\n") errors)
+
match Jsonfeed.validate podcast_feed with
+
| Ok () -> Format.printf "✓ Podcast feed is valid\n"
+
| Error errors ->
+
Format.printf "✗ Podcast feed validation errors:\n";
+
List.iter (Format.printf " - %s\n") errors
let () = main ()
+88 -88
example/feed_parser.ml
···
Format.printf " Version: %s\n" (Jsonfeed.version feed);
(match Jsonfeed.home_page_url feed with
-
| Some url -> Format.printf " Home Page: %s\n" url
-
| None -> ());
+
| Some url -> Format.printf " Home Page: %s\n" url
+
| None -> ());
(match Jsonfeed.feed_url feed with
-
| Some url -> Format.printf " Feed URL: %s\n" url
-
| None -> ());
+
| Some url -> Format.printf " Feed URL: %s\n" url
+
| None -> ());
(match Jsonfeed.description feed with
-
| Some desc -> Format.printf " Description: %s\n" desc
-
| None -> ());
+
| Some desc -> Format.printf " Description: %s\n" desc
+
| None -> ());
(match Jsonfeed.language feed with
-
| Some lang -> Format.printf " Language: %s\n" lang
-
| None -> ());
+
| Some lang -> Format.printf " Language: %s\n" lang
+
| None -> ());
(match Jsonfeed.authors feed with
-
| Some authors ->
-
Format.printf " Authors:\n";
-
List.iter (fun author ->
-
match Author.name author with
-
| Some name -> Format.printf " - %s" name;
-
(match Author.url author with
+
| Some authors ->
+
Format.printf " Authors:\n";
+
List.iter
+
(fun author ->
+
match Author.name author with
+
| Some name ->
+
Format.printf " - %s" name;
+
(match Author.url author with
| Some url -> Format.printf " (%s)" url
| None -> ());
-
Format.printf "\n"
-
| None -> ()
-
) authors
-
| None -> ());
+
Format.printf "\n"
+
| None -> ())
+
authors
+
| None -> ());
Format.printf " Items: %d\n\n" (List.length (Jsonfeed.items feed))
···
Format.printf "Item: %s\n" (Item.id item);
(match Item.title item with
-
| Some title -> Format.printf " Title: %s\n" title
-
| None -> Format.printf " (No title - microblog entry)\n");
+
| Some title -> Format.printf " Title: %s\n" title
+
| None -> Format.printf " (No title - microblog entry)\n");
(match Item.url item with
-
| Some url -> Format.printf " URL: %s\n" url
-
| None -> ());
+
| Some url -> Format.printf " URL: %s\n" url
+
| None -> ());
(* Print content info *)
(match Item.content item with
-
| `Html html ->
-
Format.printf " Content: HTML only (%d chars)\n"
-
(String.length html)
-
| `Text text ->
-
Format.printf " Content: Text only (%d chars)\n"
-
(String.length text)
-
| `Both (html, text) ->
-
Format.printf " Content: Both HTML (%d chars) and Text (%d chars)\n"
-
(String.length html) (String.length text));
+
| `Html html ->
+
Format.printf " Content: HTML only (%d chars)\n" (String.length html)
+
| `Text text ->
+
Format.printf " Content: Text only (%d chars)\n" (String.length text)
+
| `Both (html, text) ->
+
Format.printf " Content: Both HTML (%d chars) and Text (%d chars)\n"
+
(String.length html) (String.length text));
(* Print dates *)
(match Item.date_published item with
-
| Some date ->
-
Format.printf " Published: %s\n"
-
(Jsonfeed.Rfc3339.format date)
-
| None -> ());
+
| Some date ->
+
Format.printf " Published: %s\n" (Jsonfeed.Rfc3339.format date)
+
| None -> ());
(match Item.date_modified item with
-
| Some date ->
-
Format.printf " Modified: %s\n"
-
(Jsonfeed.Rfc3339.format date)
-
| None -> ());
+
| Some date -> Format.printf " Modified: %s\n" (Jsonfeed.Rfc3339.format date)
+
| None -> ());
(* Print tags *)
(match Item.tags item with
-
| Some tags when tags <> [] ->
-
Format.printf " Tags: %s\n" (String.concat ", " tags)
-
| _ -> ());
+
| Some tags when tags <> [] ->
+
Format.printf " Tags: %s\n" (String.concat ", " tags)
+
| _ -> ());
(* Print attachments *)
(match Item.attachments item with
-
| Some attachments when attachments <> [] ->
-
Format.printf " Attachments:\n";
-
List.iter (fun att ->
-
Format.printf " - %s (%s)\n"
-
(Attachment.url att)
-
(Attachment.mime_type att);
-
(match Attachment.size_in_bytes att with
+
| Some attachments when attachments <> [] ->
+
Format.printf " Attachments:\n";
+
List.iter
+
(fun att ->
+
Format.printf " - %s (%s)\n" (Attachment.url att)
+
(Attachment.mime_type att);
+
(match Attachment.size_in_bytes att with
| Some size ->
let mb = Int64.to_float size /. (1024. *. 1024.) in
Format.printf " Size: %.2f MB\n" mb
| None -> ());
-
(match Attachment.duration_in_seconds att with
+
match Attachment.duration_in_seconds att with
| Some duration ->
let mins = duration / 60 in
let secs = duration mod 60 in
Format.printf " Duration: %dm%ds\n" mins secs
| None -> ())
-
) attachments
-
| _ -> ());
+
attachments
+
| _ -> ());
Format.printf "\n"
···
let text_only = ref 0 in
let both = ref 0 in
-
List.iter (fun item ->
-
match Item.content item with
-
| `Html _ -> incr html_only
-
| `Text _ -> incr text_only
-
| `Both _ -> incr both
-
) items;
+
List.iter
+
(fun item ->
+
match Item.content item with
+
| `Html _ -> incr html_only
+
| `Text _ -> incr text_only
+
| `Both _ -> incr both)
+
items;
Format.printf "Content Types:\n";
Format.printf " HTML only: %d\n" !html_only;
···
Format.printf " Both: %d\n\n" !both;
(* Find items with attachments *)
-
let with_attachments = List.filter (fun item ->
-
match Item.attachments item with
-
| Some att when att <> [] -> true
-
| _ -> false
-
) items in
+
let with_attachments =
+
List.filter
+
(fun item ->
+
match Item.attachments item with
+
| Some att when att <> [] -> true
+
| _ -> false)
+
items
+
in
Format.printf "Items with attachments: %d\n\n" (List.length with_attachments);
(* Collect all unique tags *)
-
let all_tags = List.fold_left (fun acc item ->
-
match Item.tags item with
-
| Some tags -> acc @ tags
-
| None -> acc
-
) [] items in
+
let all_tags =
+
List.fold_left
+
(fun acc item ->
+
match Item.tags item with Some tags -> acc @ tags | None -> acc)
+
[] items
+
in
let unique_tags = List.sort_uniq String.compare all_tags in
-
if unique_tags <> [] then (
+
if unique_tags <> [] then
Format.printf "All tags used: %s\n\n" (String.concat ", " unique_tags)
-
)
let main () =
(* Parse from example_feed.json file *)
Format.printf "=== Parsing JSON Feed from example_feed.json ===\n\n";
-
(try
+
try
match of_file "example/example_feed.json" with
-
| Ok feed ->
+
| Ok feed -> (
print_feed_info feed;
Format.printf "=== Items ===\n\n";
···
(* Demonstrate round-trip parsing *)
Format.printf "\n=== Round-trip Test ===\n\n";
-
(match Jsonfeed.to_string feed with
-
| Error e ->
-
Printf.eprintf "Error serializing feed: %s\n" (Jsont.Error.to_string e);
-
exit 1
-
| Ok json ->
-
match Jsonfeed.of_string json with
-
| Ok feed2 ->
-
if Jsonfeed.equal feed feed2 then
-
Format.printf "✓ Round-trip successful: feeds are equal\n"
-
else
-
Format.printf "✗ Round-trip failed: feeds differ\n"
-
| Error err ->
-
Format.eprintf "✗ Round-trip failed: %s\n" (Jsont.Error.to_string err))
+
match Jsonfeed.to_string feed with
+
| Error e ->
+
Printf.eprintf "Error serializing feed: %s\n"
+
(Jsont.Error.to_string e);
+
exit 1
+
| Ok json -> (
+
match Jsonfeed.of_string json with
+
| Ok feed2 ->
+
if Jsonfeed.equal feed feed2 then
+
Format.printf "✓ Round-trip successful: feeds are equal\n"
+
else Format.printf "✗ Round-trip failed: feeds differ\n"
+
| Error err ->
+
Format.eprintf "✗ Round-trip failed: %s\n"
+
(Jsont.Error.to_string err)))
| Error err ->
Format.eprintf "Error parsing feed: %s\n" (Jsont.Error.to_string err)
-
with
-
| Sys_error msg ->
-
Format.eprintf "Error reading file: %s\n" msg)
+
with Sys_error msg -> Format.eprintf "Error reading file: %s\n" msg
let () = main ()
+7 -12
example/feed_validator.ml
···
Reads a JSON Feed from stdin and validates it.
-
Usage:
-
feed_validator < feed.json
-
cat feed.json | feed_validator
+
Usage: feed_validator < feed.json cat feed.json | feed_validator
-
Exit codes:
-
0 - Feed is valid
-
1 - Feed parsing failed
-
2 - Feed validation failed *)
+
Exit codes: 0 - Feed is valid 1 - Feed parsing failed 2 - Feed validation
+
failed *)
let validate_stdin () =
let stdin = Bytesrw.Bytes.Reader.of_in_channel In_channel.stdin in
···
| Error err ->
Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err);
exit 1
-
| Ok feed ->
+
| Ok feed -> (
match Jsonfeed.validate feed with
| Ok () ->
Format.printf "Feed is valid\n%!";
···
Format.printf " Title: %s\n" (Jsonfeed.title feed);
Format.printf " Version: %s\n" (Jsonfeed.version feed);
(match Jsonfeed.home_page_url feed with
-
| Some url -> Format.printf " Home page: %s\n" url
-
| None -> ());
+
| Some url -> Format.printf " Home page: %s\n" url
+
| None -> ());
Format.printf " Items: %d\n" (List.length (Jsonfeed.items feed));
exit 0
-
| Error errors ->
Format.eprintf "Validation failed:\n%!";
List.iter (fun err -> Format.eprintf " - %s\n%!" err) errors;
-
exit 2
+
exit 2)
let () = validate_stdin ()
+35 -26
lib/attachment.ml
···
unknown : Unknown.t;
}
-
let create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ?(unknown = Unknown.empty) () =
+
let create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds
+
?(unknown = Unknown.empty) () =
{ url; mime_type; title; size_in_bytes; duration_in_seconds; unknown }
let url t = t.url
···
let unknown t = t.unknown
let equal a b =
-
a.url = b.url &&
-
a.mime_type = b.mime_type &&
-
a.title = b.title &&
-
a.size_in_bytes = b.size_in_bytes &&
-
a.duration_in_seconds = b.duration_in_seconds
+
a.url = b.url && a.mime_type = b.mime_type && a.title = b.title
+
&& a.size_in_bytes = b.size_in_bytes
+
&& a.duration_in_seconds = b.duration_in_seconds
let pp ppf t =
(* Extract filename from URL *)
···
Format.fprintf ppf "%s (%s" filename t.mime_type;
(match t.size_in_bytes with
-
| Some size ->
-
let mb = Int64.to_float size /. (1024. *. 1024.) in
-
Format.fprintf ppf ", %.1f MB" mb
-
| None -> ());
+
| Some size ->
+
let mb = Int64.to_float size /. (1024. *. 1024.) in
+
Format.fprintf ppf ", %.1f MB" mb
+
| None -> ());
(match t.duration_in_seconds with
-
| Some duration ->
-
let mins = duration / 60 in
-
let secs = duration mod 60 in
-
Format.fprintf ppf ", %dm%ds" mins secs
-
| None -> ());
+
| Some duration ->
+
let mins = duration / 60 in
+
let secs = duration mod 60 in
+
Format.fprintf ppf ", %dm%ds" mins secs
+
| None -> ());
Format.fprintf ppf ")"
let jsont =
let kind = "Attachment" in
let doc = "An attachment object" in
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
((name, Jsont.Meta.none), value) :: acc
in
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
+
in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
let create_obj url mime_type title size_in_bytes duration_in_seconds unknown =
-
create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ~unknown ()
+
create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ~unknown
+
()
in
Jsont.Object.map ~kind ~doc create_obj
|> Jsont.Object.mem "url" Jsont.string ~enc:url
|> Jsont.Object.mem "mime_type" Jsont.string ~enc:mime_type
|> Jsont.Object.opt_mem "title" Jsont.string ~enc:title
|> Jsont.Object.opt_mem "size_in_bytes" Jsont.int64 ~enc:size_in_bytes
-
|> Jsont.Object.opt_mem "duration_in_seconds" Jsont.int ~enc:duration_in_seconds
+
|> Jsont.Object.opt_mem "duration_in_seconds" Jsont.int
+
~enc:duration_in_seconds
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
|> Jsont.Object.finish
+25 -30
lib/attachment.mli
···
(** Attachments for JSON Feed items.
-
An attachment represents an external resource related to a feed item,
-
such as audio files for podcasts, video files, or other downloadable content.
-
Attachments with identical titles indicate alternate formats of the same resource.
+
An attachment represents an external resource related to a feed item, such
+
as audio files for podcasts, video files, or other downloadable content.
+
Attachments with identical titles indicate alternate formats of the same
+
resource.
@see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
-
-
(** The type representing an attachment. *)
type t
-
+
(** The type representing an attachment. *)
(** {1 Unknown Fields} *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
(** [is_empty u] returns [true] if there are no unknown fields. *)
end
-
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for attachments.
-
Maps JSON objects with "url" (required), "mime_type" (required),
-
and optional "title", "size_in_bytes", "duration_in_seconds" fields. *)
-
+
Maps JSON objects with "url" (required), "mime_type" (required), and
+
optional "title", "size_in_bytes", "duration_in_seconds" fields. *)
(** {1 Construction} *)
···
?unknown:Unknown.t ->
unit ->
t
-
(** [create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ?unknown ()]
-
creates an attachment object.
+
(** [create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ?unknown
+
()] creates an attachment object.
@param url The location of the attachment (required)
-
@param mime_type The MIME type of the attachment, e.g. ["audio/mpeg"] (required)
-
@param title The name of the attachment; identical titles indicate alternate formats
-
of the same resource
+
@param mime_type
+
The MIME type of the attachment, e.g. ["audio/mpeg"] (required)
+
@param title
+
The name of the attachment; identical titles indicate alternate formats of
+
the same resource
@param size_in_bytes The size of the attachment file in bytes
-
@param duration_in_seconds The duration of the attachment in seconds (for audio/video)
+
@param duration_in_seconds
+
The duration of the attachment in seconds (for audio/video)
@param unknown Unknown/custom fields for extensions (default: empty)
{b Examples:}
{[
(* Simple attachment *)
-
let att = Attachment.create
-
~url:"https://example.com/episode.mp3"
-
~mime_type:"audio/mpeg" ()
+
let att =
+
Attachment.create ~url:"https://example.com/episode.mp3"
+
~mime_type:"audio/mpeg" ()
(* Podcast episode with metadata *)
-
let att = Attachment.create
-
~url:"https://example.com/episode.mp3"
-
~mime_type:"audio/mpeg"
-
~title:"Episode 42"
-
~size_in_bytes:15_728_640L
-
~duration_in_seconds:1800 ()
+
let att =
+
Attachment.create ~url:"https://example.com/episode.mp3"
+
~mime_type:"audio/mpeg" ~title:"Episode 42" ~size_in_bytes:15_728_640L
+
~duration_in_seconds:1800 ()
]} *)
-
(** {1 Accessors} *)
···
val unknown : t -> Unknown.t
(** [unknown t] returns unrecognized fields from the JSON. *)
-
(** {1 Comparison} *)
val equal : t -> t -> bool
(** [equal a b] tests equality between two attachments. *)
-
(** {1 Pretty Printing} *)
+26 -20
lib/author.ml
···
let create ?name ?url ?avatar ?(unknown = Unknown.empty) () =
if name = None && url = None && avatar = None then
-
invalid_arg "Author.create: at least one field (name, url, or avatar) must be provided";
+
invalid_arg
+
"Author.create: at least one field (name, url, or avatar) must be \
+
provided";
{ name; url; avatar; unknown }
let name t = t.name
let url t = t.url
let avatar t = t.avatar
let unknown t = t.unknown
-
-
let is_valid t =
-
t.name <> None || t.url <> None || t.avatar <> None
-
-
let equal a b =
-
a.name = b.name &&
-
a.url = b.url &&
-
a.avatar = b.avatar
+
let is_valid t = t.name <> None || t.url <> None || t.avatar <> None
+
let equal a b = a.name = b.name && a.url = b.url && a.avatar = b.avatar
let pp ppf t =
-
match t.name, t.url with
+
match (t.name, t.url) with
| Some name, Some url -> Format.fprintf ppf "%s <%s>" name url
| Some name, None -> Format.fprintf ppf "%s" name
| None, Some url -> Format.fprintf ppf "<%s>" url
-
| None, None ->
+
| None, None -> (
match t.avatar with
| Some avatar -> Format.fprintf ppf "(avatar: %s)" avatar
-
| None -> Format.fprintf ppf "(empty author)"
+
| None -> Format.fprintf ppf "(empty author)")
let jsont =
let kind = "Author" in
let doc = "An author object with at least one field set" in
(* Custom mems map for Unknown.t that strips metadata from names *)
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
···
let dec_finish _meta mems =
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
(* Constructor that matches the jsont object map pattern *)
-
let create_obj name url avatar unknown = create ?name ?url ?avatar ~unknown () in
+
let create_obj name url avatar unknown =
+
create ?name ?url ?avatar ~unknown ()
+
in
Jsont.Object.map ~kind ~doc create_obj
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
|> Jsont.Object.opt_mem "url" Jsont.string ~enc:url
+21 -24
lib/author.mli
···
@see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
-
-
(** The type representing an author. *)
type t
-
+
(** The type representing an author. *)
(** {1 Unknown Fields} *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
val is_empty : t -> bool
(** [is_empty u] returns [true] if there are no unknown fields. *)
end
-
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for authors.
-
Maps JSON objects with optional "name", "url", and "avatar" fields.
-
At least one field must be present during decoding. *)
-
+
Maps JSON objects with optional "name", "url", and "avatar" fields. At least
+
one field must be present during decoding. *)
(** {1 Construction} *)
val create :
-
?name:string -> ?url:string -> ?avatar:string ->
-
?unknown:Unknown.t -> unit -> t
+
?name:string ->
+
?url:string ->
+
?avatar:string ->
+
?unknown:Unknown.t ->
+
unit ->
+
t
(** [create ?name ?url ?avatar ?unknown ()] creates an author.
-
At least one of the optional parameters must be provided, otherwise
-
the function will raise [Invalid_argument].
+
At least one of the optional parameters must be provided, otherwise the
+
function will raise [Invalid_argument].
@param name The author's name
@param url URL of the author's website or profile
-
@param avatar URL of the author's avatar image (should be square, 512x512 or larger)
+
@param avatar
+
URL of the author's avatar image (should be square, 512x512 or larger)
@param unknown Unknown/custom fields for extensions (default: empty)
{b Examples:}
{[
let author = Author.create ~name:"Jane Doe" ()
let author = Author.create ~name:"Jane Doe" ~url:"https://janedoe.com" ()
-
let author = Author.create
-
~name:"Jane Doe"
-
~url:"https://janedoe.com"
-
~avatar:"https://janedoe.com/avatar.png" ()
-
]} *)
+
let author =
+
Author.create ~name:"Jane Doe" ~url:"https://janedoe.com"
+
~avatar:"https://janedoe.com/avatar.png" ()
+
]} *)
(** {1 Accessors} *)
···
val unknown : t -> Unknown.t
(** [unknown t] returns unrecognized fields from the JSON. *)
-
(** {1 Predicates} *)
val is_valid : t -> bool
(** [is_valid t] checks if the author has at least one field set.
-
This should always return [true] for authors created via {!create},
-
but may be useful when parsing from external sources. *)
-
+
This should always return [true] for authors created via {!create}, but may
+
be useful when parsing from external sources. *)
(** {1 Comparison} *)
val equal : t -> t -> bool
(** [equal a b] tests equality between two authors. *)
-
(** {1 Pretty Printing} *)
+4 -9
lib/cito.ml
···
-
type t = [
-
| `Cites
+
type t =
+
[ `Cites
| `CitesAsAuthority
| `CitesAsDataSource
| `CitesAsEvidence
···
| `SharesPublicationVenueWith
| `SharesFundingAgencyWith
| `SharesAuthorInstitutionWith
-
| `Other of string
-
]
+
| `Other of string ]
let to_string = function
| `Cites -> "cites"
···
| "sharesauthorinstitutionwith" -> `SharesAuthorInstitutionWith
| _ -> `Other s
-
let equal a b =
-
match a, b with
-
| `Other sa, `Other sb -> sa = sb
-
| _ -> a = b
-
+
let equal a b = match (a, b) with `Other sa, `Other sb -> sa = sb | _ -> a = b
let pp ppf t = Format.fprintf ppf "%s" (to_string t)
let jsont =
+77 -79
lib/cito.mli
···
(** Citation Typing Ontology (CiTO) intent annotations.
-
CiTO provides a structured vocabulary for describing the nature of citations.
-
This module implements support for CiTO annotations as used in the references extension.
+
CiTO provides a structured vocabulary for describing the nature of
+
citations. This module implements support for CiTO annotations as used in
+
the references extension.
@see <https://purl.archive.org/spar/cito> Citation Typing Ontology
-
@see <https://sparontologies.github.io/cito/current/cito.html> CiTO Specification *)
-
-
-
(** CiTO citation intent annotation.
-
-
Represents the intent or nature of a citation using the Citation Typing Ontology.
-
Each variant corresponds to a specific CiTO property. The [`Other] variant allows
-
for custom or future CiTO terms not yet included in this library.
+
@see <https://sparontologies.github.io/cito/current/cito.html>
+
CiTO Specification *)
-
{b Categories:}
-
- Factual: Citing for data, methods, evidence, or information
-
- Critical: Agreement, disagreement, correction, or qualification
-
- Rhetorical: Style-based citations (parody, ridicule, etc.)
-
- Relational: Document relationships and compilations
-
- Support: Providing or obtaining backing and context
-
- Exploratory: Speculation and recommendations
-
- Quotation: Direct quotes and excerpts
-
- Dialogue: Replies and responses
-
- Sharing: Common attributes between works *)
-
type t = [
-
| `Cites (** The base citation property *)
-
-
(* Factual citation intents *)
-
| `CitesAsAuthority (** Cites as authoritative source *)
+
type t =
+
[ `Cites (** The base citation property *)
+
| (* Factual citation intents *)
+
`CitesAsAuthority
+
(** Cites as authoritative source *)
| `CitesAsDataSource (** Cites as origin of data *)
| `CitesAsEvidence (** Cites for factual evidence *)
| `CitesForInformation (** Cites as information source *)
| `UsesDataFrom (** Uses data from cited work *)
| `UsesMethodIn (** Uses methodology from cited work *)
| `UsesConclusionsFrom (** Applies conclusions from cited work *)
-
-
(* Agreement/disagreement *)
-
| `AgreesWith (** Concurs with cited statements *)
+
| (* Agreement/disagreement *)
+
`AgreesWith
+
(** Concurs with cited statements *)
| `DisagreesWith (** Rejects cited statements *)
| `Confirms (** Validates facts in cited work *)
| `Refutes (** Disproves cited statements *)
| `Disputes (** Contests without definitive refutation *)
-
-
(* Critical engagement *)
-
| `Critiques (** Analyzes and finds fault *)
+
| (* Critical engagement *)
+
`Critiques
+
(** Analyzes and finds fault *)
| `Qualifies (** Places conditions on statements *)
| `Corrects (** Fixes errors in cited work *)
| `Updates (** Advances understanding beyond cited work *)
| `Extends (** Builds upon cited facts *)
-
-
(* Rhetorical/stylistic *)
-
| `Parodies (** Imitates for comic effect *)
+
| (* Rhetorical/stylistic *)
+
`Parodies
+
(** Imitates for comic effect *)
| `Plagiarizes (** Uses without acknowledgment *)
| `Derides (** Expresses contempt *)
| `Ridicules (** Mocks cited work *)
-
-
(* Document relationships *)
-
| `Describes (** Characterizes cited entity *)
+
| (* Document relationships *)
+
`Describes
+
(** Characterizes cited entity *)
| `Documents (** Records information about source *)
| `CitesAsSourceDocument (** Cites as foundational source *)
| `CitesAsMetadataDocument (** Cites containing metadata *)
| `Compiles (** Uses to create new work *)
| `Reviews (** Examines cited statements *)
| `Retracts (** Formally withdraws *)
-
-
(* Support/context *)
-
| `Supports (** Provides intellectual backing *)
+
| (* Support/context *)
+
`Supports
+
(** Provides intellectual backing *)
| `GivesSupportTo (** Provides support to citing entity *)
| `ObtainsSupportFrom (** Obtains backing from cited work *)
| `GivesBackgroundTo (** Provides context *)
| `ObtainsBackgroundFrom (** Obtains context from cited work *)
-
-
(* Exploratory *)
-
| `SpeculatesOn (** Theorizes without firm evidence *)
+
| (* Exploratory *)
+
`SpeculatesOn
+
(** Theorizes without firm evidence *)
| `CitesAsPotentialSolution (** Offers possible resolution *)
| `CitesAsRecommendedReading (** Suggests as further reading *)
| `CitesAsRelated (** Identifies as thematically connected *)
-
-
(* Quotation/excerpting *)
-
| `IncludesQuotationFrom (** Incorporates direct quotes *)
+
| (* Quotation/excerpting *)
+
`IncludesQuotationFrom
+
(** Incorporates direct quotes *)
| `IncludesExcerptFrom (** Uses non-quoted passages *)
-
-
(* Dialogue *)
-
| `RepliesTo (** Responds to cited statements *)
+
| (* Dialogue *)
+
`RepliesTo
+
(** Responds to cited statements *)
| `HasReplyFrom (** Evokes response *)
-
-
(* Linking *)
-
| `LinksTo (** Provides URL hyperlink *)
-
-
(* Shared attribution *)
-
| `SharesAuthorWith (** Common authorship *)
+
| (* Linking *)
+
`LinksTo
+
(** Provides URL hyperlink *)
+
| (* Shared attribution *)
+
`SharesAuthorWith
+
(** Common authorship *)
| `SharesJournalWith (** Published in same journal *)
| `SharesPublicationVenueWith (** Published in same venue *)
| `SharesFundingAgencyWith (** Funded by same agency *)
| `SharesAuthorInstitutionWith (** Authors share affiliation *)
+
| (* Extensibility *)
+
`Other of string
+
(** Custom or future CiTO term *) ]
+
(** CiTO citation intent annotation.
-
(* Extensibility *)
-
| `Other of string (** Custom or future CiTO term *)
-
]
+
Represents the intent or nature of a citation using the Citation Typing
+
Ontology. Each variant corresponds to a specific CiTO property. The [`Other]
+
variant allows for custom or future CiTO terms not yet included in this
+
library.
+
{b Categories:}
+
- Factual: Citing for data, methods, evidence, or information
+
- Critical: Agreement, disagreement, correction, or qualification
+
- Rhetorical: Style-based citations (parody, ridicule, etc.)
+
- Relational: Document relationships and compilations
+
- Support: Providing or obtaining backing and context
+
- Exploratory: Speculation and recommendations
+
- Quotation: Direct quotes and excerpts
+
- Dialogue: Replies and responses
+
- Sharing: Common attributes between works *)
(** {1 Conversion} *)
+
val of_string : string -> t
(** [of_string s] converts a CiTO term string to its variant representation.
Recognized CiTO terms are converted to their corresponding variants.
Unrecognized terms are wrapped in [`Other].
-
The comparison is case-insensitive for standard CiTO terms but preserves
-
the original case in [`Other] variants.
+
The comparison is case-insensitive for standard CiTO terms but preserves the
+
original case in [`Other] variants.
{b Examples:}
{[
-
of_string "cites" (* returns `Cites *)
-
of_string "usesMethodIn" (* returns `UsesMethodIn *)
-
of_string "citesAsRecommendedReading" (* returns `CitesAsRecommendedReading *)
-
of_string "customTerm" (* returns `Other "customTerm" *)
+
of_string "cites" (* returns `Cites *) of_string "usesMethodIn"
+
(* returns `UsesMethodIn *) of_string
+
"citesAsRecommendedReading" (* returns `CitesAsRecommendedReading *)
+
of_string "customTerm" (* returns `Other "customTerm" *)
]} *)
-
val of_string : string -> t
-
(** [to_string t] converts a CiTO variant to its canonical string representation.
+
val to_string : t -> string
+
(** [to_string t] converts a CiTO variant to its canonical string
+
representation.
Standard CiTO terms use their official CiTO local names (camelCase).
[`Other] variants return the wrapped string unchanged.
{b Examples:}
{[
-
to_string `Cites (* returns "cites" *)
-
to_string `UsesMethodIn (* returns "usesMethodIn" *)
-
to_string (`Other "customTerm") (* returns "customTerm" *)
+
to_string `Cites (* returns "cites" *) to_string `UsesMethodIn
+
(* returns "usesMethodIn" *) to_string (`Other "customTerm")
+
(* returns "customTerm" *)
]} *)
-
val to_string : t -> string
-
(** {1 Comparison} *)
+
val equal : t -> t -> bool
(** [equal a b] tests equality between two CiTO annotations.
-
Two annotations are equal if they represent the same CiTO term.
-
For [`Other] variants, string comparison is case-sensitive. *)
-
val equal : t -> t -> bool
-
+
Two annotations are equal if they represent the same CiTO term. For [`Other]
+
variants, string comparison is case-sensitive. *)
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for CiTO annotations.
-
Maps CiTO intent strings to the corresponding variants.
-
Unknown intents are mapped to [`Other s]. *)
-
+
Maps CiTO intent strings to the corresponding variants. Unknown intents are
+
mapped to [`Other s]. *)
(** {1 Pretty Printing} *)
+
val pp : Format.formatter -> t -> unit
(** [pp ppf t] pretty prints a CiTO annotation to the formatter.
{b Example output:}
{v citesAsRecommendedReading v} *)
-
val pp : Format.formatter -> t -> unit
+21 -22
lib/hub.ml
···
let is_empty = function [] -> true | _ -> false
end
-
type t = {
-
type_ : string;
-
url : string;
-
unknown : Unknown.t;
-
}
+
type t = { type_ : string; url : string; unknown : Unknown.t }
-
let create ~type_ ~url ?(unknown = Unknown.empty) () =
-
{ type_; url; unknown }
-
+
let create ~type_ ~url ?(unknown = Unknown.empty) () = { type_; url; unknown }
let type_ t = t.type_
let url t = t.url
let unknown t = t.unknown
-
-
let equal a b =
-
a.type_ = b.type_ && a.url = b.url
-
-
let pp ppf t =
-
Format.fprintf ppf "%s: %s" t.type_ t.url
+
let equal a b = a.type_ = b.type_ && a.url = b.url
+
let pp ppf t = Format.fprintf ppf "%s: %s" t.type_ t.url
let jsont =
let kind = "Hub" in
let doc = "A hub endpoint" in
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
((name, Jsont.Meta.none), value) :: acc
in
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
+
in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
let create_obj type_ url unknown = create ~type_ ~url ~unknown () in
+6 -16
lib/hub.mli
···
@see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
-
-
(** The type representing a hub endpoint. *)
type t
-
+
(** The type representing a hub endpoint. *)
(** {1 Unknown Fields} *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
val is_empty : t -> bool
(** [is_empty u] returns [true] if there are no unknown fields. *)
end
-
(** {1 Jsont Type} *)
···
Maps JSON objects with "type" and "url" fields (both required). *)
-
(** {1 Construction} *)
-
val create :
-
type_:string -> url:string ->
-
?unknown:Unknown.t -> unit -> t
+
val create : type_:string -> url:string -> ?unknown:Unknown.t -> unit -> t
(** [create ~type_ ~url ?unknown ()] creates a hub object.
@param type_ The type of hub protocol (e.g., ["rssCloud"], ["WebSub"])
···
{b Example:}
{[
-
let hub = Hub.create
-
~type_:"WebSub"
-
~url:"https://pubsubhubbub.appspot.com/" ()
+
let hub =
+
Hub.create ~type_:"WebSub" ~url:"https://pubsubhubbub.appspot.com/" ()
]} *)
-
(** {1 Accessors} *)
···
val unknown : t -> Unknown.t
(** [unknown t] returns unrecognized fields from the JSON. *)
-
(** {1 Comparison} *)
val equal : t -> t -> bool
(** [equal a b] tests equality between two hubs. *)
-
(** {1 Pretty Printing} *)
+65 -30
lib/item.ml
···
let is_empty = function [] -> true | _ -> false
end
-
type content = [
-
| `Html of string
-
| `Text of string
-
| `Both of string * string
-
]
+
type content = [ `Html of string | `Text of string | `Both of string * string ]
type t = {
id : string;
···
}
let create ~id ~content ?url ?external_url ?title ?summary ?image ?banner_image
-
?date_published ?date_modified ?authors ?tags ?language ?attachments ?references
-
?(unknown = Unknown.empty) () =
+
?date_published ?date_modified ?authors ?tags ?language ?attachments
+
?references ?(unknown = Unknown.empty) () =
{
-
id; content; url; external_url; title; summary; image; banner_image;
-
date_published; date_modified; authors; tags; language; attachments; references;
+
id;
+
content;
+
url;
+
external_url;
+
title;
+
summary;
+
image;
+
banner_image;
+
date_published;
+
date_modified;
+
authors;
+
tags;
+
language;
+
attachments;
+
references;
unknown;
}
···
let equal a b = a.id = b.id
let compare a b =
-
match a.date_published, b.date_published with
+
match (a.date_published, b.date_published) with
| None, None -> 0
| None, Some _ -> -1
| Some _, None -> 1
| Some da, Some db -> Ptime.compare da db
let pp ppf t =
-
match t.date_published, t.title with
+
match (t.date_published, t.title) with
| Some date, Some title ->
let (y, m, d), _ = Ptime.to_date_time date in
Format.fprintf ppf "[%04d-%02d-%02d] %s (%s)" y m d title t.id
| Some date, None ->
let (y, m, d), _ = Ptime.to_date_time date in
Format.fprintf ppf "[%04d-%02d-%02d] %s" y m d t.id
-
| None, Some title ->
-
Format.fprintf ppf "%s (%s)" title t.id
-
| None, None ->
-
Format.fprintf ppf "%s" t.id
+
| None, Some title -> Format.fprintf ppf "%s (%s)" title t.id
+
| None, None -> Format.fprintf ppf "%s" t.id
let pp_summary ppf t =
match t.title with
···
image banner_image date_published date_modified authors tags language
attachments references _extensions unknown =
(* Determine content from content_html and content_text *)
-
let content = match content_html, content_text with
+
let content =
+
match (content_html, content_text) with
| Some html, Some text -> `Both (html, text)
| Some html, None -> `Html html
| None, Some text -> `Text text
···
Jsont.Error.msg Jsont.Meta.none
"Item must have at least one of content_html or content_text"
in
-
{ id; content; url; external_url; title; summary; image; banner_image;
-
date_published; date_modified; authors; tags; language; attachments;
-
references; unknown }
+
{
+
id;
+
content;
+
url;
+
external_url;
+
title;
+
summary;
+
image;
+
banner_image;
+
date_published;
+
date_modified;
+
authors;
+
tags;
+
language;
+
attachments;
+
references;
+
unknown;
+
}
in
(* Encoders to extract fields from item *)
···
let enc_references t = t.references in
let enc_unknown t = t.unknown in
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
((name, Jsont.Meta.none), value) :: acc
in
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
+
in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
···
|> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:enc_authors
|> Jsont.Object.opt_mem "tags" (Jsont.list Jsont.string) ~enc:enc_tags
|> Jsont.Object.opt_mem "language" Jsont.string ~enc:enc_language
-
|> Jsont.Object.opt_mem "attachments" (Jsont.list Attachment.jsont) ~enc:enc_attachments
-
|> Jsont.Object.opt_mem "_references" (Jsont.list Reference.jsont) ~enc:enc_references
+
|> Jsont.Object.opt_mem "attachments"
+
(Jsont.list Attachment.jsont)
+
~enc:enc_attachments
+
|> Jsont.Object.opt_mem "_references"
+
(Jsont.list Reference.jsont)
+
~enc:enc_references
|> Jsont.Object.opt_mem "_extensions" Jsont.json_object ~enc:(fun _t -> None)
|> Jsont.Object.keep_unknown unknown_mems ~enc:enc_unknown
|> Jsont.Object.finish
+12 -21
lib/item.mli
···
(** Feed items in a JSON Feed.
-
An item represents a single entry in a feed, such as a blog post, podcast episode,
-
or microblog entry. Each item must have a unique identifier and content.
+
An item represents a single entry in a feed, such as a blog post, podcast
+
episode, or microblog entry. Each item must have a unique identifier and
+
content.
@see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
-
-
(** The type representing a feed item. *)
type t
+
(** The type representing a feed item. *)
+
type content = [ `Html of string | `Text of string | `Both of string * string ]
(** Content representation for an item.
-
The JSON Feed specification requires that each item has at least one
-
form of content. This type enforces that requirement at compile time.
+
The JSON Feed specification requires that each item has at least one form of
+
content. This type enforces that requirement at compile time.
- [`Html s]: Item has HTML content only
- [`Text s]: Item has plain text content only
- [`Both (html, text)]: Item has both HTML and plain text versions *)
-
type content = [
-
| `Html of string
-
| `Text of string
-
| `Both of string * string
-
]
-
(** {1 Unknown Fields} *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
(** [is_empty u] returns [true] if there are no unknown fields. *)
end
-
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for feed items.
-
Maps JSON objects with "id" (required), content fields, and various optional metadata.
-
The content must have at least one of "content_html" or "content_text". *)
-
+
Maps JSON objects with "id" (required), content fields, and various optional
+
metadata. The content must have at least one of "content_html" or
+
"content_text". *)
(** {1 Construction} *)
···
?unknown:Unknown.t ->
unit ->
t
-
(** {1 Accessors} *)
···
val references : t -> Reference.t list option
val unknown : t -> Unknown.t
-
(** {1 Comparison} *)
val equal : t -> t -> bool
val compare : t -> t -> int
-
(** {1 Pretty Printing} *)
+50 -43
lib/jsonfeed.ml
···
unknown : Unknown.t;
}
-
let create ~title ?home_page_url ?feed_url ?description ?user_comment
-
?next_url ?icon ?favicon ?authors ?language ?expired ?hubs ~items
+
let create ~title ?home_page_url ?feed_url ?description ?user_comment ?next_url
+
?icon ?favicon ?authors ?language ?expired ?hubs ~items
?(unknown = Unknown.empty) () =
{
version = "https://jsonfeed.org/version/1.1";
···
let hubs t = t.hubs
let items t = t.items
let unknown t = t.unknown
-
-
let equal a b =
-
a.title = b.title &&
-
a.items = b.items
+
let equal a b = a.title = b.title && a.items = b.items
let pp ppf t =
Format.fprintf ppf "Feed: %s (%d items)" t.title (List.length t.items)
···
let jsont =
let kind = "JSON Feed" in
let doc = "A JSON Feed document" in
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
((name, Jsont.Meta.none), value) :: acc
in
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
+
in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
(* Helper constructor that sets version automatically *)
-
let make_from_json _version title home_page_url feed_url description user_comment
-
next_url icon favicon authors language expired hubs items unknown =
+
let make_from_json _version title home_page_url feed_url description
+
user_comment next_url icon favicon authors language expired hubs items
+
unknown =
{
version = "https://jsonfeed.org/version/1.1";
title;
···
let encode_string ?format ?number_format feed =
Jsont_bytesrw.encode_string' ?format ?number_format jsont feed
-
let of_string s =
-
decode_string s
+
let of_string s = decode_string s
-
let to_string ?(minify=false) feed =
+
let to_string ?(minify = false) feed =
let format = if minify then Jsont.Minify else Jsont.Indent in
encode_string ~format feed
···
let add_error msg = errors := msg :: !errors in
(* Check required fields *)
-
if feed.title = "" then
-
add_error "title is required and cannot be empty";
+
if feed.title = "" then add_error "title is required and cannot be empty";
(* Check items have unique IDs *)
let ids = List.map Item.id feed.items in
···
(* Validate authors *)
(match feed.authors with
-
| Some authors ->
-
List.iteri (fun i author ->
-
if not (Author.is_valid author) then
-
add_error (Printf.sprintf "feed author %d is invalid (needs at least one field)" i)
-
) authors
-
| None -> ());
+
| Some authors ->
+
List.iteri
+
(fun i author ->
+
if not (Author.is_valid author) then
+
add_error
+
(Printf.sprintf
+
"feed author %d is invalid (needs at least one field)" i))
+
authors
+
| None -> ());
(* Validate items *)
-
List.iteri (fun i item ->
-
if Item.id item = "" then
-
add_error (Printf.sprintf "item %d has empty ID" i);
+
List.iteri
+
(fun i item ->
+
if Item.id item = "" then
+
add_error (Printf.sprintf "item %d has empty ID" i);
-
(* Validate item authors *)
-
(match Item.authors item with
-
| Some authors ->
-
List.iteri (fun j author ->
-
if not (Author.is_valid author) then
-
add_error (Printf.sprintf "item %d author %d is invalid" i j)
-
) authors
-
| None -> ())
-
) feed.items;
+
(* Validate item authors *)
+
match Item.authors item with
+
| Some authors ->
+
List.iteri
+
(fun j author ->
+
if not (Author.is_valid author) then
+
add_error (Printf.sprintf "item %d author %d is invalid" i j))
+
authors
+
| None -> ())
+
feed.items;
-
match !errors with
-
| [] -> Ok ()
-
| errs -> Error (List.rev errs)
+
match !errors with [] -> Ok () | errs -> Error (List.rev errs)
+29 -22
lib/jsonfeed.mli
···
@see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
-
-
(** The type representing a complete JSON Feed. *)
type t
-
+
(** The type representing a complete JSON Feed. *)
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for JSON feeds.
-
Maps the complete JSON Feed 1.1 specification including all required
-
and optional fields. *)
+
Maps the complete JSON Feed 1.1 specification including all required and
+
optional fields. *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
(** {1 Encoding and Decoding} *)
val decode :
-
?layout:bool -> ?locs:bool -> ?file:string ->
-
Bytesrw.Bytes.Reader.t -> (t, Jsont.Error.t) result
+
?layout:bool ->
+
?locs:bool ->
+
?file:string ->
+
Bytesrw.Bytes.Reader.t ->
+
(t, Jsont.Error.t) result
(** [decode r] decodes a JSON Feed from bytesrw reader [r].
@param layout Preserve whitespace for round-tripping (default: false)
···
@param file Source file name for error reporting *)
val decode_string :
-
?layout:bool -> ?locs:bool -> ?file:string ->
-
string -> (t, Jsont.Error.t) result
+
?layout:bool ->
+
?locs:bool ->
+
?file:string ->
+
string ->
+
(t, Jsont.Error.t) result
(** [decode_string s] decodes a JSON Feed from string [s]. *)
val encode :
-
?format:Jsont.format -> ?number_format:Jsont.number_format ->
-
t -> eod:bool -> Bytesrw.Bytes.Writer.t -> (unit, Jsont.Error.t) result
+
?format:Jsont.format ->
+
?number_format:Jsont.number_format ->
+
t ->
+
eod:bool ->
+
Bytesrw.Bytes.Writer.t ->
+
(unit, Jsont.Error.t) result
(** [encode feed w] encodes [feed] to bytesrw writer [w].
-
@param format Output formatting: [Jsont.Minify] or [Jsont.Indent] (default: Minify)
+
@param format
+
Output formatting: [Jsont.Minify] or [Jsont.Indent] (default: Minify)
@param number_format Printf format for numbers (default: "%.16g")
@param eod Write end-of-data marker *)
val encode_string :
-
?format:Jsont.format -> ?number_format:Jsont.number_format ->
-
t -> (string, Jsont.Error.t) result
+
?format:Jsont.format ->
+
?number_format:Jsont.number_format ->
+
t ->
+
(string, Jsont.Error.t) result
(** [encode_string feed] encodes [feed] to a string. *)
-
val of_string : string -> (t, Jsont.Error.t) result
(** Alias for [decode_string] with default options. *)
···
val to_string : ?minify:bool -> t -> (string, Jsont.Error.t) result
(** [to_string feed] encodes [feed] to string.
@param minify Use compact format (true) or indented (false, default) *)
-
(** {1 Validation} *)
val validate : t -> (unit, string list) result
-
(** [validate feed] validates the feed structure.
-
Checks for unique item IDs, valid content, etc. *)
-
+
(** [validate feed] validates the feed structure. Checks for unique item IDs,
+
valid content, etc. *)
(** {1 Comparison} *)
val equal : t -> t -> bool
(** [equal a b] tests equality between two feeds. *)
-
(** {1 Pretty Printing} *)
+18 -13
lib/reference.ml
···
let doi t = t.doi
let cito t = t.cito
let unknown t = t.unknown
-
let equal a b = String.equal a.url b.url
let pp ppf t =
let open Format in
fprintf ppf "%s" t.url;
-
match t.doi with
-
| Some d -> fprintf ppf " [DOI: %s]" d
-
| None -> ()
+
match t.doi with Some d -> fprintf ppf " [DOI: %s]" d | None -> ()
let jsont =
let kind = "Reference" in
let doc = "A reference to a cited source" in
-
let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
+
let unknown_mems :
+
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
let open Jsont.Object.Mems in
let dec_empty () = [] in
let dec_add _meta (name : string) value acc =
((name, Jsont.Meta.none), value) :: acc
in
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
-
let enc = {
-
enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
-
List.fold_left (fun acc (name, value) ->
-
-
f Jsont.Meta.none name value acc
-
) acc unknown
-
} in
+
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
+
in
+
let enc =
+
{
+
enc =
+
(fun (type acc)
+
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
+
unknown
+
(acc : acc)
+
->
+
List.fold_left
+
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
+
acc unknown);
+
}
+
in
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
in
let create_obj url doi cito unknown = create ~url ?doi ?cito ~unknown () in
+24 -28
lib/reference.mli
···
(** References extension for JSON Feed items.
This implements the references extension that allows items to cite sources.
-
Each reference represents a cited resource with optional DOI and CiTO annotations.
+
Each reference represents a cited resource with optional DOI and CiTO
+
annotations.
-
@see <https://github.com/egonw/JSONFeed-extensions/blob/main/references.md> References Extension Specification
+
@see <https://github.com/egonw/JSONFeed-extensions/blob/main/references.md>
+
References Extension Specification
@see <https://purl.archive.org/spar/cito> Citation Typing Ontology *)
-
+
type t
(** The type representing a reference to a cited source. *)
-
type t
-
(** {1 Unknown Fields} *)
module Unknown : sig
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members.
-
Useful for preserving fields from custom extensions or future spec versions. *)
+
(** Unknown/unrecognized JSON object members. Useful for preserving fields
+
from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
···
(** [is_empty u] returns [true] if there are no unknown fields. *)
end
-
(** {1 Jsont Type} *)
val jsont : t Jsont.t
(** Declarative JSON type for references.
-
Maps JSON objects with "url" (required) and optional "doi" and "cito" fields. *)
-
+
Maps JSON objects with "url" (required) and optional "doi" and "cito"
+
fields. *)
(** {1 Construction} *)
···
t
(** [create ~url ?doi ?cito ?unknown ()] creates a reference.
-
@param url Unique URL for the reference (required).
-
A URL based on a persistent unique identifier (like DOI) is recommended.
+
@param url
+
Unique URL for the reference (required). A URL based on a persistent
+
unique identifier (like DOI) is recommended.
@param doi Digital Object Identifier for the reference
@param cito Citation Typing Ontology intent annotations
@param unknown Unknown/custom fields for extensions (default: empty)
···
{b Examples:}
{[
(* Simple reference with just a URL *)
-
let ref1 = Reference.create
-
~url:"https://doi.org/10.5281/zenodo.16755947"
-
()
+
let ref1 =
+
Reference.create ~url:"https://doi.org/10.5281/zenodo.16755947" ()
(* Reference with DOI *)
-
let ref2 = Reference.create
-
~url:"https://doi.org/10.5281/zenodo.16755947"
-
~doi:"10.5281/zenodo.16755947"
-
()
+
let ref2 =
+
Reference.create ~url:"https://doi.org/10.5281/zenodo.16755947"
+
~doi:"10.5281/zenodo.16755947" ()
(* Reference with CiTO annotations *)
-
let ref3 = Reference.create
-
~url:"https://doi.org/10.5281/zenodo.16755947"
-
~doi:"10.5281/zenodo.16755947"
-
~cito:[`CitesAsRecommendedReading; `UsesMethodIn]
-
()
+
let ref3 =
+
Reference.create ~url:"https://doi.org/10.5281/zenodo.16755947"
+
~doi:"10.5281/zenodo.16755947"
+
~cito:[ `CitesAsRecommendedReading; `UsesMethodIn ]
+
()
]} *)
-
(** {1 Accessors} *)
val url : t -> string
···
val unknown : t -> Unknown.t
(** [unknown t] returns unrecognized fields from the JSON. *)
-
(** {1 Comparison} *)
val equal : t -> t -> bool
(** [equal a b] tests equality between two references.
References are considered equal if they have the same URL. *)
-
(** {1 Pretty Printing} *)
···
(** [pp ppf t] pretty prints a reference to the formatter.
{b Example output:}
-
{v https://doi.org/10.5281/zenodo.16755947 [DOI: 10.5281/zenodo.16755947] v} *)
+
{v https://doi.org/10.5281/zenodo.16755947 [DOI: 10.5281/zenodo.16755947] v}
+
*)
+8 -10
lib/rfc3339.ml
···
---------------------------------------------------------------------------*)
let parse s =
-
match Ptime.of_rfc3339 s with
-
| Ok (t, _, _) -> Some t
-
| Error _ -> None
+
match Ptime.of_rfc3339 s with Ok (t, _, _) -> Some t | Error _ -> None
-
let format t =
-
Ptime.to_rfc3339 ~frac_s:6 ~tz_offset_s:0 t
-
-
let pp ppf t =
-
Format.pp_print_string ppf (format t)
+
let format t = Ptime.to_rfc3339 ~frac_s:6 ~tz_offset_s:0 t
+
let pp ppf t = Format.pp_print_string ppf (format t)
let jsont =
let kind = "RFC 3339 timestamp" in
let doc = "An RFC 3339 date-time string" in
-
let dec s = match parse s with
+
let dec s =
+
match parse s with
| Some t -> t
-
| None -> Jsont.Error.msgf Jsont.Meta.none "%s: invalid RFC 3339 timestamp: %S" kind s
+
| None ->
+
Jsont.Error.msgf Jsont.Meta.none "%s: invalid RFC 3339 timestamp: %S"
+
kind s
in
let enc = format in
Jsont.map ~kind ~doc ~dec ~enc Jsont.string
+4 -5
lib/rfc3339.mli
···
@see <https://www.rfc-editor.org/rfc/rfc3339> RFC 3339 *)
-
val jsont : Ptime.t Jsont.t
(** [jsont] is a bidirectional JSON type for RFC 3339 timestamps.
-
On decode: accepts JSON strings in RFC 3339 format (e.g., "2024-11-03T10:30:00Z")
-
On encode: produces UTC timestamps with 'Z' suffix
+
On decode: accepts JSON strings in RFC 3339 format (e.g.,
+
"2024-11-03T10:30:00Z") On encode: produces UTC timestamps with 'Z' suffix
{b Example:}
{[
···
val format : Ptime.t -> string
(** [format t] formats a timestamp as RFC 3339.
-
Always uses UTC timezone (Z suffix) and includes fractional seconds
-
if the timestamp has sub-second precision.
+
Always uses UTC timezone (Z suffix) and includes fractional seconds if the
+
timestamp has sub-second precision.
{b Example output:} ["2024-11-03T10:30:45.123Z"] *)
+3 -2
test/dune
···
(libraries jsonfeed))
(cram
-
(deps test_location_errors.exe
-
(glob_files data/*.json)))
+
(deps
+
test_location_errors.exe
+
(glob_files data/*.json)))
+204 -159
test/test_jsonfeed.ml
···
let test_author_create_with_url () =
let author = Author.create ~url:"https://example.com" () in
Alcotest.(check (option string)) "name" None (Author.name author);
-
Alcotest.(check (option string)) "url" (Some "https://example.com") (Author.url author);
+
Alcotest.(check (option string))
+
"url" (Some "https://example.com") (Author.url author);
Alcotest.(check bool) "is_valid" true (Author.is_valid author)
let test_author_create_with_all_fields () =
-
let author = Author.create
-
~name:"Jane Doe"
-
~url:"https://example.com"
-
~avatar:"https://example.com/avatar.png"
-
() in
+
let author =
+
Author.create ~name:"Jane Doe" ~url:"https://example.com"
+
~avatar:"https://example.com/avatar.png" ()
+
in
Alcotest.(check (option string)) "name" (Some "Jane Doe") (Author.name author);
-
Alcotest.(check (option string)) "url" (Some "https://example.com") (Author.url author);
-
Alcotest.(check (option string)) "avatar" (Some "https://example.com/avatar.png") (Author.avatar author);
+
Alcotest.(check (option string))
+
"url" (Some "https://example.com") (Author.url author);
+
Alcotest.(check (option string))
+
"avatar" (Some "https://example.com/avatar.png") (Author.avatar author);
Alcotest.(check bool) "is_valid" true (Author.is_valid author)
let test_author_create_no_fields_fails () =
Alcotest.check_raises "no fields"
-
(Invalid_argument "Author.create: at least one field (name, url, or avatar) must be provided")
-
(fun () -> ignore (Author.create ()))
+
(Invalid_argument
+
"Author.create: at least one field (name, url, or avatar) must be \
+
provided") (fun () -> ignore (Author.create ()))
let test_author_equal () =
let a1 = Author.create ~name:"Jane Doe" () in
···
let test_author_pp () =
let author = Author.create ~name:"Jane Doe" ~url:"https://example.com" () in
let s = Format.asprintf "%a" Author.pp author in
-
Alcotest.(check string) "pp with name and url" "Jane Doe <https://example.com>" s
+
Alcotest.(check string)
+
"pp with name and url" "Jane Doe <https://example.com>" s
-
let author_tests = [
-
"create with name", `Quick, test_author_create_with_name;
-
"create with url", `Quick, test_author_create_with_url;
-
"create with all fields", `Quick, test_author_create_with_all_fields;
-
"create with no fields fails", `Quick, test_author_create_no_fields_fails;
-
"equal", `Quick, test_author_equal;
-
"pp", `Quick, test_author_pp;
-
]
+
let author_tests =
+
[
+
("create with name", `Quick, test_author_create_with_name);
+
("create with url", `Quick, test_author_create_with_url);
+
("create with all fields", `Quick, test_author_create_with_all_fields);
+
("create with no fields fails", `Quick, test_author_create_no_fields_fails);
+
("equal", `Quick, test_author_equal);
+
("pp", `Quick, test_author_pp);
+
]
(* Attachment tests *)
let test_attachment_create_minimal () =
-
let att = Attachment.create
-
~url:"https://example.com/file.mp3"
-
~mime_type:"audio/mpeg"
-
() in
-
Alcotest.(check string) "url" "https://example.com/file.mp3" (Attachment.url att);
+
let att =
+
Attachment.create ~url:"https://example.com/file.mp3"
+
~mime_type:"audio/mpeg" ()
+
in
+
Alcotest.(check string)
+
"url" "https://example.com/file.mp3" (Attachment.url att);
Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att);
Alcotest.(check (option string)) "title" None (Attachment.title att);
-
Alcotest.(check (option int64)) "size_in_bytes" None (Attachment.size_in_bytes att);
-
Alcotest.(check (option int)) "duration_in_seconds" None (Attachment.duration_in_seconds att)
+
Alcotest.(check (option int64))
+
"size_in_bytes" None
+
(Attachment.size_in_bytes att);
+
Alcotest.(check (option int))
+
"duration_in_seconds" None
+
(Attachment.duration_in_seconds att)
let test_attachment_create_complete () =
-
let att = Attachment.create
-
~url:"https://example.com/episode.mp3"
-
~mime_type:"audio/mpeg"
-
~title:"Episode 1"
-
~size_in_bytes:15_728_640L
-
~duration_in_seconds:1800
-
() in
-
Alcotest.(check string) "url" "https://example.com/episode.mp3" (Attachment.url att);
+
let att =
+
Attachment.create ~url:"https://example.com/episode.mp3"
+
~mime_type:"audio/mpeg" ~title:"Episode 1" ~size_in_bytes:15_728_640L
+
~duration_in_seconds:1800 ()
+
in
+
Alcotest.(check string)
+
"url" "https://example.com/episode.mp3" (Attachment.url att);
Alcotest.(check string) "mime_type" "audio/mpeg" (Attachment.mime_type att);
-
Alcotest.(check (option string)) "title" (Some "Episode 1") (Attachment.title att);
-
Alcotest.(check (option int64)) "size_in_bytes" (Some 15_728_640L) (Attachment.size_in_bytes att);
-
Alcotest.(check (option int)) "duration_in_seconds" (Some 1800) (Attachment.duration_in_seconds att)
+
Alcotest.(check (option string))
+
"title" (Some "Episode 1") (Attachment.title att);
+
Alcotest.(check (option int64))
+
"size_in_bytes" (Some 15_728_640L)
+
(Attachment.size_in_bytes att);
+
Alcotest.(check (option int))
+
"duration_in_seconds" (Some 1800)
+
(Attachment.duration_in_seconds att)
let test_attachment_equal () =
-
let a1 = Attachment.create
-
~url:"https://example.com/file.mp3"
-
~mime_type:"audio/mpeg"
-
() in
-
let a2 = Attachment.create
-
~url:"https://example.com/file.mp3"
-
~mime_type:"audio/mpeg"
-
() in
-
let a3 = Attachment.create
-
~url:"https://example.com/other.mp3"
-
~mime_type:"audio/mpeg"
-
() in
+
let a1 =
+
Attachment.create ~url:"https://example.com/file.mp3"
+
~mime_type:"audio/mpeg" ()
+
in
+
let a2 =
+
Attachment.create ~url:"https://example.com/file.mp3"
+
~mime_type:"audio/mpeg" ()
+
in
+
let a3 =
+
Attachment.create ~url:"https://example.com/other.mp3"
+
~mime_type:"audio/mpeg" ()
+
in
Alcotest.(check bool) "equal same" true (Attachment.equal a1 a2);
Alcotest.(check bool) "equal different" false (Attachment.equal a1 a3)
-
let attachment_tests = [
-
"create minimal", `Quick, test_attachment_create_minimal;
-
"create complete", `Quick, test_attachment_create_complete;
-
"equal", `Quick, test_attachment_equal;
-
]
+
let attachment_tests =
+
[
+
("create minimal", `Quick, test_attachment_create_minimal);
+
("create complete", `Quick, test_attachment_create_complete);
+
("equal", `Quick, test_attachment_equal);
+
]
(* Hub tests *)
···
Alcotest.(check bool) "equal same" true (Hub.equal h1 h2);
Alcotest.(check bool) "equal different" false (Hub.equal h1 h3)
-
let hub_tests = [
-
"create", `Quick, test_hub_create;
-
"equal", `Quick, test_hub_equal;
-
]
+
let hub_tests =
+
[ ("create", `Quick, test_hub_create); ("equal", `Quick, test_hub_equal) ]
(* Item tests *)
let test_item_create_html () =
-
let item = Item.create
-
~id:"https://example.com/1"
-
~content:(`Html "<p>Hello</p>")
-
() in
+
let item =
+
Item.create ~id:"https://example.com/1" ~content:(`Html "<p>Hello</p>") ()
+
in
Alcotest.(check string) "id" "https://example.com/1" (Item.id item);
-
Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item);
+
Alcotest.(check (option string))
+
"content_html" (Some "<p>Hello</p>") (Item.content_html item);
Alcotest.(check (option string)) "content_text" None (Item.content_text item)
let test_item_create_text () =
-
let item = Item.create
-
~id:"https://example.com/2"
-
~content:(`Text "Hello world")
-
() in
+
let item =
+
Item.create ~id:"https://example.com/2" ~content:(`Text "Hello world") ()
+
in
Alcotest.(check string) "id" "https://example.com/2" (Item.id item);
Alcotest.(check (option string)) "content_html" None (Item.content_html item);
-
Alcotest.(check (option string)) "content_text" (Some "Hello world") (Item.content_text item)
+
Alcotest.(check (option string))
+
"content_text" (Some "Hello world") (Item.content_text item)
let test_item_create_both () =
-
let item = Item.create
-
~id:"https://example.com/3"
-
~content:(`Both ("<p>Hello</p>", "Hello"))
-
() in
+
let item =
+
Item.create ~id:"https://example.com/3"
+
~content:(`Both ("<p>Hello</p>", "Hello"))
+
()
+
in
Alcotest.(check string) "id" "https://example.com/3" (Item.id item);
-
Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item);
-
Alcotest.(check (option string)) "content_text" (Some "Hello") (Item.content_text item)
+
Alcotest.(check (option string))
+
"content_html" (Some "<p>Hello</p>") (Item.content_html item);
+
Alcotest.(check (option string))
+
"content_text" (Some "Hello") (Item.content_text item)
let test_item_with_metadata () =
-
let item = Item.create
-
~id:"https://example.com/4"
-
~content:(`Html "<p>Test</p>")
-
~title:"Test Post"
-
~url:"https://example.com/posts/4"
-
~tags:["test"; "example"]
-
() in
+
let item =
+
Item.create ~id:"https://example.com/4" ~content:(`Html "<p>Test</p>")
+
~title:"Test Post" ~url:"https://example.com/posts/4"
+
~tags:[ "test"; "example" ] ()
+
in
Alcotest.(check (option string)) "title" (Some "Test Post") (Item.title item);
-
Alcotest.(check (option string)) "url" (Some "https://example.com/posts/4") (Item.url item);
-
Alcotest.(check (option (list string))) "tags" (Some ["test"; "example"]) (Item.tags item)
+
Alcotest.(check (option string))
+
"url" (Some "https://example.com/posts/4") (Item.url item);
+
Alcotest.(check (option (list string)))
+
"tags"
+
(Some [ "test"; "example" ])
+
(Item.tags item)
let test_item_equal () =
let i1 = Item.create ~id:"https://example.com/1" ~content:(`Text "test") () in
-
let i2 = Item.create ~id:"https://example.com/1" ~content:(`Html "<p>test</p>") () in
+
let i2 =
+
Item.create ~id:"https://example.com/1" ~content:(`Html "<p>test</p>") ()
+
in
let i3 = Item.create ~id:"https://example.com/2" ~content:(`Text "test") () in
Alcotest.(check bool) "equal same id" true (Item.equal i1 i2);
Alcotest.(check bool) "equal different id" false (Item.equal i1 i3)
-
let item_tests = [
-
"create with HTML content", `Quick, test_item_create_html;
-
"create with text content", `Quick, test_item_create_text;
-
"create with both contents", `Quick, test_item_create_both;
-
"create with metadata", `Quick, test_item_with_metadata;
-
"equal", `Quick, test_item_equal;
-
]
+
let item_tests =
+
[
+
("create with HTML content", `Quick, test_item_create_html);
+
("create with text content", `Quick, test_item_create_text);
+
("create with both contents", `Quick, test_item_create_both);
+
("create with metadata", `Quick, test_item_with_metadata);
+
("equal", `Quick, test_item_equal);
+
]
(* Jsonfeed tests *)
let test_feed_create_minimal () =
let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in
Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed);
-
Alcotest.(check string) "version" "https://jsonfeed.org/version/1.1" (Jsonfeed.version feed);
+
Alcotest.(check string)
+
"version" "https://jsonfeed.org/version/1.1" (Jsonfeed.version feed);
Alcotest.(check int) "items length" 0 (List.length (Jsonfeed.items feed))
let test_feed_create_with_items () =
-
let item = Item.create
-
~id:"https://example.com/1"
-
~content:(`Text "Hello")
-
() in
-
let feed = Jsonfeed.create
-
~title:"Test Feed"
-
~items:[item]
-
() in
+
let item =
+
Item.create ~id:"https://example.com/1" ~content:(`Text "Hello") ()
+
in
+
let feed = Jsonfeed.create ~title:"Test Feed" ~items:[ item ] () in
Alcotest.(check int) "items length" 1 (List.length (Jsonfeed.items feed))
let test_feed_validate_valid () =
···
match Jsonfeed.validate feed with
| Ok () -> ()
| Error errors ->
-
Alcotest.fail (Printf.sprintf "Validation should succeed: %s"
-
(String.concat "; " errors))
+
Alcotest.fail
+
(Printf.sprintf "Validation should succeed: %s"
+
(String.concat "; " errors))
let test_feed_validate_empty_title () =
let feed = Jsonfeed.create ~title:"" ~items:[] () in
match Jsonfeed.validate feed with
| Ok () -> Alcotest.fail "Should fail validation"
| Error errors ->
-
Alcotest.(check bool) "has error" true
+
Alcotest.(check bool)
+
"has error" true
(List.exists (fun s -> String.starts_with ~prefix:"title" s) errors)
let contains_substring s sub =
···
let feed = Jsonfeed.create ~title:"Test Feed" ~items:[] () in
match Jsonfeed.to_string feed with
| Ok json ->
-
Alcotest.(check bool) "contains version" true (contains_substring json "version");
-
Alcotest.(check bool) "contains title" true (contains_substring json "Test Feed")
+
Alcotest.(check bool)
+
"contains version" true
+
(contains_substring json "version");
+
Alcotest.(check bool)
+
"contains title" true
+
(contains_substring json "Test Feed")
| Error e ->
-
Alcotest.fail (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
+
Alcotest.fail
+
(Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
let test_feed_parse_minimal () =
-
let json = {|{
+
let json =
+
{|{
"version": "https://jsonfeed.org/version/1.1",
"title": "Test Feed",
"items": []
-
}|} in
+
}|}
+
in
match Jsonfeed.of_string json with
| Ok feed ->
Alcotest.(check string) "title" "Test Feed" (Jsonfeed.title feed);
Alcotest.(check int) "items" 0 (List.length (Jsonfeed.items feed))
| Error err ->
-
Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
let test_feed_parse_with_item () =
-
let json = {|{
+
let json =
+
{|{
"version": "https://jsonfeed.org/version/1.1",
"title": "Test Feed",
"items": [
···
"content_html": "<p>Hello</p>"
}
]
-
}|} in
+
}|}
+
in
match Jsonfeed.of_string json with
-
| Ok feed ->
+
| Ok feed -> (
let items = Jsonfeed.items feed in
Alcotest.(check int) "items count" 1 (List.length items);
-
(match items with
-
| [item] ->
-
Alcotest.(check string) "item id" "https://example.com/1" (Item.id item);
-
Alcotest.(check (option string)) "content_html" (Some "<p>Hello</p>") (Item.content_html item)
-
| _ -> Alcotest.fail "Expected 1 item")
+
match items with
+
| [ item ] ->
+
Alcotest.(check string)
+
"item id" "https://example.com/1" (Item.id item);
+
Alcotest.(check (option string))
+
"content_html" (Some "<p>Hello</p>") (Item.content_html item)
+
| _ -> Alcotest.fail "Expected 1 item")
| Error err ->
-
Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string err))
let test_feed_roundtrip () =
let author = Author.create ~name:"Test Author" () in
-
let item = Item.create
-
~id:"https://example.com/1"
-
~title:"Test Item"
-
~content:(`Html "<p>Hello, world!</p>")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
-
~tags:["test"; "example"]
-
() in
+
let item =
+
Item.create ~id:"https://example.com/1" ~title:"Test Item"
+
~content:(`Html "<p>Hello, world!</p>")
+
~date_published:
+
(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
+
~tags:[ "test"; "example" ] ()
+
in
-
let feed1 = Jsonfeed.create
-
~title:"Test Feed"
-
~home_page_url:"https://example.com"
-
~authors:[author]
-
~items:[item]
-
() in
+
let feed1 =
+
Jsonfeed.create ~title:"Test Feed" ~home_page_url:"https://example.com"
+
~authors:[ author ] ~items:[ item ] ()
+
in
(* Serialize and parse *)
match Jsonfeed.to_string feed1 with
| Error e ->
-
Alcotest.fail (Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
-
| Ok json ->
+
Alcotest.fail
+
(Printf.sprintf "Serialization failed: %s" (Jsont.Error.to_string e))
+
| Ok json -> (
match Jsonfeed.of_string json with
| Ok feed2 ->
-
Alcotest.(check string) "title" (Jsonfeed.title feed1) (Jsonfeed.title feed2);
-
Alcotest.(check (option string)) "home_page_url"
-
(Jsonfeed.home_page_url feed1) (Jsonfeed.home_page_url feed2);
-
Alcotest.(check int) "items count"
+
Alcotest.(check string)
+
"title" (Jsonfeed.title feed1) (Jsonfeed.title feed2);
+
Alcotest.(check (option string))
+
"home_page_url"
+
(Jsonfeed.home_page_url feed1)
+
(Jsonfeed.home_page_url feed2);
+
Alcotest.(check int)
+
"items count"
(List.length (Jsonfeed.items feed1))
(List.length (Jsonfeed.items feed2))
| Error err ->
-
Alcotest.fail (Printf.sprintf "Round-trip parse failed: %s" (Jsont.Error.to_string err))
+
Alcotest.fail
+
(Printf.sprintf "Round-trip parse failed: %s"
+
(Jsont.Error.to_string err)))
let test_feed_parse_invalid_missing_content () =
-
let json = {|{
+
let json =
+
{|{
"version": "https://jsonfeed.org/version/1.1",
"title": "Test",
"items": [
···
"id": "1"
}
]
-
}|} in
+
}|}
+
in
match Jsonfeed.of_string json with
| Ok _ -> Alcotest.fail "Should reject item without content"
| Error err ->
let err_str = Jsont.Error.to_string err in
-
Alcotest.(check bool) "has error" true
+
Alcotest.(check bool)
+
"has error" true
(contains_substring err_str "content")
-
let jsonfeed_tests = [
-
"create minimal feed", `Quick, test_feed_create_minimal;
-
"create feed with items", `Quick, test_feed_create_with_items;
-
"validate valid feed", `Quick, test_feed_validate_valid;
-
"validate empty title", `Quick, test_feed_validate_empty_title;
-
"to_string", `Quick, test_feed_to_string;
-
"parse minimal feed", `Quick, test_feed_parse_minimal;
-
"parse feed with item", `Quick, test_feed_parse_with_item;
-
"round-trip", `Quick, test_feed_roundtrip;
-
"parse invalid missing content", `Quick, test_feed_parse_invalid_missing_content;
-
]
+
let jsonfeed_tests =
+
[
+
("create minimal feed", `Quick, test_feed_create_minimal);
+
("create feed with items", `Quick, test_feed_create_with_items);
+
("validate valid feed", `Quick, test_feed_validate_valid);
+
("validate empty title", `Quick, test_feed_validate_empty_title);
+
("to_string", `Quick, test_feed_to_string);
+
("parse minimal feed", `Quick, test_feed_parse_minimal);
+
("parse feed with item", `Quick, test_feed_parse_with_item);
+
("round-trip", `Quick, test_feed_roundtrip);
+
( "parse invalid missing content",
+
`Quick,
+
test_feed_parse_invalid_missing_content );
+
]
(* Main test suite *)
let () =
-
Alcotest.run "jsonfeed" [
-
"Author", author_tests;
-
"Attachment", attachment_tests;
-
"Hub", hub_tests;
-
"Item", item_tests;
-
"Jsonfeed", jsonfeed_tests;
-
]
+
Alcotest.run "jsonfeed"
+
[
+
("Author", author_tests);
+
("Attachment", attachment_tests);
+
("Hub", hub_tests);
+
("Item", item_tests);
+
("Jsonfeed", jsonfeed_tests);
+
]
+24 -29
test/test_location_errors.ml
···
(* Helper to format path context *)
let format_context (ctx : Jsont.Error.Context.t) =
-
if Jsont.Error.Context.is_empty ctx then
-
"$"
+
if Jsont.Error.Context.is_empty ctx then "$"
else
let indices = ctx in
let rec format_path acc = function
| [] -> if acc = "" then "$" else "$" ^ acc
| ((_kinded_sort, _meta), idx) :: rest ->
-
let segment = match idx with
+
let segment =
+
match idx with
| Jsont.Path.Mem (name, _meta) -> "." ^ name
| Jsont.Path.Nth (n, _meta) -> "[" ^ string_of_int n ^ "]"
in
···
| "title" -> Jsonfeed.title feed
| "version" -> Jsonfeed.version feed
| "item_count" -> string_of_int (List.length (Jsonfeed.items feed))
-
| "first_item_id" ->
-
(match Jsonfeed.items feed with
-
| [] -> "(no items)"
-
| item :: _ -> Item.id item)
+
| "first_item_id" -> (
+
match Jsonfeed.items feed with
+
| [] -> "(no items)"
+
| item :: _ -> Item.id item)
| _ -> "(unknown field)"
(* Escape JSON strings *)
let escape_json_string s =
let buf = Buffer.create (String.length s) in
-
String.iter (function
-
| '"' -> Buffer.add_string buf "\\\""
-
| '\\' -> Buffer.add_string buf "\\\\"
-
| '\n' -> Buffer.add_string buf "\\n"
-
| '\r' -> Buffer.add_string buf "\\r"
-
| '\t' -> Buffer.add_string buf "\\t"
-
| c when c < ' ' -> Printf.bprintf buf "\\u%04x" (Char.code c)
-
| c -> Buffer.add_char buf c
-
) s;
+
String.iter
+
(function
+
| '"' -> Buffer.add_string buf "\\\""
+
| '\\' -> Buffer.add_string buf "\\\\"
+
| '\n' -> Buffer.add_string buf "\\n"
+
| '\r' -> Buffer.add_string buf "\\r"
+
| '\t' -> Buffer.add_string buf "\\t"
+
| c when c < ' ' -> Printf.bprintf buf "\\u%04x" (Char.code c)
+
| c -> Buffer.add_char buf c)
+
s;
Buffer.contents buf
(* Output success as JSON *)
let output_success field value =
Printf.printf {|{"status":"ok","field":"%s","value":"%s"}|}
-
(escape_json_string field)
-
(escape_json_string value);
+
(escape_json_string field) (escape_json_string value);
print_newline ()
(* Output error as JSON *)
···
let file = Jsont.Textloc.file textloc in
let first_byte = Jsont.Textloc.first_byte textloc in
let last_byte = Jsont.Textloc.last_byte textloc in
-
let (line_num, line_start_byte) = Jsont.Textloc.first_line textloc in
+
let line_num, line_start_byte = Jsont.Textloc.first_line textloc in
let column = first_byte - line_start_byte + 1 in
let context = format_context ctx in
-
Printf.printf {|{"status":"error","message":"%s","location":{"file":"%s","line":%d,"column":%d,"byte_start":%d,"byte_end":%d},"context":"%s"}|}
+
Printf.printf
+
{|{"status":"error","message":"%s","location":{"file":"%s","line":%d,"column":%d,"byte_start":%d,"byte_end":%d},"context":"%s"}|}
(escape_json_string message)
-
(escape_json_string file)
-
line_num
-
column
-
first_byte
-
last_byte
+
(escape_json_string file) line_num column first_byte last_byte
(escape_json_string context);
print_newline ()
···
if Array.length Sys.argv < 2 then (
Printf.eprintf "Usage: %s <file> [field]\n" Sys.argv.(0);
Printf.eprintf "Fields: title, version, item_count, first_item_id\n";
-
exit 1
-
);
+
exit 1);
let file = Sys.argv.(1) in
let field = if Array.length Sys.argv > 2 then Sys.argv.(2) else "title" in
(* Read file *)
let content =
-
try
-
In_channel.with_open_text file In_channel.input_all
+
try In_channel.with_open_text file In_channel.input_all
with Sys_error msg ->
Printf.printf {|{"status":"error","message":"File error: %s"}|}
(escape_json_string msg);
+10 -12
test/test_serialization.ml
···
let () =
(* Create a simple feed *)
let author = Author.create ~name:"Test Author" () in
-
let item = Item.create
-
~id:"https://example.com/1"
-
~title:"Test Item"
-
~content:(`Html "<p>Hello, world!</p>")
-
() in
+
let item =
+
Item.create ~id:"https://example.com/1" ~title:"Test Item"
+
~content:(`Html "<p>Hello, world!</p>") ()
+
in
-
let feed = Jsonfeed.create
-
~title:"Test Feed"
-
~home_page_url:"https://example.com"
-
~authors:[author]
-
~items:[item]
-
() in
+
let feed =
+
Jsonfeed.create ~title:"Test Feed" ~home_page_url:"https://example.com"
+
~authors:[ author ] ~items:[ item ] ()
+
in
(* Serialize to JSON *)
-
let json = match Jsonfeed.to_string feed with
+
let json =
+
match Jsonfeed.to_string feed with
| Ok s -> s
| Error e -> failwith (Jsont.Error.to_string e)
in