OCaml library for JSONfeed parsing and creation
1(** Example: Validating JSON Feeds 2 3 This demonstrates: 4 - Validating feed structure 5 - Testing various edge cases 6 - Handling invalid feeds 7 - Best practices for feed construction *) 8 9open Jsonfeed 10 11let test_valid_minimal_feed () = 12 Format.printf "=== Test: Minimal Valid Feed ===\n"; 13 14 let feed = Jsonfeed.create 15 ~title:"Minimal Feed" 16 ~items:[] 17 () in 18 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" 25 26let 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.parse_rfc3339 "2024-11-01T10:00:00Z" |> Option.get) 52 ~date_modified:(Jsonfeed.parse_rfc3339 "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 85let 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.parse_rfc3339 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 111let 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.parse_rfc3339 "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 162let 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.parse_rfc3339 "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.parse_rfc3339 "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.parse_rfc3339 "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 204let 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 222let 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 248let 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 (`Msg err) -> 260 Format.printf "✓ Correctly rejected invalid feed: %s\n" 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 (`Msg err) -> 271 Format.printf "✓ Correctly rejected invalid feed: %s\n" 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 (`Msg err) -> 285 Format.printf "✓ Correctly rejected invalid feed: %s\n" err); 286 287 Format.printf "\n" 288 289let 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 303let () = main ()