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 = Jsont.json
16
17 let empty = Jsont.Object ([], Jsont.Meta.none)
18 let is_empty = function Jsont.Object ([], _) -> 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 create ~title ?home_page_url ?feed_url ?description ?user_comment ?next_url
40 ?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 version t = t.version
61let title t = t.title
62let home_page_url t = t.home_page_url
63let feed_url t = t.feed_url
64let description t = t.description
65let user_comment t = t.user_comment
66let next_url t = t.next_url
67let icon t = t.icon
68let favicon t = t.favicon
69let authors t = t.authors
70let language t = t.language
71let expired t = t.expired
72let hubs t = t.hubs
73let items t = t.items
74let unknown t = t.unknown
75let equal a b = a.title = b.title && a.items = b.items
76
77let pp ppf t =
78 Format.fprintf ppf "Feed: %s (%d items)" t.title (List.length t.items)
79
80let pp_summary ppf t =
81 Format.fprintf ppf "%s (%d items)" t.title (List.length t.items)
82
83(* Jsont type *)
84
85let jsont =
86 let kind = "JSON Feed" in
87 let doc = "A JSON Feed document" in
88
89 (* Helper constructor that sets version automatically *)
90 let make_from_json _version title home_page_url feed_url description
91 user_comment next_url icon favicon authors language expired hubs items
92 unknown =
93 {
94 version = "https://jsonfeed.org/version/1.1";
95 title;
96 home_page_url;
97 feed_url;
98 description;
99 user_comment;
100 next_url;
101 icon;
102 favicon;
103 authors;
104 language;
105 expired;
106 hubs;
107 items;
108 unknown;
109 }
110 in
111
112 Jsont.Object.map ~kind ~doc make_from_json
113 |> Jsont.Object.mem "version" Jsont.string ~enc:version
114 |> Jsont.Object.mem "title" Jsont.string ~enc:title
115 |> Jsont.Object.opt_mem "home_page_url" Jsont.string ~enc:home_page_url
116 |> Jsont.Object.opt_mem "feed_url" Jsont.string ~enc:feed_url
117 |> Jsont.Object.opt_mem "description" Jsont.string ~enc:description
118 |> Jsont.Object.opt_mem "user_comment" Jsont.string ~enc:user_comment
119 |> Jsont.Object.opt_mem "next_url" Jsont.string ~enc:next_url
120 |> Jsont.Object.opt_mem "icon" Jsont.string ~enc:icon
121 |> Jsont.Object.opt_mem "favicon" Jsont.string ~enc:favicon
122 |> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:authors
123 |> Jsont.Object.opt_mem "language" Jsont.string ~enc:language
124 |> Jsont.Object.opt_mem "expired" Jsont.bool ~enc:expired
125 |> Jsont.Object.opt_mem "hubs" (Jsont.list Hub.jsont) ~enc:hubs
126 |> Jsont.Object.mem "items" (Jsont.list Item.jsont) ~enc:items
127 |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
128 |> Jsont.Object.finish
129
130(* Encoding and Decoding *)
131
132let decode ?layout ?locs ?file r =
133 Jsont_bytesrw.decode' ?layout ?locs ?file jsont r
134
135let decode_string ?layout ?locs ?file s =
136 Jsont_bytesrw.decode_string' ?layout ?locs ?file jsont s
137
138let encode ?format ?number_format feed ~eod w =
139 Jsont_bytesrw.encode' ?format ?number_format jsont feed ~eod w
140
141let encode_string ?format ?number_format feed =
142 Jsont_bytesrw.encode_string' ?format ?number_format jsont feed
143
144let of_string s = decode_string s
145
146let to_string ?(minify = false) feed =
147 let format = if minify then Jsont.Minify else Jsont.Indent in
148 encode_string ~format feed
149
150(* Validation *)
151
152let validate feed =
153 let errors = ref [] in
154 let add_error msg = errors := msg :: !errors in
155
156 (* Check required fields *)
157 if feed.title = "" then add_error "title is required and cannot be empty";
158
159 (* Check items have unique IDs *)
160 let ids = List.map Item.id feed.items in
161 let unique_ids = List.sort_uniq String.compare ids in
162 if List.length ids <> List.length unique_ids then
163 add_error "items must have unique IDs";
164
165 (* Validate authors *)
166 (match feed.authors with
167 | Some authors ->
168 List.iteri
169 (fun i author ->
170 if not (Author.is_valid author) then
171 add_error
172 (Printf.sprintf
173 "feed author %d is invalid (needs at least one field)" i))
174 authors
175 | None -> ());
176
177 (* Validate items *)
178 List.iteri
179 (fun i item ->
180 if Item.id item = "" then
181 add_error (Printf.sprintf "item %d has empty ID" i);
182
183 (* Validate item authors *)
184 match Item.authors item with
185 | Some authors ->
186 List.iteri
187 (fun j author ->
188 if not (Author.is_valid author) then
189 add_error (Printf.sprintf "item %d author %d is invalid" i j))
190 authors
191 | None -> ())
192 feed.items;
193
194 match !errors with [] -> Ok () | errs -> Error (List.rev errs)