OCaml library for JSONfeed parsing and creation

Compare changes

Choose any two refs to compare.

+1
.tangled/workflows/build.yml
···
- bzip2
- gcc
- ocaml
+
- pkg-config
steps:
- name: opam
+8 -2
CHANGES.md
···
-
v1.0.0
-
------
+
v1.1.0 (2025-11-12)
+
-------------------
+
+
- Simplify round trip processing of unknown messages using Jsont combinators (@avsm).
+
- Relax alcotest version constraints (@avsm).
+
+
v1.0.0 (2025-11-12)
+
-------------------
- Initial public release (@avsm)
+3 -3
dune-project
···
and serialization using Jsonm and Ptime.")
(depends
(ocaml (>= 5.2.0))
-
(jsonm (>= 1.0.0))
+
(jsont (>= 0.2.0))
(ptime (>= 1.2.0))
-
(fmt (>= 0.11.0))
+
bytesrw
(odoc :with-doc)
-
(alcotest (and :with-test (>= 1.9.0)))))
+
(alcotest (and :with-test (>= 1.5.0)))))
+4 -3
jsonfeed.opam
···
depends: [
"dune" {>= "3.18"}
"ocaml" {>= "5.2.0"}
-
"jsonm" {>= "1.0.0"}
+
"jsont" {>= "0.2.0"}
"ptime" {>= "1.2.0"}
-
"fmt" {>= "0.11.0"}
+
"bytesrw"
"odoc" {with-doc}
-
"alcotest" {with-test & >= "1.9.0"}
+
"alcotest" {with-test & >= "1.5.0"}
]
build: [
["dune" "subst"] {dev}
···
]
]
x-maintenance-intent: ["(latest)"]
+
dev-repo: "git+https://tangled.sh/@anil.recoil.org/ocaml-jsonfeed"
+12 -37
lib/attachment.ml
···
---------------------------------------------------------------------------*)
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type t = {
···
Format.fprintf ppf "%s (%s" filename t.mime_type;
-
(match t.size_in_bytes with
-
| Some size ->
+
Option.iter
+
(fun size ->
let mb = Int64.to_float size /. (1024. *. 1024.) in
-
Format.fprintf ppf ", %.1f MB" mb
-
| None -> ());
+
Format.fprintf ppf ", %.1f MB" mb)
+
t.size_in_bytes;
-
(match t.duration_in_seconds with
-
| Some duration ->
+
Option.iter
+
(fun duration ->
let mins = duration / 60 in
let secs = duration mod 60 in
-
Format.fprintf ppf ", %dm%ds" mins secs
-
| None -> ());
+
Format.fprintf ppf ", %dm%ds" mins secs)
+
t.duration_in_seconds;
Format.fprintf ppf ")"
let jsont =
let kind = "Attachment" in
let doc = "An attachment object" in
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
let create_obj url mime_type title size_in_bytes duration_in_seconds unknown =
create ~url ~mime_type ?title ?size_in_bytes ?duration_in_seconds ~unknown
()
···
|> Jsont.Object.opt_mem "size_in_bytes" Jsont.int64 ~enc:size_in_bytes
|> Jsont.Object.opt_mem "duration_in_seconds" Jsont.int
~enc:duration_in_seconds
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+3 -3
lib/attachment.mli
···
(** {1 Unknown Fields} *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown/unrecognized JSON object members as a generic JSON object. Useful
+
for preserving fields from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+4 -30
lib/author.ml
···
---------------------------------------------------------------------------*)
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type t = {
···
let jsont =
let kind = "Author" in
let doc = "An author object with at least one field set" in
-
(* Custom mems map for Unknown.t that strips metadata from names *)
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
(* Constructor that matches the jsont object map pattern *)
let create_obj name url avatar unknown =
create ?name ?url ?avatar ~unknown ()
···
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
|> Jsont.Object.opt_mem "url" Jsont.string ~enc:url
|> Jsont.Object.opt_mem "avatar" Jsont.string ~enc:avatar
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+3 -3
lib/author.mli
···
(** {1 Unknown Fields} *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown/unrecognized JSON object members as a generic JSON object. Useful
+
for preserving fields from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+1 -3
lib/cito.ml
···
let jsont =
let kind = "CiTO intent" in
let doc = "A Citation Typing Ontology intent annotation" in
-
let dec = of_string in
-
let enc = to_string in
-
Jsont.map ~kind ~doc ~dec ~enc Jsont.string
+
Jsont.map ~kind ~doc ~dec:of_string ~enc:to_string Jsont.string
+4 -29
lib/hub.ml
···
---------------------------------------------------------------------------*)
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type t = { type_ : string; url : string; unknown : Unknown.t }
···
let jsont =
let kind = "Hub" in
let doc = "A hub endpoint" in
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
let create_obj type_ url unknown = create ~type_ ~url ~unknown () in
Jsont.Object.map ~kind ~doc create_obj
|> Jsont.Object.mem "type" Jsont.string ~enc:type_
|> Jsont.Object.mem "url" Jsont.string ~enc:url
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+3 -3
lib/hub.mli
···
(** {1 Unknown Fields} *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown/unrecognized JSON object members as a generic JSON object. Useful
+
for preserving fields from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+22 -75
lib/item.ml
···
---------------------------------------------------------------------------*)
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type content = [ `Html of string | `Text of string | `Both of string * string ]
···
| `Both (_, text) -> Some text
let equal a b = a.id = b.id
-
-
let compare a b =
-
match (a.date_published, b.date_published) with
-
| None, None -> 0
-
| None, Some _ -> -1
-
| Some _, None -> 1
-
| Some da, Some db -> Ptime.compare da db
+
let compare a b = Option.compare Ptime.compare a.date_published b.date_published
let pp ppf t =
match (t.date_published, t.title) with
···
| None, None -> Format.fprintf ppf "%s" t.id
let pp_summary ppf t =
-
match t.title with
-
| Some title -> Format.fprintf ppf "%s" title
-
| None -> Format.fprintf ppf "%s" t.id
+
Format.fprintf ppf "%s" (Option.value ~default:t.id t.title)
(* Jsont type *)
···
}
in
-
(* Encoders to extract fields from item *)
-
let enc_id t = t.id in
-
let enc_content_html t = content_html t in
-
let enc_content_text t = content_text t in
-
let enc_url t = t.url in
-
let enc_external_url t = t.external_url in
-
let enc_title t = t.title in
-
let enc_summary t = t.summary in
-
let enc_image t = t.image in
-
let enc_banner_image t = t.banner_image in
-
let enc_date_published t = t.date_published in
-
let enc_date_modified t = t.date_modified in
-
let enc_authors t = t.authors in
-
let enc_tags t = t.tags in
-
let enc_language t = t.language in
-
let enc_attachments t = t.attachments in
-
let enc_references t = t.references in
-
let enc_unknown t = t.unknown in
-
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
-
Jsont.Object.map ~kind ~doc make_from_json
-
|> Jsont.Object.mem "id" Jsont.string ~enc:enc_id
-
|> Jsont.Object.opt_mem "content_html" Jsont.string ~enc:enc_content_html
-
|> Jsont.Object.opt_mem "content_text" Jsont.string ~enc:enc_content_text
-
|> Jsont.Object.opt_mem "url" Jsont.string ~enc:enc_url
-
|> Jsont.Object.opt_mem "external_url" Jsont.string ~enc:enc_external_url
-
|> Jsont.Object.opt_mem "title" Jsont.string ~enc:enc_title
-
|> Jsont.Object.opt_mem "summary" Jsont.string ~enc:enc_summary
-
|> Jsont.Object.opt_mem "image" Jsont.string ~enc:enc_image
-
|> Jsont.Object.opt_mem "banner_image" Jsont.string ~enc:enc_banner_image
-
|> Jsont.Object.opt_mem "date_published" Rfc3339.jsont ~enc:enc_date_published
-
|> Jsont.Object.opt_mem "date_modified" Rfc3339.jsont ~enc:enc_date_modified
-
|> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:enc_authors
-
|> Jsont.Object.opt_mem "tags" (Jsont.list Jsont.string) ~enc:enc_tags
-
|> Jsont.Object.opt_mem "language" Jsont.string ~enc:enc_language
+
|> Jsont.Object.mem "id" Jsont.string ~enc:id
+
|> Jsont.Object.opt_mem "content_html" Jsont.string ~enc:content_html
+
|> Jsont.Object.opt_mem "content_text" Jsont.string ~enc:content_text
+
|> Jsont.Object.opt_mem "url" Jsont.string ~enc:url
+
|> Jsont.Object.opt_mem "external_url" Jsont.string ~enc:external_url
+
|> Jsont.Object.opt_mem "title" Jsont.string ~enc:title
+
|> Jsont.Object.opt_mem "summary" Jsont.string ~enc:summary
+
|> Jsont.Object.opt_mem "image" Jsont.string ~enc:image
+
|> Jsont.Object.opt_mem "banner_image" Jsont.string ~enc:banner_image
+
|> Jsont.Object.opt_mem "date_published" Rfc3339.jsont ~enc:date_published
+
|> Jsont.Object.opt_mem "date_modified" Rfc3339.jsont ~enc:date_modified
+
|> Jsont.Object.opt_mem "authors" (Jsont.list Author.jsont) ~enc:authors
+
|> Jsont.Object.opt_mem "tags" (Jsont.list Jsont.string) ~enc:tags
+
|> Jsont.Object.opt_mem "language" Jsont.string ~enc:language
|> Jsont.Object.opt_mem "attachments"
(Jsont.list Attachment.jsont)
-
~enc:enc_attachments
+
~enc:attachments
|> Jsont.Object.opt_mem "_references"
(Jsont.list Reference.jsont)
-
~enc:enc_references
+
~enc:references
|> Jsont.Object.opt_mem "_extensions" Jsont.json_object ~enc:(fun _t -> None)
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:enc_unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+3 -3
lib/item.mli
···
(** {1 Unknown Fields} *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown/unrecognized JSON object members as a generic JSON object. Useful
+
for preserving fields from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+16 -47
lib/jsonfeed.ml
···
module Item = Item
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type t = {
···
let jsont =
let kind = "JSON Feed" in
let doc = "A JSON Feed document" in
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
(* Helper constructor that sets version automatically *)
let make_from_json _version title home_page_url feed_url description
···
|> Jsont.Object.opt_mem "expired" Jsont.bool ~enc:expired
|> Jsont.Object.opt_mem "hubs" (Jsont.list Hub.jsont) ~enc:hubs
|> Jsont.Object.mem "items" (Jsont.list Item.jsont) ~enc:items
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
(* Encoding and Decoding *)
···
add_error "items must have unique IDs";
(* Validate authors *)
-
(match feed.authors with
-
| Some authors ->
-
List.iteri
-
(fun i author ->
-
if not (Author.is_valid author) then
-
add_error
-
(Printf.sprintf
-
"feed author %d is invalid (needs at least one field)" i))
-
authors
-
| None -> ());
+
Option.iter
+
(List.iteri (fun i author ->
+
if not (Author.is_valid author) then
+
add_error
+
(Printf.sprintf
+
"feed author %d is invalid (needs at least one field)" i)))
+
feed.authors;
(* Validate items *)
List.iteri
···
add_error (Printf.sprintf "item %d has empty ID" i);
(* Validate item authors *)
-
match Item.authors item with
-
| Some authors ->
-
List.iteri
-
(fun j author ->
-
if not (Author.is_valid author) then
-
add_error (Printf.sprintf "item %d author %d is invalid" i j))
-
authors
-
| None -> ())
+
Option.iter
+
(List.iteri (fun j author ->
+
if not (Author.is_valid author) then
+
add_error (Printf.sprintf "item %d author %d is invalid" i j)))
+
(Item.authors item))
feed.items;
match !errors with [] -> Ok () | errs -> Error (List.rev errs)
+4 -3
lib/jsonfeed.mli
···
optional fields. *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown or unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown or unrecognized JSON object members as a generic JSON object.
+
Useful for preserving fields from custom extensions or future spec
+
versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+5 -30
lib/reference.ml
···
---------------------------------------------------------------------------*)
module Unknown = struct
-
type t = (string * Jsont.json) list
+
type t = Jsont.json
-
let empty = []
-
let is_empty = function [] -> true | _ -> false
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
let is_empty = function Jsont.Object ([], _) -> true | _ -> false
end
type t = {
···
let pp ppf t =
let open Format in
fprintf ppf "%s" t.url;
-
match t.doi with Some d -> fprintf ppf " [DOI: %s]" d | None -> ()
+
Option.iter (fprintf ppf " [DOI: %s]") t.doi
let jsont =
let kind = "Reference" in
let doc = "A reference to a cited source" in
-
let unknown_mems :
-
(Unknown.t, Jsont.json, Jsont.mem list) Jsont.Object.Mems.map =
-
let open Jsont.Object.Mems in
-
let dec_empty () = [] in
-
let dec_add _meta (name : string) value acc =
-
((name, Jsont.Meta.none), value) :: acc
-
in
-
let dec_finish _meta mems =
-
List.rev_map (fun ((name, _meta), value) -> (name, value)) mems
-
in
-
let enc =
-
{
-
enc =
-
(fun (type acc)
-
(f : Jsont.Meta.t -> string -> Jsont.json -> acc -> acc)
-
unknown
-
(acc : acc)
-
->
-
List.fold_left
-
(fun acc (name, value) -> f Jsont.Meta.none name value acc)
-
acc unknown);
-
}
-
in
-
map ~kind:"Unknown members" Jsont.json ~dec_empty ~dec_add ~dec_finish ~enc
-
in
let create_obj url doi cito unknown = create ~url ?doi ?cito ~unknown () in
Jsont.Object.map ~kind ~doc create_obj
|> Jsont.Object.mem "url" Jsont.string ~enc:url
|> Jsont.Object.opt_mem "doi" Jsont.string ~enc:doi
|> Jsont.Object.opt_mem "cito" (Jsont.list Cito.jsont) ~enc:cito
-
|> Jsont.Object.keep_unknown unknown_mems ~enc:unknown
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:unknown
|> Jsont.Object.finish
+3 -3
lib/reference.mli
···
(** {1 Unknown Fields} *)
module Unknown : sig
-
type t = (string * Jsont.json) list
-
(** Unknown/unrecognized JSON object members. Useful for preserving fields
-
from custom extensions or future spec versions. *)
+
type t = Jsont.json
+
(** Unknown/unrecognized JSON object members as a generic JSON object. Useful
+
for preserving fields from custom extensions or future spec versions. *)
val empty : t
(** [empty] is the empty list of unknown fields. *)
+1 -1
lib/rfc3339.ml
···
---------------------------------------------------------------------------*)
let parse s =
-
match Ptime.of_rfc3339 s with Ok (t, _, _) -> Some t | Error _ -> None
+
Ptime.of_rfc3339 s |> Result.to_option |> Option.map (fun (t, _, _) -> t)
let format t = Ptime.to_rfc3339 ~frac_s:6 ~tz_offset_s:0 t
let pp ppf t = Format.pp_print_string ppf (format t)
+182
test/test_jsonfeed.ml
···
test_feed_parse_invalid_missing_content );
]
+
(* Unknown fields preservation tests *)
+
+
let test_author_unknown_roundtrip () =
+
let json =
+
{|{
+
"name": "Test Author",
+
"custom_field": "custom value",
+
"another_extension": 42
+
}|}
+
in
+
match Jsont_bytesrw.decode_string' Author.jsont json with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e))
+
| Ok author -> (
+
(* Check that unknown fields are preserved *)
+
let unknown = Author.unknown author in
+
Alcotest.(check bool)
+
"has unknown fields" false
+
(Jsonfeed.Unknown.is_empty unknown);
+
(* Encode and decode again *)
+
match Jsont_bytesrw.encode_string' Author.jsont author with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e))
+
| Ok json2 -> (
+
match Jsont_bytesrw.decode_string' Author.jsont json2 with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e))
+
| Ok author2 ->
+
(* Verify unknown fields survive roundtrip *)
+
let unknown2 = Author.unknown author2 in
+
Alcotest.(check bool)
+
"unknown fields preserved" false
+
(Jsonfeed.Unknown.is_empty unknown2)))
+
+
let test_item_unknown_roundtrip () =
+
let json =
+
{|{
+
"id": "https://example.com/1",
+
"content_html": "<p>Test</p>",
+
"custom_metadata": "some custom data",
+
"x_custom_number": 123.45
+
}|}
+
in
+
match Jsont_bytesrw.decode_string' Item.jsont json with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e))
+
| Ok item -> (
+
(* Check that unknown fields are preserved *)
+
let unknown = Item.unknown item in
+
Alcotest.(check bool)
+
"has unknown fields" false
+
(Jsonfeed.Unknown.is_empty unknown);
+
(* Encode and decode again *)
+
match Jsont_bytesrw.encode_string' Item.jsont item with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e))
+
| Ok json2 -> (
+
match Jsont_bytesrw.decode_string' Item.jsont json2 with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e))
+
| Ok item2 ->
+
let unknown2 = Item.unknown item2 in
+
Alcotest.(check bool)
+
"unknown fields preserved" false
+
(Jsonfeed.Unknown.is_empty unknown2)))
+
+
let test_feed_unknown_roundtrip () =
+
let json =
+
{|{
+
"version": "https://jsonfeed.org/version/1.1",
+
"title": "Test Feed",
+
"items": [],
+
"custom_extension": "custom value",
+
"x_another_field": {"nested": "data"}
+
}|}
+
in
+
match Jsonfeed.of_string json with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e))
+
| Ok feed -> (
+
(* Check that unknown fields are preserved *)
+
let unknown = Jsonfeed.unknown feed in
+
Alcotest.(check bool)
+
"has unknown fields" false
+
(Jsonfeed.Unknown.is_empty unknown);
+
(* Encode and decode again *)
+
match Jsonfeed.to_string feed with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e))
+
| Ok json2 -> (
+
match Jsonfeed.of_string json2 with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e))
+
| Ok feed2 ->
+
let unknown2 = Jsonfeed.unknown feed2 in
+
Alcotest.(check bool)
+
"unknown fields preserved" false
+
(Jsonfeed.Unknown.is_empty unknown2)))
+
+
let test_hub_unknown_roundtrip () =
+
let json =
+
{|{
+
"type": "WebSub",
+
"url": "https://example.com/hub",
+
"custom_field": "test"
+
}|}
+
in
+
match Jsont_bytesrw.decode_string' Hub.jsont json with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e))
+
| Ok hub -> (
+
let unknown = Hub.unknown hub in
+
Alcotest.(check bool)
+
"has unknown fields" false
+
(Jsonfeed.Unknown.is_empty unknown);
+
match Jsont_bytesrw.encode_string' Hub.jsont hub with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e))
+
| Ok json2 -> (
+
match Jsont_bytesrw.decode_string' Hub.jsont json2 with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e))
+
| Ok hub2 ->
+
let unknown2 = Hub.unknown hub2 in
+
Alcotest.(check bool)
+
"unknown fields preserved" false
+
(Jsonfeed.Unknown.is_empty unknown2)))
+
+
let test_attachment_unknown_roundtrip () =
+
let json =
+
{|{
+
"url": "https://example.com/file.mp3",
+
"mime_type": "audio/mpeg",
+
"x_custom": "value"
+
}|}
+
in
+
match Jsont_bytesrw.decode_string' Attachment.jsont json with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e))
+
| Ok att -> (
+
let unknown = Attachment.unknown att in
+
Alcotest.(check bool)
+
"has unknown fields" false
+
(Jsonfeed.Unknown.is_empty unknown);
+
match Jsont_bytesrw.encode_string' Attachment.jsont att with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e))
+
| Ok json2 -> (
+
match Jsont_bytesrw.decode_string' Attachment.jsont json2 with
+
| Error e ->
+
Alcotest.fail
+
(Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e))
+
| Ok att2 ->
+
let unknown2 = Attachment.unknown att2 in
+
Alcotest.(check bool)
+
"unknown fields preserved" false
+
(Jsonfeed.Unknown.is_empty unknown2)))
+
+
let unknown_fields_tests =
+
[
+
("author unknown roundtrip", `Quick, test_author_unknown_roundtrip);
+
("item unknown roundtrip", `Quick, test_item_unknown_roundtrip);
+
("feed unknown roundtrip", `Quick, test_feed_unknown_roundtrip);
+
("hub unknown roundtrip", `Quick, test_hub_unknown_roundtrip);
+
("attachment unknown roundtrip", `Quick, test_attachment_unknown_roundtrip);
+
]
+
(* Main test suite *)
let () =
···
("Hub", hub_tests);
("Item", item_tests);
("Jsonfeed", jsonfeed_tests);
+
("Unknown Fields", unknown_fields_tests);
]