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