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 ()