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