OCaml library for JSONfeed parsing and creation
at v1.0.0 6.4 kB view raw
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 ?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 let unknown_mems : 89 (Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map = 90 let open Jsont.Object.Mems in 91 let dec_empty () = [] in 92 let dec_add _meta (name : string) value acc = 93 ((name, Jsont.Meta.none), value) :: acc 94 in 95 let dec_finish _meta mems = 96 List.rev_map (fun ((name, _meta), value) -> (name, value)) mems 97 in 98 let enc = 99 { 100 enc = 101 (fun (type acc) 102 (f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc) 103 unknown 104 (acc : acc) 105 -> 106 List.fold_left 107 (fun acc (name, value) -> f Jsont.Meta.none name value acc) 108 acc unknown); 109 } 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 116 user_comment next_url icon favicon authors language expired hubs items 117 unknown = 118 { 119 version = "https://jsonfeed.org/version/1.1"; 120 title; 121 home_page_url; 122 feed_url; 123 description; 124 user_comment; 125 next_url; 126 icon; 127 favicon; 128 authors; 129 language; 130 expired; 131 hubs; 132 items; 133 unknown; 134 } 135 in 136 137 Jsont.Object.map ~kind ~doc make_from_json 138 |> Jsont.Object.mem "version" Jsont.string ~enc:version 139 |> Jsont.Object.mem "title" Jsont.string ~enc:title 140 |> Jsont.Object.opt_mem "home_page_url" Jsont.string ~enc:home_page_url 141 |> Jsont.Object.opt_mem "feed_url" Jsont.string ~enc:feed_url 142 |> Jsont.Object.opt_mem "description" Jsont.string ~enc:description 143 |> Jsont.Object.opt_mem "user_comment" Jsont.string ~enc:user_comment 144 |> Jsont.Object.opt_mem "next_url" Jsont.string ~enc:next_url 145 |> Jsont.Object.opt_mem "icon" Jsont.string ~enc:icon 146 |> Jsont.Object.opt_mem "favicon" Jsont.string ~enc:favicon 147 |> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:authors 148 |> Jsont.Object.opt_mem "language" Jsont.string ~enc:language 149 |> Jsont.Object.opt_mem "expired" Jsont.bool ~enc:expired 150 |> Jsont.Object.opt_mem "hubs" (Jsont.list Hub.jsont) ~enc:hubs 151 |> Jsont.Object.mem "items" (Jsont.list Item.jsont) ~enc:items 152 |> Jsont.Object.keep_unknown unknown_mems ~enc:unknown 153 |> Jsont.Object.finish 154 155(* Encoding and Decoding *) 156 157let decode ?layout ?locs ?file r = 158 Jsont_bytesrw.decode' ?layout ?locs ?file jsont r 159 160let decode_string ?layout ?locs ?file s = 161 Jsont_bytesrw.decode_string' ?layout ?locs ?file jsont s 162 163let encode ?format ?number_format feed ~eod w = 164 Jsont_bytesrw.encode' ?format ?number_format jsont feed ~eod w 165 166let encode_string ?format ?number_format feed = 167 Jsont_bytesrw.encode_string' ?format ?number_format jsont feed 168 169let of_string s = 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 add_error "title is required and cannot be empty"; 183 184 (* Check items have unique IDs *) 185 let ids = List.map Item.id feed.items in 186 let unique_ids = List.sort_uniq String.compare ids in 187 if List.length ids <> List.length unique_ids then 188 add_error "items must have unique IDs"; 189 190 (* Validate authors *) 191 (match feed.authors with 192 | Some authors -> 193 List.iteri 194 (fun i author -> 195 if not (Author.is_valid author) then 196 add_error 197 (Printf.sprintf 198 "feed author %d is invalid (needs at least one field)" i)) 199 authors 200 | None -> ()); 201 202 (* Validate items *) 203 List.iteri 204 (fun i item -> 205 if Item.id item = "" then 206 add_error (Printf.sprintf "item %d has empty ID" i); 207 208 (* Validate item authors *) 209 match Item.authors item with 210 | Some authors -> 211 List.iteri 212 (fun j author -> 213 if not (Author.is_valid author) then 214 add_error (Printf.sprintf "item %d author %d is invalid" i j)) 215 authors 216 | None -> ()) 217 feed.items; 218 219 match !errors with [] -> Ok () | errs -> Error (List.rev errs)