OCaml library for JSONfeed parsing and creation

add echo examples

+75 -297
+5
example/dune
··· 12 12 (name feed_validator) 13 13 (modules feed_validator) 14 14 (libraries jsonfeed)) 15 + 16 + (executable 17 + (name feed_echo) 18 + (modules feed_echo) 19 + (libraries jsonfeed))
+38
example/feed_echo.ml
··· 1 + (** Example: JSON Feed Echo 2 + 3 + Reads a JSON Feed from stdin, parses it, and outputs it to stdout. 4 + Useful for testing round-trip parsing and identifying any changes 5 + during serialization/deserialization. 6 + 7 + Usage: 8 + feed_echo < feed.json 9 + cat feed.json | feed_echo > output.json 10 + diff <(cat feed.json | feed_echo) feed.json 11 + 12 + Exit codes: 13 + 0 - Success 14 + 1 - Parsing or encoding failed *) 15 + 16 + let echo_feed () = 17 + (* Create a bytesrw reader from stdin *) 18 + let stdin = Bytesrw.Bytes.Reader.of_in_channel In_channel.stdin in 19 + 20 + (* Parse the JSON feed *) 21 + match Jsonfeed.decode ~locs:true stdin with 22 + | Error err -> 23 + Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err); 24 + exit 1 25 + 26 + | Ok feed -> 27 + (* Encode the feed back to stdout *) 28 + match Jsonfeed.to_string ~minify:false feed with 29 + | Error err -> 30 + Format.eprintf "Encoding failed:\n %s\n%!" (Jsont.Error.to_string err); 31 + exit 1 32 + 33 + | Ok json -> 34 + print_string json; 35 + print_newline (); 36 + exit 0 37 + 38 + let () = echo_feed ()
+32 -297
example/feed_validator.ml
··· 1 - (** Example: Validating JSON Feeds 1 + (** Example: JSON Feed Validator 2 2 3 - This demonstrates: 4 - - Validating feed structure 5 - - Testing various edge cases 6 - - Handling invalid feeds 7 - - Best practices for feed construction *) 3 + Reads a JSON Feed from stdin and validates it. 8 4 9 - open Jsonfeed 5 + Usage: 6 + feed_validator < feed.json 7 + cat feed.json | feed_validator 10 8 11 - let test_valid_minimal_feed () = 12 - Format.printf "=== Test: Minimal Valid Feed ===\n"; 9 + Exit codes: 10 + 0 - Feed is valid 11 + 1 - Feed parsing failed 12 + 2 - Feed validation failed *) 13 13 14 - let feed = Jsonfeed.create 15 - ~title:"Minimal Feed" 16 - ~items:[] 17 - () in 14 + let validate_stdin () = 15 + let stdin = Bytesrw.Bytes.Reader.of_in_channel In_channel.stdin in 16 + match Jsonfeed.decode ~locs:true stdin with 17 + | Error err -> 18 + Format.eprintf "Parsing failed:\n %s\n%!" (Jsont.Error.to_string err); 19 + exit 1 20 + | Ok feed -> 21 + match Jsonfeed.validate feed with 22 + | Ok () -> 23 + Format.printf "Feed is valid\n%!"; 24 + Format.printf "\nFeed details:\n"; 25 + Format.printf " Title: %s\n" (Jsonfeed.title feed); 26 + Format.printf " Version: %s\n" (Jsonfeed.version feed); 27 + (match Jsonfeed.home_page_url feed with 28 + | Some url -> Format.printf " Home page: %s\n" url 29 + | None -> ()); 30 + Format.printf " Items: %d\n" (List.length (Jsonfeed.items feed)); 31 + exit 0 18 32 19 - match Jsonfeed.validate feed with 20 - | Ok () -> Format.printf "✓ Minimal feed is valid\n\n" 21 - | Error errors -> 22 - Format.printf "✗ Minimal feed validation failed:\n"; 23 - List.iter (Format.printf " - %s\n") errors; 24 - Format.printf "\n" 33 + | Error errors -> 34 + Format.eprintf "Validation failed:\n%!"; 35 + List.iter (fun err -> Format.eprintf " - %s\n%!" err) errors; 36 + exit 2 25 37 26 - let test_valid_complete_feed () = 27 - Format.printf "=== Test: Complete Valid Feed ===\n"; 28 - 29 - let author = Author.create 30 - ~name:"Test Author" 31 - ~url:"https://example.com/author" 32 - ~avatar:"https://example.com/avatar.png" 33 - () in 34 - 35 - let attachment = Attachment.create 36 - ~url:"https://example.com/file.mp3" 37 - ~mime_type:"audio/mpeg" 38 - ~title:"Audio File" 39 - ~size_in_bytes:1024L 40 - ~duration_in_seconds:60 41 - () in 42 - 43 - let item = Item.create 44 - ~id:"https://example.com/items/1" 45 - ~url:"https://example.com/items/1" 46 - ~title:"Test Item" 47 - ~content:(`Both ("<p>HTML content</p>", "Text content")) 48 - ~summary:"A test item" 49 - ~image:"https://example.com/image.jpg" 50 - ~banner_image:"https://example.com/banner.jpg" 51 - ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T10:00:00Z" |> Option.get) 52 - ~date_modified:(Jsonfeed.Rfc3339.parse "2024-11-01T15:00:00Z" |> Option.get) 53 - ~authors:[author] 54 - ~tags:["test"; "example"] 55 - ~language:"en" 56 - ~attachments:[attachment] 57 - () in 58 - 59 - let hub = Hub.create 60 - ~type_:"WebSub" 61 - ~url:"https://pubsubhubbub.appspot.com/" 62 - () in 63 - 64 - let feed = Jsonfeed.create 65 - ~title:"Complete Feed" 66 - ~home_page_url:"https://example.com" 67 - ~feed_url:"https://example.com/feed.json" 68 - ~description:"A complete test feed" 69 - ~user_comment:"This is a test feed" 70 - ~icon:"https://example.com/icon.png" 71 - ~favicon:"https://example.com/favicon.ico" 72 - ~authors:[author] 73 - ~language:"en-US" 74 - ~hubs:[hub] 75 - ~items:[item] 76 - () in 77 - 78 - match Jsonfeed.validate feed with 79 - | Ok () -> Format.printf "✓ Complete feed is valid\n\n" 80 - | Error errors -> 81 - Format.printf "✗ Complete feed validation failed:\n"; 82 - List.iter (Format.printf " - %s\n") errors; 83 - Format.printf "\n" 84 - 85 - let test_feed_with_multiple_items () = 86 - Format.printf "=== Test: Feed with Multiple Items ===\n"; 87 - 88 - let items = List.init 10 (fun i -> 89 - Item.create 90 - ~id:(Printf.sprintf "https://example.com/items/%d" i) 91 - ~content:(`Text (Printf.sprintf "Item %d content" i)) 92 - ~title:(Printf.sprintf "Item %d" i) 93 - ~date_published:(Jsonfeed.Rfc3339.parse 94 - (Printf.sprintf "2024-11-%02dT10:00:00Z" (i + 1)) |> Option.get) 95 - () 96 - ) in 97 - 98 - let feed = Jsonfeed.create 99 - ~title:"Multi-item Feed" 100 - ~items 101 - () in 102 - 103 - match Jsonfeed.validate feed with 104 - | Ok () -> 105 - Format.printf "✓ Feed with %d items is valid\n\n" (List.length items) 106 - | Error errors -> 107 - Format.printf "✗ Multi-item feed validation failed:\n"; 108 - List.iter (Format.printf " - %s\n") errors; 109 - Format.printf "\n" 110 - 111 - let test_podcast_feed () = 112 - Format.printf "=== Test: Podcast Feed ===\n"; 113 - 114 - let host = Author.create 115 - ~name:"Podcast Host" 116 - ~url:"https://podcast.example.com/host" 117 - () in 118 - 119 - let episode1 = Attachment.create 120 - ~url:"https://podcast.example.com/ep1.mp3" 121 - ~mime_type:"audio/mpeg" 122 - ~title:"Episode 1" 123 - ~size_in_bytes:20_971_520L (* 20 MB *) 124 - ~duration_in_seconds:1800 (* 30 minutes *) 125 - () in 126 - 127 - (* Alternate format of the same episode *) 128 - let episode1_aac = Attachment.create 129 - ~url:"https://podcast.example.com/ep1.aac" 130 - ~mime_type:"audio/aac" 131 - ~title:"Episode 1" 132 - ~size_in_bytes:16_777_216L 133 - ~duration_in_seconds:1800 134 - () in 135 - 136 - let item = Item.create 137 - ~id:"https://podcast.example.com/episodes/1" 138 - ~url:"https://podcast.example.com/episodes/1" 139 - ~title:"Episode 1: Introduction" 140 - ~content:(`Html "<p>Welcome to the first episode!</p>") 141 - ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T12:00:00Z" |> Option.get) 142 - ~authors:[host] 143 - ~attachments:[episode1; episode1_aac] 144 - ~image:"https://podcast.example.com/ep1-cover.jpg" 145 - () in 146 - 147 - let feed = Jsonfeed.create 148 - ~title:"Example Podcast" 149 - ~home_page_url:"https://podcast.example.com" 150 - ~feed_url:"https://podcast.example.com/feed.json" 151 - ~authors:[host] 152 - ~items:[item] 153 - () in 154 - 155 - match Jsonfeed.validate feed with 156 - | Ok () -> Format.printf "✓ Podcast feed is valid\n\n" 157 - | Error errors -> 158 - Format.printf "✗ Podcast feed validation failed:\n"; 159 - List.iter (Format.printf " - %s\n") errors; 160 - Format.printf "\n" 161 - 162 - let test_microblog_feed () = 163 - Format.printf "=== Test: Microblog Feed (no titles) ===\n"; 164 - 165 - let author = Author.create 166 - ~name:"Microblogger" 167 - ~url:"https://micro.example.com" 168 - () in 169 - 170 - let items = [ 171 - Item.create 172 - ~id:"https://micro.example.com/1" 173 - ~content:(`Text "Just posted a new photo!") 174 - ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T08:00:00Z" |> Option.get) 175 - (); 176 - Item.create 177 - ~id:"https://micro.example.com/2" 178 - ~content:(`Text "Having a great day! ☀️") 179 - ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T12:30:00Z" |> Option.get) 180 - (); 181 - Item.create 182 - ~id:"https://micro.example.com/3" 183 - ~content:(`Html "<p>Check out this <a href=\"#\">link</a></p>") 184 - ~date_published:(Jsonfeed.Rfc3339.parse "2024-11-01T16:45:00Z" |> Option.get) 185 - () 186 - ] in 187 - 188 - let feed = Jsonfeed.create 189 - ~title:"Microblog" 190 - ~home_page_url:"https://micro.example.com" 191 - ~authors:[author] 192 - ~items 193 - () in 194 - 195 - match Jsonfeed.validate feed with 196 - | Ok () -> 197 - Format.printf "✓ Microblog feed with %d items is valid\n\n" 198 - (List.length items) 199 - | Error errors -> 200 - Format.printf "✗ Microblog feed validation failed:\n"; 201 - List.iter (Format.printf " - %s\n") errors; 202 - Format.printf "\n" 203 - 204 - let test_expired_feed () = 205 - Format.printf "=== Test: Expired Feed ===\n"; 206 - 207 - let feed = Jsonfeed.create 208 - ~title:"Archived Blog" 209 - ~home_page_url:"https://archive.example.com" 210 - ~description:"This blog is no longer updated" 211 - ~expired:true 212 - ~items:[] 213 - () in 214 - 215 - match Jsonfeed.validate feed with 216 - | Ok () -> Format.printf "✓ Expired feed is valid\n\n" 217 - | Error errors -> 218 - Format.printf "✗ Expired feed validation failed:\n"; 219 - List.iter (Format.printf " - %s\n") errors; 220 - Format.printf "\n" 221 - 222 - let test_paginated_feed () = 223 - Format.printf "=== Test: Paginated Feed ===\n"; 224 - 225 - let items = List.init 25 (fun i -> 226 - Item.create 227 - ~id:(Printf.sprintf "https://example.com/items/%d" i) 228 - ~content:(`Text (Printf.sprintf "Item %d" i)) 229 - () 230 - ) in 231 - 232 - let feed = Jsonfeed.create 233 - ~title:"Large Feed" 234 - ~home_page_url:"https://example.com" 235 - ~feed_url:"https://example.com/feed.json?page=1" 236 - ~next_url:"https://example.com/feed.json?page=2" 237 - ~items 238 - () in 239 - 240 - match Jsonfeed.validate feed with 241 - | Ok () -> 242 - Format.printf "✓ Paginated feed is valid (page 1 with next_url)\n\n" 243 - | Error errors -> 244 - Format.printf "✗ Paginated feed validation failed:\n"; 245 - List.iter (Format.printf " - %s\n") errors; 246 - Format.printf "\n" 247 - 248 - let test_invalid_feed_from_json () = 249 - Format.printf "=== Test: Parsing Invalid JSON ===\n"; 250 - 251 - (* Missing required version field *) 252 - let invalid_json1 = {|{ 253 - "title": "Test", 254 - "items": [] 255 - }|} in 256 - 257 - (match Jsonfeed.of_string invalid_json1 with 258 - | Ok _ -> Format.printf "✗ Should have failed (missing version)\n" 259 - | Error err -> 260 - Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err)); 261 - 262 - (* Missing required title field *) 263 - let invalid_json2 = {|{ 264 - "version": "https://jsonfeed.org/version/1.1", 265 - "items": [] 266 - }|} in 267 - 268 - (match Jsonfeed.of_string invalid_json2 with 269 - | Ok _ -> Format.printf "✗ Should have failed (missing title)\n" 270 - | Error err -> 271 - Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err)); 272 - 273 - (* Item without id *) 274 - let invalid_json3 = {|{ 275 - "version": "https://jsonfeed.org/version/1.1", 276 - "title": "Test", 277 - "items": [{ 278 - "content_text": "Hello" 279 - }] 280 - }|} in 281 - 282 - (match Jsonfeed.of_string invalid_json3 with 283 - | Ok _ -> Format.printf "✗ Should have failed (item without id)\n" 284 - | Error err -> 285 - Format.printf "✓ Correctly rejected invalid feed: %s\n" (Jsont.Error.to_string err)); 286 - 287 - Format.printf "\n" 288 - 289 - let main () = 290 - Format.printf "\n=== JSON Feed Validation Tests ===\n\n"; 291 - 292 - test_valid_minimal_feed (); 293 - test_valid_complete_feed (); 294 - test_feed_with_multiple_items (); 295 - test_podcast_feed (); 296 - test_microblog_feed (); 297 - test_expired_feed (); 298 - test_paginated_feed (); 299 - test_invalid_feed_from_json (); 300 - 301 - Format.printf "=== All Tests Complete ===\n" 302 - 303 - let () = main () 38 + let () = validate_stdin ()