OCaml library for JSONfeed parsing and creation
1(*---------------------------------------------------------------------------
2 Copyright (c) 2024 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6module Rfc3339 = Rfc3339
7module Cito = Cito
8module Author = Author
9module Attachment = Attachment
10module Hub = Hub
11module Reference = Reference
12module Item = Item
13
14module Unknown = struct
15 type t = (string * Jsont.json) list
16
17 let empty = []
18 let is_empty = function [] -> true | _ -> false
19end
20
21type t = {
22 version : string;
23 title : string;
24 home_page_url : string option;
25 feed_url : string option;
26 description : string option;
27 user_comment : string option;
28 next_url : string option;
29 icon : string option;
30 favicon : string option;
31 authors : Author.t list option;
32 language : string option;
33 expired : bool option;
34 hubs : Hub.t list option;
35 items : Item.t list;
36 unknown : Unknown.t;
37}
38
39let make ~title ?home_page_url ?feed_url ?description ?user_comment
40 ?next_url ?icon ?favicon ?authors ?language ?expired ?hubs ~items
41 ?(unknown = Unknown.empty) () =
42 {
43 version = "https://jsonfeed.org/version/1.1";
44 title;
45 home_page_url;
46 feed_url;
47 description;
48 user_comment;
49 next_url;
50 icon;
51 favicon;
52 authors;
53 language;
54 expired;
55 hubs;
56 items;
57 unknown;
58 }
59
60let create ~title ?home_page_url ?feed_url ?description ?user_comment
61 ?next_url ?icon ?favicon ?authors ?language ?expired ?hubs ~items () =
62 make ~title ?home_page_url ?feed_url ?description ?user_comment
63 ?next_url ?icon ?favicon ?authors ?language ?expired ?hubs ~items ()
64
65let version t = t.version
66let title t = t.title
67let home_page_url t = t.home_page_url
68let feed_url t = t.feed_url
69let description t = t.description
70let user_comment t = t.user_comment
71let next_url t = t.next_url
72let icon t = t.icon
73let favicon t = t.favicon
74let authors t = t.authors
75let language t = t.language
76let expired t = t.expired
77let hubs t = t.hubs
78let items t = t.items
79let unknown t = t.unknown
80
81let equal a b =
82 a.title = b.title &&
83 a.items = b.items
84
85let pp ppf t =
86 Format.fprintf ppf "Feed: %s (%d items)" t.title (List.length t.items)
87
88let pp_summary ppf t =
89 Format.fprintf ppf "%s (%d items)" t.title (List.length t.items)
90
91(* Jsont type *)
92
93let jsont =
94 let kind = "JSON Feed" in
95 let doc = "A JSON Feed document" in
96 let unknown_mems : (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
97 let open Jsont.Object.Mems in
98 let dec_empty () = [] in
99 let dec_add _meta (name : string) value acc =
100 ((name, Jsont.Meta.none), value) :: acc
101 in
102 let dec_finish _meta mems =
103 List.rev_map (fun ((name, _meta), value) -> (name, value)) mems in
104 let enc = {
105 enc = fun (type acc) (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) unknown (acc : acc) ->
106 List.fold_left (fun acc (name, value) ->
107
108 f Jsont.Meta.none name value acc
109 ) acc unknown
110 } in
111 map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
112 in
113
114 (* Helper constructor that sets version automatically *)
115 let make_from_json _version title home_page_url feed_url description user_comment
116 next_url icon favicon authors language expired hubs items unknown =
117 {
118 version = "https://jsonfeed.org/version/1.1";
119 title;
120 home_page_url;
121 feed_url;
122 description;
123 user_comment;
124 next_url;
125 icon;
126 favicon;
127 authors;
128 language;
129 expired;
130 hubs;
131 items;
132 unknown;
133 }
134 in
135
136 Jsont.Object.map ~kind ~doc make_from_json
137 |> Jsont.Object.mem "version" Jsont.string ~enc:version
138 |> Jsont.Object.mem "title" Jsont.string ~enc:title
139 |> Jsont.Object.opt_mem "home_page_url" Jsont.string ~enc:home_page_url
140 |> Jsont.Object.opt_mem "feed_url" Jsont.string ~enc:feed_url
141 |> Jsont.Object.opt_mem "description" Jsont.string ~enc:description
142 |> Jsont.Object.opt_mem "user_comment" Jsont.string ~enc:user_comment
143 |> Jsont.Object.opt_mem "next_url" Jsont.string ~enc:next_url
144 |> Jsont.Object.opt_mem "icon" Jsont.string ~enc:icon
145 |> Jsont.Object.opt_mem "favicon" Jsont.string ~enc:favicon
146 |> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:authors
147 |> Jsont.Object.opt_mem "language" Jsont.string ~enc:language
148 |> Jsont.Object.opt_mem "expired" Jsont.bool ~enc:expired
149 |> Jsont.Object.opt_mem "hubs" (Jsont.list Hub.jsont) ~enc:hubs
150 |> Jsont.Object.mem "items" (Jsont.list Item.jsont) ~enc:items
151 |> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
152 |> Jsont.Object.finish
153
154(* Encoding and Decoding *)
155
156let decode ?layout ?locs ?file r =
157 Jsont_bytesrw.decode' ?layout ?locs ?file jsont r
158
159let decode_string ?layout ?locs ?file s =
160 Jsont_bytesrw.decode_string' ?layout ?locs ?file jsont s
161
162let encode ?format ?number_format feed ~eod w =
163 Jsont_bytesrw.encode' ?format ?number_format jsont feed ~eod w
164
165let encode_string ?format ?number_format feed =
166 Jsont_bytesrw.encode_string' ?format ?number_format jsont feed
167
168let of_string s =
169 decode_string s
170
171let to_string ?(minify=false) feed =
172 let format = if minify then Jsont.Minify else Jsont.Indent in
173 encode_string ~format feed
174
175(* Validation *)
176
177let validate feed =
178 let errors = ref [] in
179 let add_error msg = errors := msg :: !errors in
180
181 (* Check required fields *)
182 if feed.title = "" then
183 add_error "title is required and cannot be empty";
184
185 (* Check items have unique IDs *)
186 let ids = List.map Item.id feed.items in
187 let unique_ids = List.sort_uniq String.compare ids in
188 if List.length ids <> List.length unique_ids then
189 add_error "items must have unique IDs";
190
191 (* Validate authors *)
192 (match feed.authors with
193 | Some authors ->
194 List.iteri (fun i author ->
195 if not (Author.is_valid author) then
196 add_error (Printf.sprintf "feed author %d is invalid (needs at least one field)" i)
197 ) authors
198 | None -> ());
199
200 (* Validate items *)
201 List.iteri (fun i item ->
202 if Item.id item = "" then
203 add_error (Printf.sprintf "item %d has empty ID" i);
204
205 (* Validate item authors *)
206 (match Item.authors item with
207 | Some authors ->
208 List.iteri (fun j author ->
209 if not (Author.is_valid author) then
210 add_error (Printf.sprintf "item %d author %d is invalid" i j)
211 ) authors
212 | None -> ())
213 ) feed.items;
214
215 match !errors with
216 | [] -> Ok ()
217 | errs -> Error (List.rev errs)