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 Unknown = struct
7 type t = (string * Jsont.json) list
8
9 let empty = []
10 let is_empty = function [] -> true | _ -> false
11end
12
13type content = [ `Html of string | `Text of string | `Both of string * string ]
14
15type t = {
16 id : string;
17 content : content;
18 url : string option;
19 external_url : string option;
20 title : string option;
21 summary : string option;
22 image : string option;
23 banner_image : string option;
24 date_published : Ptime.t option;
25 date_modified : Ptime.t option;
26 authors : Author.t list option;
27 tags : string list option;
28 language : string option;
29 attachments : Attachment.t list option;
30 references : Reference.t list option;
31 unknown : Unknown.t;
32}
33
34let create ~id ~content ?url ?external_url ?title ?summary ?image ?banner_image
35 ?date_published ?date_modified ?authors ?tags ?language ?attachments
36 ?references ?(unknown = Unknown.empty) () =
37 {
38 id;
39 content;
40 url;
41 external_url;
42 title;
43 summary;
44 image;
45 banner_image;
46 date_published;
47 date_modified;
48 authors;
49 tags;
50 language;
51 attachments;
52 references;
53 unknown;
54 }
55
56let id t = t.id
57let content t = t.content
58let url t = t.url
59let external_url t = t.external_url
60let title t = t.title
61let summary t = t.summary
62let image t = t.image
63let banner_image t = t.banner_image
64let date_published t = t.date_published
65let date_modified t = t.date_modified
66let authors t = t.authors
67let tags t = t.tags
68let language t = t.language
69let attachments t = t.attachments
70let references t = t.references
71let unknown t = t.unknown
72
73let content_html t =
74 match t.content with
75 | `Html html -> Some html
76 | `Text _ -> None
77 | `Both (html, _) -> Some html
78
79let content_text t =
80 match t.content with
81 | `Html _ -> None
82 | `Text text -> Some text
83 | `Both (_, text) -> Some text
84
85let equal a b = a.id = b.id
86
87let compare a b =
88 match (a.date_published, b.date_published) with
89 | None, None -> 0
90 | None, Some _ -> -1
91 | Some _, None -> 1
92 | Some da, Some db -> Ptime.compare da db
93
94let pp ppf t =
95 match (t.date_published, t.title) with
96 | Some date, Some title ->
97 let (y, m, d), _ = Ptime.to_date_time date in
98 Format.fprintf ppf "[%04d-%02d-%02d] %s (%s)" y m d title t.id
99 | Some date, None ->
100 let (y, m, d), _ = Ptime.to_date_time date in
101 Format.fprintf ppf "[%04d-%02d-%02d] %s" y m d t.id
102 | None, Some title -> Format.fprintf ppf "%s (%s)" title t.id
103 | None, None -> Format.fprintf ppf "%s" t.id
104
105let pp_summary ppf t =
106 match t.title with
107 | Some title -> Format.fprintf ppf "%s" title
108 | None -> Format.fprintf ppf "%s" t.id
109
110(* Jsont type *)
111
112let jsont =
113 let kind = "Item" in
114 let doc = "A JSON Feed item" in
115
116 (* Helper to construct item from JSON fields *)
117 let make_from_json id content_html content_text url external_url title summary
118 image banner_image date_published date_modified authors tags language
119 attachments references _extensions unknown =
120 (* Determine content from content_html and content_text *)
121 let content =
122 match (content_html, content_text) with
123 | Some html, Some text -> `Both (html, text)
124 | Some html, None -> `Html html
125 | None, Some text -> `Text text
126 | None, None ->
127 Jsont.Error.msg Jsont.Meta.none
128 "Item must have at least one of content_html or content_text"
129 in
130 {
131 id;
132 content;
133 url;
134 external_url;
135 title;
136 summary;
137 image;
138 banner_image;
139 date_published;
140 date_modified;
141 authors;
142 tags;
143 language;
144 attachments;
145 references;
146 unknown;
147 }
148 in
149
150 (* Encoders to extract fields from item *)
151 let enc_id t = t.id in
152 let enc_content_html t = content_html t in
153 let enc_content_text t = content_text t in
154 let enc_url t = t.url in
155 let enc_external_url t = t.external_url in
156 let enc_title t = t.title in
157 let enc_summary t = t.summary in
158 let enc_image t = t.image in
159 let enc_banner_image t = t.banner_image in
160 let enc_date_published t = t.date_published in
161 let enc_date_modified t = t.date_modified in
162 let enc_authors t = t.authors in
163 let enc_tags t = t.tags in
164 let enc_language t = t.language in
165 let enc_attachments t = t.attachments in
166 let enc_references t = t.references in
167 let enc_unknown t = t.unknown in
168
169 let unknown_mems :
170 (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
171 let open Jsont.Object.Mems in
172 let dec_empty () = [] in
173 let dec_add _meta (name : string) value acc =
174 ((name, Jsont.Meta.none), value) :: acc
175 in
176 let dec_finish _meta mems =
177 List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
178 in
179 let enc =
180 {
181 enc =
182 (fun (type acc)
183 (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
184 unknown
185 (acc : acc)
186 ->
187 List.fold_left
188 (fun acc (name, value) -> f Jsont.Meta.none name value acc)
189 acc unknown);
190 }
191 in
192 map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
193 in
194
195 Jsont.Object.map ~kind ~doc make_from_json
196 |> Jsont.Object.mem "id" Jsont.string ~enc:enc_id
197 |> Jsont.Object.opt_mem "content_html" Jsont.string ~enc:enc_content_html
198 |> Jsont.Object.opt_mem "content_text" Jsont.string ~enc:enc_content_text
199 |> Jsont.Object.opt_mem "url" Jsont.string ~enc:enc_url
200 |> Jsont.Object.opt_mem "external_url" Jsont.string ~enc:enc_external_url
201 |> Jsont.Object.opt_mem "title" Jsont.string ~enc:enc_title
202 |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:enc_summary
203 |> Jsont.Object.opt_mem "image" Jsont.string ~enc:enc_image
204 |> Jsont.Object.opt_mem "banner_image" Jsont.string ~enc:enc_banner_image
205 |> Jsont.Object.opt_mem "date_published" Rfc3339.jsont ~enc:enc_date_published
206 |> Jsont.Object.opt_mem "date_modified" Rfc3339.jsont ~enc:enc_date_modified
207 |> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:enc_authors
208 |> Jsont.Object.opt_mem "tags" (Jsont.list Jsont.string) ~enc:enc_tags
209 |> Jsont.Object.opt_mem "language" Jsont.string ~enc:enc_language
210 |> Jsont.Object.opt_mem "attachments"
211 (Jsont.list Attachment.jsont)
212 ~enc:enc_attachments
213 |> Jsont.Object.opt_mem "_references"
214 (Jsont.list Reference.jsont)
215 ~enc:enc_references
216 |> Jsont.Object.opt_mem "_extensions" Jsont.json_object ~enc:(fun _t -> None)
217 |> Jsont.Object.keep_unknown unknown_mems ~enc:enc_unknown
218 |> Jsont.Object.finish