OCaml library for JSONfeed parsing and creation
1(** JSON Feed format parser and serializer.
2
3 This library implements the JSON Feed specification version 1.1, providing
4 type-safe parsing and serialization of JSON Feed documents. JSON Feed is a
5 syndication format similar to RSS and Atom, but using JSON instead of XML.
6
7 {b Key Features:}
8 - Type-safe construction with compile-time validation
9 - Support for all JSON Feed 1.1 fields
10 - RFC 3339 date parsing with Ptime integration
11 - Streaming parsing and serialization with Jsonm
12 - Comprehensive documentation and examples
13
14 {b Quick Start:}
15 {[
16 (* Create a simple feed *)
17 let feed = Jsonfeed.create
18 ~title:"My Blog"
19 ~home_page_url:"https://example.com"
20 ~feed_url:"https://example.com/feed.json"
21 ~items:[
22 Item.create
23 ~id:"https://example.com/post/1"
24 ~content:(Item.Html "<p>Hello, world!</p>")
25 ~title:"First Post"
26 ()
27 ]
28 ()
29
30 (* Serialize to string *)
31 let json = Jsonfeed.to_string feed
32
33 (* Parse from string *)
34 match Jsonfeed.of_string json with
35 | Ok feed -> Printf.printf "Feed: %s\n" (Jsonfeed.title feed)
36 | Error (`Msg err) -> Printf.eprintf "Error: %s\n" err
37 ]}
38
39 @see <https://www.jsonfeed.org/version/1.1/> JSON Feed Specification *)
40
41
42(** The type representing a complete JSON Feed. *)
43type t
44
45(** Exception raised when attempting to parse an invalid feed. *)
46exception Invalid_feed of string
47
48(** {1 Construction} *)
49
50(** [create ~title ?home_page_url ?feed_url ?description ?user_comment ?next_url
51 ?icon ?favicon ?authors ?language ?expired ?hubs ~items ()]
52 creates a JSON Feed.
53
54 @param title The name of the feed (required)
55 @param home_page_url The URL of the resource the feed describes
56 @param feed_url The URL of the feed itself (serves as unique identifier)
57 @param description Additional information about the feed
58 @param user_comment A description of the feed's purpose for humans reading the raw JSON
59 @param next_url URL of the next page of items (for pagination)
60 @param icon The feed's icon URL (should be square, 512x512 or larger)
61 @param favicon The feed's favicon URL (should be square, 64x64 or larger)
62 @param authors The feed's default authors (inherited by items without authors)
63 @param language The primary language of the feed (RFC 5646 format, e.g. ["en-US"])
64 @param expired Whether the feed will update again ([true] means no more updates)
65 @param hubs Endpoints for real-time notifications
66 @param items The list of feed items (required)
67
68 {b Examples:}
69 {[
70 (* Minimal feed *)
71 let feed = Jsonfeed.create
72 ~title:"My Blog"
73 ~items:[] ()
74
75 (* Full-featured blog feed *)
76 let feed = Jsonfeed.create
77 ~title:"Example Blog"
78 ~home_page_url:"https://example.com"
79 ~feed_url:"https://example.com/feed.json"
80 ~description:"A blog about OCaml and functional programming"
81 ~icon:"https://example.com/icon.png"
82 ~authors:[
83 Author.create
84 ~name:"Jane Doe"
85 ~url:"https://example.com/about"
86 ()
87 ]
88 ~language:"en-US"
89 ~items:[
90 Item.create
91 ~id:"https://example.com/posts/1"
92 ~content:(Item.Html "<p>First post</p>")
93 ~title:"Hello World"
94 ();
95 Item.create
96 ~id:"https://example.com/posts/2"
97 ~content:(Item.Html "<p>Second post</p>")
98 ~title:"Another Post"
99 ()
100 ]
101 ()
102
103 (* Podcast feed with hubs *)
104 let hub = Hub.create
105 ~type_:"WebSub"
106 ~url:"https://pubsubhubbub.appspot.com/"
107 () in
108 let feed = Jsonfeed.create
109 ~title:"My Podcast"
110 ~home_page_url:"https://podcast.example.com"
111 ~feed_url:"https://podcast.example.com/feed.json"
112 ~hubs:[hub]
113 ~items:[
114 Item.create
115 ~id:"https://podcast.example.com/episodes/1"
116 ~content:(Item.Html "<p>Episode description</p>")
117 ~title:"Episode 1"
118 ~attachments:[
119 Attachment.create
120 ~url:"https://podcast.example.com/ep1.mp3"
121 ~mime_type:"audio/mpeg"
122 ~duration_in_seconds:1800
123 ()
124 ]
125 ()
126 ]
127 ()
128 ]} *)
129val create :
130 title:string ->
131 ?home_page_url:string ->
132 ?feed_url:string ->
133 ?description:string ->
134 ?user_comment:string ->
135 ?next_url:string ->
136 ?icon:string ->
137 ?favicon:string ->
138 ?authors:Author.t list ->
139 ?language:string ->
140 ?expired:bool ->
141 ?hubs:Hub.t list ->
142 items:Item.t list ->
143 unit ->
144 t
145
146
147(** {1 Accessors} *)
148
149(** [version t] returns the JSON Feed version URL.
150
151 This is always ["https://jsonfeed.org/version/1.1"] for feeds created
152 by this library, but may differ when parsing external feeds. *)
153val version : t -> string
154
155(** [title t] returns the feed's title. *)
156val title : t -> string
157
158(** [home_page_url t] returns the feed's home page URL, if set. *)
159val home_page_url : t -> string option
160
161(** [feed_url t] returns the feed's URL, if set. *)
162val feed_url : t -> string option
163
164(** [description t] returns the feed's description, if set. *)
165val description : t -> string option
166
167(** [user_comment t] returns the feed's user comment, if set. *)
168val user_comment : t -> string option
169
170(** [next_url t] returns the URL for the next page of items, if set. *)
171val next_url : t -> string option
172
173(** [icon t] returns the feed's icon URL, if set. *)
174val icon : t -> string option
175
176(** [favicon t] returns the feed's favicon URL, if set. *)
177val favicon : t -> string option
178
179(** [authors t] returns the feed's default authors, if set. *)
180val authors : t -> Author.t list option
181
182(** [language t] returns the feed's primary language, if set. *)
183val language : t -> string option
184
185(** [expired t] returns whether the feed will update again. *)
186val expired : t -> bool option
187
188(** [hubs t] returns the feed's hub endpoints, if set. *)
189val hubs : t -> Hub.t list option
190
191(** [items t] returns the feed's items. *)
192val items : t -> Item.t list
193
194
195(** {1 Parsing and Serialization} *)
196
197(** Error type for parsing operations. *)
198type error = [ `Msg of string ]
199
200(** [of_jsonm decoder] parses a JSON Feed from a Jsonm decoder.
201
202 This is the lowest-level parsing function, suitable for integration
203 with streaming JSON processing pipelines.
204
205 @param decoder A Jsonm decoder positioned at the start of a JSON Feed document
206 @return [Ok feed] on success, [Error (`Msg err)] on parse error
207
208 {b Example:}
209 {[
210 let decoder = Jsonm.decoder (`String json_string) in
211 match Jsonfeed.of_jsonm decoder with
212 | Ok feed -> (* process feed *)
213 | Error (`Msg err) -> (* handle error *)
214 ]} *)
215val of_jsonm : Jsonm.decoder -> (t, [> error]) result
216
217(** [to_jsonm encoder feed] serializes a JSON Feed to a Jsonm encoder.
218
219 This is the lowest-level serialization function, suitable for integration
220 with streaming JSON generation pipelines.
221
222 @param encoder A Jsonm encoder
223 @param feed The feed to serialize
224
225 {b Example:}
226 {[
227 let buffer = Buffer.create 1024 in
228 let encoder = Jsonm.encoder (`Buffer buffer) in
229 Jsonfeed.to_jsonm encoder feed;
230 let json = Buffer.contents buffer
231 ]} *)
232val to_jsonm : Jsonm.encoder -> t -> unit
233
234(** [of_string s] parses a JSON Feed from a string.
235
236 @param s A JSON string containing a JSON Feed document
237 @return [Ok feed] on success, [Error (`Msg err)] on parse error
238
239 {b Example:}
240 {[
241 let json = {|{
242 "version": "https://jsonfeed.org/version/1.1",
243 "title": "My Feed",
244 "items": []
245 }|} in
246 match Jsonfeed.of_string json with
247 | Ok feed -> Printf.printf "Parsed: %s\n" (Jsonfeed.title feed)
248 | Error (`Msg err) -> Printf.eprintf "Error: %s\n" err
249 ]} *)
250val of_string : string -> (t, [> error]) result
251
252(** [to_string ?minify feed] serializes a JSON Feed to a string.
253
254 @param minify If [true], produces compact JSON without whitespace.
255 If [false] (default), produces indented, human-readable JSON.
256 @param feed The feed to serialize
257 @return A JSON string
258
259 {b Example:}
260 {[
261 let json = Jsonfeed.to_string feed
262 let compact = Jsonfeed.to_string ~minify:true feed
263 ]} *)
264val to_string : ?minify:bool -> t -> string
265
266
267(** {1 Date Utilities} *)
268
269(** [parse_rfc3339 s] parses an RFC 3339 date/time string.
270
271 This function parses timestamps in the format required by JSON Feed,
272 such as ["2024-11-03T10:30:00Z"] or ["2024-11-03T10:30:00-08:00"].
273
274 @param s An RFC 3339 formatted date/time string
275 @return [Some time] on success, [None] if the string is invalid
276
277 {b Examples:}
278 {[
279 parse_rfc3339 "2024-11-03T10:30:00Z"
280 (* returns Some time *)
281
282 parse_rfc3339 "2024-11-03T10:30:00-08:00"
283 (* returns Some time *)
284
285 parse_rfc3339 "invalid"
286 (* returns None *)
287 ]} *)
288val parse_rfc3339 : string -> Ptime.t option
289
290(** [format_rfc3339 time] formats a timestamp as an RFC 3339 string.
291
292 The output uses UTC timezone (Z suffix) and includes fractional seconds
293 if the timestamp has sub-second precision.
294
295 @param time A Ptime timestamp
296 @return An RFC 3339 formatted string
297
298 {b Example:}
299 {[
300 let now = Ptime_clock.now () in
301 let s = format_rfc3339 now
302 (* returns "2024-11-03T10:30:45.123Z" or similar *)
303 ]} *)
304val format_rfc3339 : Ptime.t -> string
305
306
307(** {1 Validation} *)
308
309(** [validate feed] validates a JSON Feed.
310
311 Checks that:
312 - All required fields are present
313 - All items have unique IDs
314 - All items have valid content
315 - All URLs are well-formed (if possible)
316 - Authors have at least one field set
317
318 @param feed The feed to validate
319 @return [Ok ()] if valid, [Error errors] with a list of validation issues
320
321 {b Example:}
322 {[
323 match Jsonfeed.validate feed with
324 | Ok () -> (* feed is valid *)
325 | Error errors ->
326 List.iter (Printf.eprintf "Validation error: %s\n") errors
327 ]} *)
328val validate : t -> (unit, string list) result
329
330
331(** {1 Comparison} *)
332
333(** [equal a b] tests equality between two feeds.
334
335 Feeds are compared structurally, including all fields and items. *)
336val equal : t -> t -> bool
337
338
339(** {1 Pretty Printing} *)
340
341(** [pp ppf feed] pretty prints a feed to the formatter.
342
343 The output is human-readable and suitable for debugging. It shows
344 the feed's metadata and a summary of items.
345
346 {b Example output:}
347 {v
348 Feed: My Blog (https://example.com)
349 Items: 2
350 Authors: Jane Doe
351 Language: en-US
352 v} *)
353val pp : Format.formatter -> t -> unit
354
355(** [pp_summary ppf feed] prints a brief summary of the feed.
356
357 Shows only the title and item count.
358
359 {b Example output:}
360 {v My Blog (2 items) v} *)
361val pp_summary : Format.formatter -> t -> unit
362
363
364(** {1 Feed Content} *)
365
366(** Author information for feeds and items. *)
367module Author = Author
368
369(** Attachments for feed items (audio, video, downloads). *)
370module Attachment = Attachment
371
372(** Hub endpoints for real-time notifications. *)
373module Hub = Hub
374
375(** Feed items (posts, episodes, entries). *)
376module Item = Item