OCaml library for JSONfeed parsing and creation

add echo examples

Changed files
+75 -297
example
+5
example/dune
···
(name feed_validator)
(modules feed_validator)
(libraries jsonfeed))
+
+
(executable
+
(name feed_echo)
+
(modules feed_echo)
+
(libraries jsonfeed))
+38
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.
+
+
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 *)
+
+
let echo_feed () =
+
(* Create a bytesrw reader from stdin *)
+
let stdin = Bytesrw.Bytes.Reader.of_in_channel In_channel.stdin in
+
+
(* Parse the JSON feed *)
+
match Jsonfeed.decode ~locs:true stdin with
+
| Error err ->
+
Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err);
+
exit 1
+
+
| 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);
+
exit 1
+
+
| Ok json ->
+
print_string json;
+
print_newline ();
+
exit 0
+
+
let () = echo_feed ()
+32 -297
example/feed_validator.ml
···
-
(** Example: Validating JSON Feeds
+
(** Example: JSON Feed Validator
-
This demonstrates:
-
- Validating feed structure
-
- Testing various edge cases
-
- Handling invalid feeds
-
- Best practices for feed construction *)
+
Reads a JSON Feed from stdin and validates it.
-
open Jsonfeed
+
Usage:
+
feed_validator < feed.json
+
cat feed.json | feed_validator
-
let test_valid_minimal_feed () =
-
Format.printf "=== Test: Minimal Valid Feed ===\n";
+
Exit codes:
+
0 - Feed is valid
+
1 - Feed parsing failed
+
2 - Feed validation failed *)
-
let feed = Jsonfeed.create
-
~title:"Minimal Feed"
-
~items:[]
-
() in
+
let validate_stdin () =
+
let stdin = Bytesrw.Bytes.Reader.of_in_channel In_channel.stdin in
+
match Jsonfeed.decode ~locs:true stdin with
+
| Error err ->
+
Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err);
+
exit 1
+
| Ok feed ->
+
match Jsonfeed.validate feed with
+
| Ok () ->
+
Format.printf "Feed is valid\n%!";
+
Format.printf "\nFeed details:\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 -> ());
+
Format.printf " Items: %d\n" (List.length (Jsonfeed.items feed));
+
exit 0
-
match Jsonfeed.validate feed with
-
| Ok () -> Format.printf "✓ Minimal feed is valid\n\n"
-
| Error errors ->
-
Format.printf "✗ Minimal feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
+
| Error errors ->
+
Format.eprintf "Validation failed:\n%!";
+
List.iter (fun err -> Format.eprintf " - %s\n%!" err) errors;
+
exit 2
-
let test_valid_complete_feed () =
-
Format.printf "=== Test: Complete Valid Feed ===\n";
-
-
let author = Author.create
-
~name:"Test Author"
-
~url:"https://example.com/author"
-
~avatar:"https://example.com/avatar.png"
-
() in
-
-
let attachment = Attachment.create
-
~url:"https://example.com/file.mp3"
-
~mime_type:"audio/mpeg"
-
~title:"Audio File"
-
~size_in_bytes:1024L
-
~duration_in_seconds:60
-
() in
-
-
let item = Item.create
-
~id:"https://example.com/items/1"
-
~url:"https://example.com/items/1"
-
~title:"Test Item"
-
~content:(`Both ("<p>HTML content</p>", "Text content"))
-
~summary:"A test item"
-
~image:"https://example.com/image.jpg"
-
~banner_image:"https://example.com/banner.jpg"
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get)
-
~date_modified:(Jsonfeed.Rfc3339.parse "2024-11-01T15:00:00Z" |> Option.get)
-
~authors:[author]
-
~tags:["test"; "example"]
-
~language:"en"
-
~attachments:[attachment]
-
() in
-
-
let hub = Hub.create
-
~type_:"WebSub"
-
~url:"https://pubsubhubbub.appspot.com/"
-
() in
-
-
let feed = Jsonfeed.create
-
~title:"Complete Feed"
-
~home_page_url:"https://example.com"
-
~feed_url:"https://example.com/feed.json"
-
~description:"A complete test feed"
-
~user_comment:"This is a test feed"
-
~icon:"https://example.com/icon.png"
-
~favicon:"https://example.com/favicon.ico"
-
~authors:[author]
-
~language:"en-US"
-
~hubs:[hub]
-
~items:[item]
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () -> Format.printf "✓ Complete feed is valid\n\n"
-
| Error errors ->
-
Format.printf "✗ Complete feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_feed_with_multiple_items () =
-
Format.printf "=== Test: Feed with Multiple Items ===\n";
-
-
let items = List.init 10 (fun i ->
-
Item.create
-
~id:(Printf.sprintf "https://example.com/items/%d" i)
-
~content:(`Text (Printf.sprintf "Item %d content" i))
-
~title:(Printf.sprintf "Item %d" i)
-
~date_published:(Jsonfeed.Rfc3339.parse
-
(Printf.sprintf "2024-11-%02dT10:00:00Z" (i + 1)) |> Option.get)
-
()
-
) in
-
-
let feed = Jsonfeed.create
-
~title:"Multi-item Feed"
-
~items
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () ->
-
Format.printf "✓ Feed with %d items is valid\n\n" (List.length items)
-
| Error errors ->
-
Format.printf "✗ Multi-item feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_podcast_feed () =
-
Format.printf "=== Test: Podcast Feed ===\n";
-
-
let host = Author.create
-
~name:"Podcast Host"
-
~url:"https://podcast.example.com/host"
-
() in
-
-
let episode1 = Attachment.create
-
~url:"https://podcast.example.com/ep1.mp3"
-
~mime_type:"audio/mpeg"
-
~title:"Episode 1"
-
~size_in_bytes:20_971_520L (* 20 MB *)
-
~duration_in_seconds:1800 (* 30 minutes *)
-
() in
-
-
(* Alternate format of the same episode *)
-
let episode1_aac = Attachment.create
-
~url:"https://podcast.example.com/ep1.aac"
-
~mime_type:"audio/aac"
-
~title:"Episode 1"
-
~size_in_bytes:16_777_216L
-
~duration_in_seconds:1800
-
() in
-
-
let item = 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 the first episode!</p>")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T12:00:00Z" |> Option.get)
-
~authors:[host]
-
~attachments:[episode1; episode1_aac]
-
~image:"https://podcast.example.com/ep1-cover.jpg"
-
() in
-
-
let feed = Jsonfeed.create
-
~title:"Example Podcast"
-
~home_page_url:"https://podcast.example.com"
-
~feed_url:"https://podcast.example.com/feed.json"
-
~authors:[host]
-
~items:[item]
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () -> Format.printf "✓ Podcast feed is valid\n\n"
-
| Error errors ->
-
Format.printf "✗ Podcast feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_microblog_feed () =
-
Format.printf "=== Test: Microblog Feed (no titles) ===\n";
-
-
let author = Author.create
-
~name:"Microblogger"
-
~url:"https://micro.example.com"
-
() in
-
-
let items = [
-
Item.create
-
~id:"https://micro.example.com/1"
-
~content:(`Text "Just posted a new photo!")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T08:00:00Z" |> Option.get)
-
();
-
Item.create
-
~id:"https://micro.example.com/2"
-
~content:(`Text "Having a great day! ☀️")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T12:30:00Z" |> Option.get)
-
();
-
Item.create
-
~id:"https://micro.example.com/3"
-
~content:(`Html "<p>Check out this <a href=\"#\">link</a></p>")
-
~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T16:45:00Z" |> Option.get)
-
()
-
] in
-
-
let feed = Jsonfeed.create
-
~title:"Microblog"
-
~home_page_url:"https://micro.example.com"
-
~authors:[author]
-
~items
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () ->
-
Format.printf "✓ Microblog feed with %d items is valid\n\n"
-
(List.length items)
-
| Error errors ->
-
Format.printf "✗ Microblog feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_expired_feed () =
-
Format.printf "=== Test: Expired Feed ===\n";
-
-
let feed = Jsonfeed.create
-
~title:"Archived Blog"
-
~home_page_url:"https://archive.example.com"
-
~description:"This blog is no longer updated"
-
~expired:true
-
~items:[]
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () -> Format.printf "✓ Expired feed is valid\n\n"
-
| Error errors ->
-
Format.printf "✗ Expired feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_paginated_feed () =
-
Format.printf "=== Test: Paginated Feed ===\n";
-
-
let items = List.init 25 (fun i ->
-
Item.create
-
~id:(Printf.sprintf "https://example.com/items/%d" i)
-
~content:(`Text (Printf.sprintf "Item %d" i))
-
()
-
) in
-
-
let feed = Jsonfeed.create
-
~title:"Large Feed"
-
~home_page_url:"https://example.com"
-
~feed_url:"https://example.com/feed.json?page=1"
-
~next_url:"https://example.com/feed.json?page=2"
-
~items
-
() in
-
-
match Jsonfeed.validate feed with
-
| Ok () ->
-
Format.printf "✓ Paginated feed is valid (page 1 with next_url)\n\n"
-
| Error errors ->
-
Format.printf "✗ Paginated feed validation failed:\n";
-
List.iter (Format.printf " - %s\n") errors;
-
Format.printf "\n"
-
-
let test_invalid_feed_from_json () =
-
Format.printf "=== Test: Parsing Invalid JSON ===\n";
-
-
(* Missing required version field *)
-
let invalid_json1 = {|{
-
"title": "Test",
-
"items": []
-
}|} in
-
-
(match Jsonfeed.of_string invalid_json1 with
-
| Ok _ -> Format.printf "✗ Should have failed (missing version)\n"
-
| Error err ->
-
Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err));
-
-
(* Missing required title field *)
-
let invalid_json2 = {|{
-
"version": "https://jsonfeed.org/version/1.1",
-
"items": []
-
}|} in
-
-
(match Jsonfeed.of_string invalid_json2 with
-
| Ok _ -> Format.printf "✗ Should have failed (missing title)\n"
-
| Error err ->
-
Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err));
-
-
(* Item without id *)
-
let invalid_json3 = {|{
-
"version": "https://jsonfeed.org/version/1.1",
-
"title": "Test",
-
"items": [{
-
"content_text": "Hello"
-
}]
-
}|} in
-
-
(match Jsonfeed.of_string invalid_json3 with
-
| Ok _ -> Format.printf "✗ Should have failed (item without id)\n"
-
| Error err ->
-
Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err));
-
-
Format.printf "\n"
-
-
let main () =
-
Format.printf "\n=== JSON Feed Validation Tests ===\n\n";
-
-
test_valid_minimal_feed ();
-
test_valid_complete_feed ();
-
test_feed_with_multiple_items ();
-
test_podcast_feed ();
-
test_microblog_feed ();
-
test_expired_feed ();
-
test_paginated_feed ();
-
test_invalid_feed_from_json ();
-
-
Format.printf "=== All Tests Complete ===\n"
-
-
let () = main ()
+
let () = validate_stdin ()