···1+ISC License
2+3+Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org>
4+5+Permission to use, copy, modify, and distribute this software for any
6+purpose with or without fee is hereby granted, provided that the above
7+copyright notice and this permission notice appear in all copies.
8+9+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Citation File Format (CFF) codec for OCaml.
78- This library provides types and codecs for the
9 {{:https://citation-file-format.github.io/}Citation File Format (CFF)}
10 version 1.2.0, a human- and machine-readable format for software and
11 dataset citation metadata.
···22 - [title]: The name of the software or dataset
23 - [authors]: A list of persons and/or entities
2425- {1 Quick Start}
26-27 {2 Creating a CFF record}
2829 {[
30- let author = Cff.Author.Person
31- (Cff.Person.make ~family_names:"Smith" ~given_names:"Jane" ()) in
32 let cff = Cff.make
33 ~title:"My Research Software"
34 ~authors:[author]
···37 ()
38 ]}
3940- {2 File I/O}
4142 For file operations, use the backend-specific subpackages:
43 - [cff.unix] - Unix file I/O using {!In_channel}/{!Out_channel}
···5455 This implementation follows the
56 {{:https://github.com/citation-file-format/citation-file-format}CFF 1.2.0 specification}.
57- Key concepts include:
5859 - {!module:Author}: Can be persons (with family/given names) or entities
60 (organizations, identified by a [name] field)
···100 Used for author and entity addresses. *)
101module Country = Cff_country
102000000000000103(** SPDX license identifiers.
104105 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers} for
···160161(** {1 Construction} *)
162163-val default_cff_version : string
164(** The default CFF version used when not specified: ["1.2.0"]. *)
0165166-val default_message : string
167(** The default citation message:
168 ["If you use this software, please cite it using the metadata from this file."] *)
0169170-val make :
171- ?cff_version:string ->
172- ?message:string ->
173- title:string ->
174- authors:Author.t list ->
175- ?abstract:string ->
176- ?commit:string ->
177- ?contact:Author.t list ->
178- ?date_released:Date.t ->
179- ?doi:string ->
180- ?identifiers:Identifier.t list ->
181- ?keywords:string list ->
182- ?license:License.t ->
183- ?license_url:string ->
184- ?preferred_citation:Reference.t ->
185- ?references:Reference.t list ->
186- ?repository:string ->
187- ?repository_artifact:string ->
188- ?repository_code:string ->
189- ?type_:Cff_type.t ->
190- ?url:string ->
191- ?version:string ->
192- unit -> t
193(** [make ~title ~authors ...] constructs a CFF value.
194195 @param cff_version The CFF schema version (default: {!default_cff_version})
196 @param message Instructions for users on how to cite (default: {!default_message})
197 @param title The name of the software or dataset
198 @param authors List of persons and/or entities who created the work *)
00000000000000000000000199200(** {2 Required Fields} *)
201202-val cff_version : t -> string
203(** The CFF schema version that this file adheres to.
204205 For CFF 1.2.0 files, this should be ["1.2.0"]. The version determines
206 which keys are valid and how they should be interpreted. *)
0207208-val message : t -> string
209(** A message to readers explaining how to cite the work.
210211 Common examples:
···213 - ["Please cite this software using the metadata from 'preferred-citation'."]
214215 The message should guide users toward the preferred citation method. *)
0216217-val title : t -> string
218(** The name of the software or dataset.
219220 This is the title that should appear in citations. For software, it's
221 typically the project name; for datasets, the dataset title. *)
0222223-val authors : t -> Author.t list
224(** The creators of the software or dataset.
225226 Authors can be persons (individuals) or entities (organizations).
227 At least one author is required for a valid CFF file. The order
228 typically reflects contribution significance. *)
0229230(** {2 Optional Fields} *)
231232-val abstract : t -> string option
233(** A description of the software or dataset.
234235 Provides context about what the work does, its purpose, and scope. *)
0236237-val commit : t -> string option
238(** The commit hash or revision number of the software version.
239240 Useful for precise version identification beyond semantic versioning.
241 Example: ["1ff847d81f29c45a3a1a5ce73d38e45c2f319bba"] *)
0242243-val contact : t -> Author.t list option
244(** Contact persons or entities for the software or dataset.
245246 May differ from authors; useful when the primary contact is a
247 project maintainer rather than the original author. *)
0248249-val date_released : t -> Date.t option
250(** The date when the software or dataset was released.
251252 Format is [(year, month, day)], corresponding to ISO 8601 [YYYY-MM-DD]. *)
0253254-val doi : t -> string option
255(** The Digital Object Identifier for the software or dataset.
256257 DOIs provide persistent, citable identifiers. This is a shorthand
258 for a single DOI; use {!identifiers} for multiple DOIs or other
259 identifier types. Example: ["10.5281/zenodo.1234567"] *)
0260261-val identifiers : t -> Identifier.t list option
262(** Additional identifiers beyond the primary DOI.
263264 Each identifier has a type (DOI, URL, SWH, other), value, and
265 optional description. Useful for versioned DOIs, Software Heritage
266 identifiers, or repository URLs. *)
0267268-val keywords : t -> string list option
269(** Descriptive keywords for the work.
270271 Help with discoverability and categorization. Example:
272 [["machine learning"; "image processing"; "python"]] *)
0273274-val license : t -> License.t option
275(** The SPDX license identifier(s) for the work.
276277 Uses {{:https://spdx.org/licenses/}SPDX identifiers}. Multiple
278 licenses imply an OR relationship (user may choose any).
279 Example: ["MIT"], ["Apache-2.0"], or [["GPL-3.0-only"; "MIT"]]. *)
280-281-val license_url : t -> string option
282-(** URL to the license text for non-standard licenses.
283-284- Only needed for licenses not in the SPDX list. Standard SPDX
285- licenses have well-known URLs. *)
286287-val preferred_citation : t -> Reference.t option
288(** A reference to cite instead of the software itself.
289290 Used for "credit redirection" when authors prefer citation of
291 a related publication (e.g., a methods paper) over the software.
292 Note: Software citation principles recommend citing software
293 directly; use this field judiciously. *)
0294295-val references : t -> Reference.t list option
296(** Works that this software cites or depends upon.
297298 Functions like a bibliography, listing dependencies, foundational
299 works, or related publications. Each reference includes full
300 bibliographic metadata. *)
0301302-val repository : t -> string option
303(** URL to the repository where the software is developed.
304305 Typically a version control system URL. For source code repositories,
306 prefer {!repository_code}. *)
0307308-val repository_artifact : t -> string option
309(** URL to the built/compiled artifact repository.
310311 For binary distributions, package registries (npm, PyPI, CRAN),
312 or container registries. *)
0313314-val repository_code : t -> string option
315(** URL to the source code repository.
316317 Typically a GitHub, GitLab, or similar URL where the source
318 code is publicly accessible. *)
0319320-val type_ : t -> Cff_type.t option
321(** The type of work: [`Software] (default) or [`Dataset].
322323 Most CFF files describe software; use [`Dataset] for data packages. *)
0324325-val url : t -> string option
326(** The URL of the software or dataset homepage.
327328 A general landing page, documentation site, or project website. *)
0329330-val version : t -> string option
331(** The version string of the software or dataset.
332333 Can be any version format: semantic versioning (["1.2.3"]),
334 date-based (["2024.01"]), or other schemes. *)
0335336(** {1 Formatting and Codec} *)
3370338val pp : Format.formatter -> t -> unit
339-(** Pretty-print a CFF value in a human-readable YAML-like format. *)
340341-val jsont : t Jsont.t
342(** JSON/YAML codec for serialization and deserialization.
343344 Used internally by the YAML codec functions. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Citation File Format (CFF) codec for OCaml.
78+ This library provides codecs for the
9 {{:https://citation-file-format.github.io/}Citation File Format (CFF)}
10 version 1.2.0, a human- and machine-readable format for software and
11 dataset citation metadata.
···22 - [title]: The name of the software or dataset
23 - [authors]: A list of persons and/or entities
240025 {2 Creating a CFF record}
2627 {[
28+ let author = Cff.Author.person
29+ ~family_names:"Smith" ~given_names:"Jane" () in
30 let cff = Cff.make
31 ~title:"My Research Software"
32 ~authors:[author]
···35 ()
36 ]}
3738+ {2 I/O}
3940 For file operations, use the backend-specific subpackages:
41 - [cff.unix] - Unix file I/O using {!In_channel}/{!Out_channel}
···5253 This implementation follows the
54 {{:https://github.com/citation-file-format/citation-file-format}CFF 1.2.0 specification}.
55+ Useful modules include:
5657 - {!module:Author}: Can be persons (with family/given names) or entities
58 (organizations, identified by a [name] field)
···98 Used for author and entity addresses. *)
99module Country = Cff_country
100101+(** Physical address information.
102+103+ Address fields used for persons and entities: street address, city,
104+ region (state/province), postal code, and country code. *)
105+module Address = Cff_address.Address
106+107+(** Contact information.
108+109+ Contact fields used for persons and entities: email, telephone, fax,
110+ website URL, and ORCID identifier. *)
111+module Contact = Cff_address.Contact
112+113(** SPDX license identifiers.
114115 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers} for
···170171(** {1 Construction} *)
1720173(** The default CFF version used when not specified: ["1.2.0"]. *)
174+val default_cff_version : string
1750176(** The default citation message:
177 ["If you use this software, please cite it using the metadata from this file."] *)
178+val default_message : string
17900000000000000000000000180(** [make ~title ~authors ...] constructs a CFF value.
181182 @param cff_version The CFF schema version (default: {!default_cff_version})
183 @param message Instructions for users on how to cite (default: {!default_message})
184 @param title The name of the software or dataset
185 @param authors List of persons and/or entities who created the work *)
186+val make
187+ : ?cff_version:string
188+ -> ?message:string
189+ -> title:string
190+ -> authors:Author.t list
191+ -> ?abstract:string
192+ -> ?commit:string
193+ -> ?contact:Author.t list
194+ -> ?date_released:Date.t
195+ -> ?doi:string
196+ -> ?identifiers:Identifier.t list
197+ -> ?keywords:string list
198+ -> ?license:License.t
199+ -> ?preferred_citation:Reference.t
200+ -> ?references:Reference.t list
201+ -> ?repository:string
202+ -> ?repository_artifact:string
203+ -> ?repository_code:string
204+ -> ?type_:Cff_type.t
205+ -> ?url:string
206+ -> ?version:string
207+ -> unit
208+ -> t
209210(** {2 Required Fields} *)
2110212(** The CFF schema version that this file adheres to.
213214 For CFF 1.2.0 files, this should be ["1.2.0"]. The version determines
215 which keys are valid and how they should be interpreted. *)
216+val cff_version : t -> string
2170218(** A message to readers explaining how to cite the work.
219220 Common examples:
···222 - ["Please cite this software using the metadata from 'preferred-citation'."]
223224 The message should guide users toward the preferred citation method. *)
225+val message : t -> string
2260227(** The name of the software or dataset.
228229 This is the title that should appear in citations. For software, it's
230 typically the project name; for datasets, the dataset title. *)
231+val title : t -> string
2320233(** The creators of the software or dataset.
234235 Authors can be persons (individuals) or entities (organizations).
236 At least one author is required for a valid CFF file. The order
237 typically reflects contribution significance. *)
238+val authors : t -> Author.t list
239240(** {2 Optional Fields} *)
2410242(** A description of the software or dataset.
243244 Provides context about what the work does, its purpose, and scope. *)
245+val abstract : t -> string option
2460247(** The commit hash or revision number of the software version.
248249 Useful for precise version identification beyond semantic versioning.
250 Example: ["1ff847d81f29c45a3a1a5ce73d38e45c2f319bba"] *)
251+val commit : t -> string option
2520253(** Contact persons or entities for the software or dataset.
254255 May differ from authors; useful when the primary contact is a
256 project maintainer rather than the original author. *)
257+val contact : t -> Author.t list option
2580259(** The date when the software or dataset was released.
260261 Format is [(year, month, day)], corresponding to ISO 8601 [YYYY-MM-DD]. *)
262+val date_released : t -> Date.t option
2630264(** The Digital Object Identifier for the software or dataset.
265266 DOIs provide persistent, citable identifiers. This is a shorthand
267 for a single DOI; use {!identifiers} for multiple DOIs or other
268 identifier types. Example: ["10.5281/zenodo.1234567"] *)
269+val doi : t -> string option
2700271(** Additional identifiers beyond the primary DOI.
272273 Each identifier has a type (DOI, URL, SWH, other), value, and
274 optional description. Useful for versioned DOIs, Software Heritage
275 identifiers, or repository URLs. *)
276+val identifiers : t -> Identifier.t list option
2770278(** Descriptive keywords for the work.
279280 Help with discoverability and categorization. Example:
281 [["machine learning"; "image processing"; "python"]] *)
282+val keywords : t -> string list option
2830284(** The SPDX license identifier(s) for the work.
285286 Uses {{:https://spdx.org/licenses/}SPDX identifiers}. Multiple
287 licenses imply an OR relationship (user may choose any).
288 Example: ["MIT"], ["Apache-2.0"], or [["GPL-3.0-only"; "MIT"]]. *)
289+val license : t -> License.t option
000002900291(** A reference to cite instead of the software itself.
292293 Used for "credit redirection" when authors prefer citation of
294 a related publication (e.g., a methods paper) over the software.
295 Note: Software citation principles recommend citing software
296 directly; use this field judiciously. *)
297+val preferred_citation : t -> Reference.t option
2980299(** Works that this software cites or depends upon.
300301 Functions like a bibliography, listing dependencies, foundational
302 works, or related publications. Each reference includes full
303 bibliographic metadata. *)
304+val references : t -> Reference.t list option
3050306(** URL to the repository where the software is developed.
307308 Typically a version control system URL. For source code repositories,
309 prefer {!repository_code}. *)
310+val repository : t -> string option
3110312(** URL to the built/compiled artifact repository.
313314 For binary distributions, package registries (npm, PyPI, CRAN),
315 or container registries. *)
316+val repository_artifact : t -> string option
3170318(** URL to the source code repository.
319320 Typically a GitHub, GitLab, or similar URL where the source
321 code is publicly accessible. *)
322+val repository_code : t -> string option
3230324(** The type of work: [`Software] (default) or [`Dataset].
325326 Most CFF files describe software; use [`Dataset] for data packages. *)
327+val type_ : t -> Cff_type.t option
3280329(** The URL of the software or dataset homepage.
330331 A general landing page, documentation site, or project website. *)
332+val url : t -> string option
3330334(** The version string of the software or dataset.
335336 Can be any version format: semantic versioning (["1.2.3"]),
337 date-based (["2024.01"]), or other schemes. *)
338+val version : t -> string option
339340(** {1 Formatting and Codec} *)
341342+(** Pretty-print a CFF value in a human-readable YAML-like format. *)
343val pp : Format.formatter -> t -> unit
03440345(** JSON/YAML codec for serialization and deserialization.
346347 Used internally by the YAML codec functions. *)
348+val jsont : t Jsont.t
+43-55
lib/cff_address.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···78(** Physical address information. *)
9module Address = struct
10- type t = {
11- address : string option;
12- city : string option;
13- region : string option;
14- post_code : string option;
15- country : string option; (* ISO 3166-1 alpha-2 *)
16- }
1718- let empty = {
19- address = None;
20- city = None;
21- region = None;
22- post_code = None;
23- country = None;
24- }
2526 let make ?address ?city ?region ?post_code ?country () =
27 { address; city; region; post_code; country }
02829 let of_options ~address ~city ~region ~post_code ~country =
30 { address; city; region; post_code; country }
03132 let address t = t.address
33 let city t = t.city
···36 let country t = t.country
3738 let is_empty t =
39- t.address = None && t.city = None && t.region = None &&
40- t.post_code = None && t.country = None
00004142 let pp ppf t =
43- let parts = List.filter_map Fun.id [
44- t.address;
45- t.city;
46- t.region;
47- t.post_code;
48- t.country;
49- ] in
50 Format.pp_print_string ppf (String.concat ", " parts)
05152 let jsont_fields ~get obj =
53 obj
···56 |> Jsont.Object.opt_mem "region" Jsont.string ~enc:(fun x -> (get x).region)
57 |> Jsont.Object.opt_mem "post-code" Jsont.string ~enc:(fun x -> (get x).post_code)
58 |> Jsont.Object.opt_mem "country" Jsont.string ~enc:(fun x -> (get x).country)
059end
6061(** Contact information. *)
62module Contact = struct
63- type t = {
64- email : string option;
65- tel : string option;
66- fax : string option;
67- website : string option;
68- orcid : string option;
69- }
7071- let empty = {
72- email = None;
73- tel = None;
74- fax = None;
75- website = None;
76- orcid = None;
77- }
78-79- let make ?email ?tel ?fax ?website ?orcid () =
80- { email; tel; fax; website; orcid }
81-82- let of_options ~email ~tel ~fax ~website ~orcid =
83- { email; tel; fax; website; orcid }
84-85 let email t = t.email
86 let tel t = t.tel
87 let fax t = t.fax
···89 let orcid t = t.orcid
9091 let is_empty t =
92- t.email = None && t.tel = None && t.fax = None &&
93- t.website = None && t.orcid = None
9495 let pp ppf t =
96- let parts = List.filter_map (fun (k, v) ->
97- Option.map (fun v -> k ^ ": " ^ v) v
98- ) [
99- ("email", t.email);
100- ("tel", t.tel);
101- ("website", t.website);
102- ("orcid", t.orcid);
103- ] in
104 Format.pp_print_string ppf (String.concat ", " parts)
0105106 let jsont_fields ~get obj =
107 obj
···110 |> Jsont.Object.opt_mem "fax" Jsont.string ~enc:(fun x -> (get x).fax)
111 |> Jsont.Object.opt_mem "website" Jsont.string ~enc:(fun x -> (get x).website)
112 |> Jsont.Object.opt_mem "orcid" Jsont.string ~enc:(fun x -> (get x).orcid)
0113end
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···78(** Physical address information. *)
9module Address = struct
10+ type t =
11+ { address : string option
12+ ; city : string option
13+ ; region : string option
14+ ; post_code : string option
15+ ; country : string option (* ISO 3166-1 alpha-2 *)
16+ }
1718+ let empty =
19+ { address = None; city = None; region = None; post_code = None; country = None }
20+ ;;
00002122 let make ?address ?city ?region ?post_code ?country () =
23 { address; city; region; post_code; country }
24+ ;;
2526 let of_options ~address ~city ~region ~post_code ~country =
27 { address; city; region; post_code; country }
28+ ;;
2930 let address t = t.address
31 let city t = t.city
···34 let country t = t.country
3536 let is_empty t =
37+ t.address = None
38+ && t.city = None
39+ && t.region = None
40+ && t.post_code = None
41+ && t.country = None
42+ ;;
4344 let pp ppf t =
45+ let parts =
46+ List.filter_map Fun.id [ t.address; t.city; t.region; t.post_code; t.country ]
47+ in
000048 Format.pp_print_string ppf (String.concat ", " parts)
49+ ;;
5051 let jsont_fields ~get obj =
52 obj
···55 |> Jsont.Object.opt_mem "region" Jsont.string ~enc:(fun x -> (get x).region)
56 |> Jsont.Object.opt_mem "post-code" Jsont.string ~enc:(fun x -> (get x).post_code)
57 |> Jsont.Object.opt_mem "country" Jsont.string ~enc:(fun x -> (get x).country)
58+ ;;
59end
6061(** Contact information. *)
62module Contact = struct
63+ type t =
64+ { email : string option
65+ ; tel : string option
66+ ; fax : string option
67+ ; website : string option
68+ ; orcid : string option
69+ }
7071+ let empty = { email = None; tel = None; fax = None; website = None; orcid = None }
72+ let make ?email ?tel ?fax ?website ?orcid () = { email; tel; fax; website; orcid }
73+ let of_options ~email ~tel ~fax ~website ~orcid = { email; tel; fax; website; orcid }
0000000000074 let email t = t.email
75 let tel t = t.tel
76 let fax t = t.fax
···78 let orcid t = t.orcid
7980 let is_empty t =
81+ t.email = None && t.tel = None && t.fax = None && t.website = None && t.orcid = None
82+ ;;
8384 let pp ppf t =
85+ let parts =
86+ List.filter_map
87+ (fun (k, v) -> Option.map (fun v -> k ^ ": " ^ v) v)
88+ [ "email", t.email; "tel", t.tel; "website", t.website; "orcid", t.orcid ]
89+ in
00090 Format.pp_print_string ppf (String.concat ", " parts)
91+ ;;
9293 let jsont_fields ~get obj =
94 obj
···97 |> Jsont.Object.opt_mem "fax" Jsont.string ~enc:(fun x -> (get x).fax)
98 |> Jsont.Object.opt_mem "website" Jsont.string ~enc:(fun x -> (get x).website)
99 |> Jsont.Object.opt_mem "orcid" Jsont.string ~enc:(fun x -> (get x).orcid)
100+ ;;
101end
+71-57
lib/cff_address.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···4849 All fields are optional; an empty address is valid. *)
50module Address : sig
51- type t
52 (** Physical address record. *)
05354- val empty : t
55 (** Empty address with all fields [None]. *)
05657- val make :
58- ?address:string ->
59- ?city:string ->
60- ?region:string ->
61- ?post_code:string ->
62- ?country:string ->
63- unit -> t
64 (** Create an address with optional fields.
6566 @param address Street address
···68 @param region State, province, or administrative region
69 @param post_code Postal code, ZIP code, or postcode
70 @param country ISO 3166-1 alpha-2 country code *)
000000007172- val of_options :
73- address:string option ->
74- city:string option ->
75- region:string option ->
76- post_code:string option ->
77- country:string option ->
78- t
79 (** Create an address from option values directly.
8081 Used internally by jsont decoders where fields are decoded as options. *)
00000008283- val address : t -> string option
84 (** Street address (e.g., ["77 Massachusetts Avenue"]). *)
08586- val city : t -> string option
87 (** City name (e.g., ["Cambridge"], ["London"]). *)
08889- val region : t -> string option
90 (** State, province, or region (e.g., ["Massachusetts"], ["Bavaria"]). *)
09192- val post_code : t -> string option
93 (** Postal or ZIP code (e.g., ["02139"], ["W1A 1AA"]). *)
09495- val country : t -> string option
96 (** ISO 3166-1 alpha-2 country code (e.g., ["US"], ["DE"], ["GB"]). *)
09798- val is_empty : t -> bool
99 (** [true] if all fields are [None]. *)
0100101- val pp : Format.formatter -> t -> unit
102 (** Pretty-print the address. *)
0103104- val jsont_fields :
105- get:('a -> t) ->
106- ('a, string option -> string option -> string option ->
107- string option -> string option -> 'b) Jsont.Object.map ->
108- ('a, 'b) Jsont.Object.map
109 (** Add address fields to a jsont object builder.
110111 This adds the five address fields (address, city, region, post-code,
···113 [string option] arguments in that order.
114115 @param get Extracts the address from the parent type for encoding *)
00000000000116end
117118(** Contact information.
···120 Electronic contact details for persons and entities. All fields
121 are optional. *)
122module Contact : sig
123- type t
124 (** Contact information record. *)
0125126- val empty : t
127 (** Empty contact with all fields [None]. *)
0128129- val make :
130- ?email:string ->
131- ?tel:string ->
132- ?fax:string ->
133- ?website:string ->
134- ?orcid:string ->
135- unit -> t
136 (** Create contact information with optional fields.
137138 @param email Email address
···140 @param fax Fax number (any format)
141 @param website Website URL
142 @param orcid ORCID identifier URL *)
00000000143144- val of_options :
145- email:string option ->
146- tel:string option ->
147- fax:string option ->
148- website:string option ->
149- orcid:string option ->
150- t
151 (** Create contact info from option values directly.
152153 Used internally by jsont decoders where fields are decoded as options. *)
0000000154155- val email : t -> string option
156 (** Email address (e.g., ["jane.smith\@example.org"]). *)
0157158- val tel : t -> string option
159 (** Telephone number. No specific format is required. *)
0160161- val fax : t -> string option
162 (** Fax number. No specific format is required. *)
0163164- val website : t -> string option
165 (** Website URL (e.g., ["https://example.org/~jsmith"]). *)
0166167- val orcid : t -> string option
168 (** ORCID identifier as a URL.
169170 ORCID (Open Researcher and Contributor ID) provides persistent
···173 Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]
174175 Example: ["https://orcid.org/0000-0001-2345-6789"] *)
0176177- val is_empty : t -> bool
178 (** [true] if all fields are [None]. *)
0179180- val pp : Format.formatter -> t -> unit
181 (** Pretty-print the contact information. *)
0182183- val jsont_fields :
184- get:('a -> t) ->
185- ('a, string option -> string option -> string option ->
186- string option -> string option -> 'b) Jsont.Object.map ->
187- ('a, 'b) Jsont.Object.map
188 (** Add contact fields to a jsont object builder.
189190 This adds the five contact fields (email, tel, fax, website, orcid)
···192 [string option] arguments in that order.
193194 @param get Extracts the contact from the parent type for encoding *)
00000000000195end
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···4849 All fields are optional; an empty address is valid. *)
50module Address : sig
051 (** Physical address record. *)
52+ type t
53054 (** Empty address with all fields [None]. *)
55+ val empty : t
56000000057 (** Create an address with optional fields.
5859 @param address Street address
···61 @param region State, province, or administrative region
62 @param post_code Postal code, ZIP code, or postcode
63 @param country ISO 3166-1 alpha-2 country code *)
64+ val make
65+ : ?address:string
66+ -> ?city:string
67+ -> ?region:string
68+ -> ?post_code:string
69+ -> ?country:string
70+ -> unit
71+ -> t
72000000073 (** Create an address from option values directly.
7475 Used internally by jsont decoders where fields are decoded as options. *)
76+ val of_options
77+ : address:string option
78+ -> city:string option
79+ -> region:string option
80+ -> post_code:string option
81+ -> country:string option
82+ -> t
83084 (** Street address (e.g., ["77 Massachusetts Avenue"]). *)
85+ val address : t -> string option
86087 (** City name (e.g., ["Cambridge"], ["London"]). *)
88+ val city : t -> string option
89090 (** State, province, or region (e.g., ["Massachusetts"], ["Bavaria"]). *)
91+ val region : t -> string option
92093 (** Postal or ZIP code (e.g., ["02139"], ["W1A 1AA"]). *)
94+ val post_code : t -> string option
95096 (** ISO 3166-1 alpha-2 country code (e.g., ["US"], ["DE"], ["GB"]). *)
97+ val country : t -> string option
98099 (** [true] if all fields are [None]. *)
100+ val is_empty : t -> bool
1010102 (** Pretty-print the address. *)
103+ val pp : Format.formatter -> t -> unit
10400000105 (** Add address fields to a jsont object builder.
106107 This adds the five address fields (address, city, region, post-code,
···109 [string option] arguments in that order.
110111 @param get Extracts the address from the parent type for encoding *)
112+ val jsont_fields
113+ : get:('a -> t)
114+ -> ( 'a
115+ , string option
116+ -> string option
117+ -> string option
118+ -> string option
119+ -> string option
120+ -> 'b )
121+ Jsont.Object.map
122+ -> ('a, 'b) Jsont.Object.map
123end
124125(** Contact information.
···127 Electronic contact details for persons and entities. All fields
128 are optional. *)
129module Contact : sig
0130 (** Contact information record. *)
131+ type t
1320133 (** Empty contact with all fields [None]. *)
134+ val empty : t
1350000000136 (** Create contact information with optional fields.
137138 @param email Email address
···140 @param fax Fax number (any format)
141 @param website Website URL
142 @param orcid ORCID identifier URL *)
143+ val make
144+ : ?email:string
145+ -> ?tel:string
146+ -> ?fax:string
147+ -> ?website:string
148+ -> ?orcid:string
149+ -> unit
150+ -> t
1510000000152 (** Create contact info from option values directly.
153154 Used internally by jsont decoders where fields are decoded as options. *)
155+ val of_options
156+ : email:string option
157+ -> tel:string option
158+ -> fax:string option
159+ -> website:string option
160+ -> orcid:string option
161+ -> t
1620163 (** Email address (e.g., ["jane.smith\@example.org"]). *)
164+ val email : t -> string option
1650166 (** Telephone number. No specific format is required. *)
167+ val tel : t -> string option
1680169 (** Fax number. No specific format is required. *)
170+ val fax : t -> string option
1710172 (** Website URL (e.g., ["https://example.org/~jsmith"]). *)
173+ val website : t -> string option
1740175 (** ORCID identifier as a URL.
176177 ORCID (Open Researcher and Contributor ID) provides persistent
···180 Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]
181182 Example: ["https://orcid.org/0000-0001-2345-6789"] *)
183+ val orcid : t -> string option
1840185 (** [true] if all fields are [None]. *)
186+ val is_empty : t -> bool
1870188 (** Pretty-print the contact information. *)
189+ val pp : Format.formatter -> t -> unit
19000000191 (** Add contact fields to a jsont object builder.
192193 This adds the five contact fields (email, tel, fax, website, orcid)
···195 [string option] arguments in that order.
196197 @param get Extracts the contact from the parent type for encoding *)
198+ val jsont_fields
199+ : get:('a -> t)
200+ -> ( 'a
201+ , string option
202+ -> string option
203+ -> string option
204+ -> string option
205+ -> string option
206+ -> 'b )
207+ Jsont.Object.map
208+ -> ('a, 'b) Jsont.Object.map
209end
+189-166
lib/cff_author.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Person, Entity, and Author types for CFF. *)
78-(** Person name components. *)
9-module Name = struct
10- type t = {
11- family_names : string option;
12- given_names : string option;
13- name_particle : string option; (* e.g., "von" *)
14- name_suffix : string option; (* e.g., "Jr." *)
15- alias : string option;
16- }
0001718- let empty = {
19- family_names = None;
20- given_names = None;
21- name_particle = None;
22- name_suffix = None;
23- alias = None;
24- }
25-26- let make ?family_names ?given_names ?name_particle ?name_suffix ?alias () =
27- { family_names; given_names; name_particle; name_suffix; alias }
000000000002829 let family_names t = t.family_names
30 let given_names t = t.given_names
31 let name_particle t = t.name_particle
32 let name_suffix t = t.name_suffix
33 let alias t = t.alias
0003435 let full_name t =
36- let parts = List.filter_map Fun.id [
37- t.given_names;
38- t.name_particle;
39- t.family_names;
40- ] in
41 let base = String.concat " " parts in
42 match t.name_suffix with
43 | Some suffix -> base ^ ", " ^ suffix
44 | None -> base
45-46- let pp ppf t =
47- Format.pp_print_string ppf (full_name t)
48-end
49-50-(** A person (individual author/contributor). *)
51-module Person = struct
52- type t = {
53- name : Name.t;
54- affiliation : string option;
55- address : Cff_address.Address.t;
56- contact : Cff_address.Contact.t;
57- }
58-59- let make
60- ?family_names ?given_names ?name_particle ?name_suffix ?alias
61- ?affiliation
62- ?(address = Cff_address.Address.empty)
63- ?(contact = Cff_address.Contact.empty)
64- () =
65- let name = Name.make ?family_names ?given_names ?name_particle
66- ?name_suffix ?alias () in
67- { name; affiliation; address; contact }
68-69- let name t = t.name
70- let affiliation t = t.affiliation
71- let address t = t.address
72- let contact t = t.contact
73-74- let family_names t = Name.family_names t.name
75- let given_names t = Name.given_names t.name
76- let name_particle t = Name.name_particle t.name
77- let name_suffix t = Name.name_suffix t.name
78- let alias t = Name.alias t.name
79- let full_name t = Name.full_name t.name
8081 let email t = Cff_address.Contact.email t.contact
82 let orcid t = Cff_address.Contact.orcid t.contact
···85 let pp ppf t =
86 Format.fprintf ppf "%s" (full_name t);
87 Option.iter (Format.fprintf ppf " (%s)") t.affiliation
08889 let jsont =
90- Jsont.Object.map ~kind:"Person"
91- (fun family_names given_names name_particle name_suffix alias
92- affiliation address city region post_code country
93- email tel fax website orcid ->
94- let name = Name.make ?family_names ?given_names ?name_particle
95- ?name_suffix ?alias () in
96- let address = Cff_address.Address.of_options
97- ~address ~city ~region ~post_code ~country in
98- let contact = Cff_address.Contact.of_options
99- ~email ~tel ~fax ~website ~orcid in
100- { name; affiliation; address; contact })
101- |> Jsont.Object.opt_mem "family-names" Jsont.string
102- ~enc:(fun p -> Name.family_names p.name)
103- |> Jsont.Object.opt_mem "given-names" Jsont.string
104- ~enc:(fun p -> Name.given_names p.name)
105- |> Jsont.Object.opt_mem "name-particle" Jsont.string
106- ~enc:(fun p -> Name.name_particle p.name)
107- |> Jsont.Object.opt_mem "name-suffix" Jsont.string
108- ~enc:(fun p -> Name.name_suffix p.name)
109- |> Jsont.Object.opt_mem "alias" Jsont.string
110- ~enc:(fun p -> Name.alias p.name)
111- |> Jsont.Object.opt_mem "affiliation" Jsont.string
112- ~enc:(fun p -> p.affiliation)
0000000000000000113 |> Cff_address.Address.jsont_fields ~get:(fun p -> p.address)
114 |> Cff_address.Contact.jsont_fields ~get:(fun p -> p.contact)
115 |> Jsont.Object.skip_unknown
116 |> Jsont.Object.finish
117-end
118-119-(** Event dates for entities (e.g., conferences). *)
120-module Event_dates = struct
121- type t = {
122- date_start : Cff_date.t option;
123- date_end : Cff_date.t option;
124- }
125-126- let empty = { date_start = None; date_end = None }
127-128- let make ?date_start ?date_end () = { date_start; date_end }
129-130- let date_start t = t.date_start
131- let date_end t = t.date_end
132-133- let is_empty t = t.date_start = None && t.date_end = None
134-135- let pp ppf t =
136- match t.date_start, t.date_end with
137- | Some s, Some e ->
138- Format.fprintf ppf "%a - %a" Cff_date.pp s Cff_date.pp e
139- | Some s, None ->
140- Format.fprintf ppf "%a -" Cff_date.pp s
141- | None, Some e ->
142- Format.fprintf ppf "- %a" Cff_date.pp e
143- | None, None -> ()
144end
145146(** An entity (organization, team, conference, etc.). *)
147module Entity = struct
148- type t = {
149- name : string;
150- alias : string option;
151- address : Cff_address.Address.t;
152- contact : Cff_address.Contact.t;
153- event_dates : Event_dates.t;
154- location : string option;
155- }
0156157 let make
158- ~name ?alias
159- ?(address = Cff_address.Address.empty)
160- ?(contact = Cff_address.Contact.empty)
161- ?date_start ?date_end ?location
162- () =
163- let event_dates = Event_dates.make ?date_start ?date_end () in
164- { name; alias; address; contact; event_dates; location }
0000165166 let name t = t.name
167 let alias t = t.alias
168 let address t = t.address
169 let contact t = t.contact
170- let event_dates t = t.event_dates
0171 let location t = t.location
172-173 let email t = Cff_address.Contact.email t.contact
174 let orcid t = Cff_address.Contact.orcid t.contact
175 let website t = Cff_address.Contact.website t.contact
···177 let pp ppf t =
178 Format.pp_print_string ppf t.name;
179 Option.iter (Format.fprintf ppf " (%s)") t.alias
0180181 let jsont =
182- Jsont.Object.map ~kind:"Entity"
183- (fun name alias address city region post_code country
184- email tel fax website orcid date_start date_end location ->
185- let address = Cff_address.Address.of_options
186- ~address ~city ~region ~post_code ~country in
187- let contact = Cff_address.Contact.of_options
188- ~email ~tel ~fax ~website ~orcid in
189- let event_dates = Event_dates.make ?date_start ?date_end () in
190- { name; alias; address; contact; event_dates; location })
191- |> Jsont.Object.mem "name" Jsont.string
192- ~enc:(fun e -> e.name)
193- |> Jsont.Object.opt_mem "alias" Jsont.string
194- ~enc:(fun e -> e.alias)
0000000000000195 |> Cff_address.Address.jsont_fields ~get:(fun e -> e.address)
196 |> Cff_address.Contact.jsont_fields ~get:(fun e -> e.contact)
197- |> Jsont.Object.opt_mem "date-start" Cff_date.jsont
198- ~enc:(fun e -> Event_dates.date_start e.event_dates)
199- |> Jsont.Object.opt_mem "date-end" Cff_date.jsont
200- ~enc:(fun e -> Event_dates.date_end e.event_dates)
201- |> Jsont.Object.opt_mem "location" Jsont.string
202- ~enc:(fun e -> e.location)
203 |> Jsont.Object.skip_unknown
204 |> Jsont.Object.finish
0205end
206207(** An author can be either a Person or an Entity. *)
208type t =
209- | Person of Person.t
210- | Entity of Entity.t
0211212-let person p = Person p
213-let entity e = Entity e
0000000000000000000000000214215let name = function
216- | Person p -> Person.full_name p
217- | Entity e -> Entity.name e
0218219let orcid = function
220- | Person p -> Person.orcid p
221- | Entity e -> Entity.orcid e
0222223let email = function
224- | Person p -> Person.email p
225- | Entity e -> Entity.email e
0226227let pp ppf = function
228- | Person p -> Person.pp ppf p
229- | Entity e -> Entity.pp ppf e
0230231(* Jsont codec that discriminates based on "name" field presence.
232 If "name" is present -> Entity, otherwise -> Person *)
···237 | _ -> false
238 in
239 let dec_json j =
240- if has_name_member j then
0241 match Jsont.Json.decode' Entity.jsont j with
242- | Ok e -> Entity e
243- | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err)
244- else
0245 match Jsont.Json.decode' Person.jsont j with
246- | Ok p -> Person p
247- | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err)
0248 in
249 let enc_author = function
250- | Person p ->
251 (match Jsont.Json.encode' Person.jsont p with
252 | Ok j -> j
253 | Error _ -> assert false)
254- | Entity e ->
255 (match Jsont.Json.encode' Entity.jsont e with
256 | Ok j -> j
257 | Error _ -> assert false)
258 in
259 Jsont.json |> Jsont.map ~dec:dec_json ~enc:enc_author
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Person, Entity, and Author types for CFF. *)
78+(** A person (individual author/contributor). *)
9+module Person = struct
10+ type t =
11+ { family_names : string option
12+ ; given_names : string option
13+ ; name_particle : string option
14+ ; name_suffix : string option
15+ ; alias : string option
16+ ; affiliation : string option
17+ ; address : Cff_address.Address.t
18+ ; contact : Cff_address.Contact.t
19+ }
2021+ let make
22+ ?family_names
23+ ?given_names
24+ ?name_particle
25+ ?name_suffix
26+ ?alias
27+ ?affiliation
28+ ?(address = Cff_address.Address.empty)
29+ ?(contact = Cff_address.Contact.empty)
30+ ()
31+ =
32+ { family_names
33+ ; given_names
34+ ; name_particle
35+ ; name_suffix
36+ ; alias
37+ ; affiliation
38+ ; address
39+ ; contact
40+ }
41+ ;;
4243 let family_names t = t.family_names
44 let given_names t = t.given_names
45 let name_particle t = t.name_particle
46 let name_suffix t = t.name_suffix
47 let alias t = t.alias
48+ let affiliation t = t.affiliation
49+ let address t = t.address
50+ let contact t = t.contact
5152 let full_name t =
53+ let parts =
54+ List.filter_map Fun.id [ t.given_names; t.name_particle; t.family_names ]
55+ in
0056 let base = String.concat " " parts in
57 match t.name_suffix with
58 | Some suffix -> base ^ ", " ^ suffix
59 | None -> base
60+ ;;
00000000000000000000000000000000006162 let email t = Cff_address.Contact.email t.contact
63 let orcid t = Cff_address.Contact.orcid t.contact
···66 let pp ppf t =
67 Format.fprintf ppf "%s" (full_name t);
68 Option.iter (Format.fprintf ppf " (%s)") t.affiliation
69+ ;;
7071 let jsont =
72+ Jsont.Object.map
73+ ~kind:"Person"
74+ (fun
75+ family_names
76+ given_names
77+ name_particle
78+ name_suffix
79+ alias
80+ affiliation
81+ address
82+ city
83+ region
84+ post_code
85+ country
86+ email
87+ tel
88+ fax
89+ website
90+ orcid
91+ ->
92+ let address =
93+ Cff_address.Address.of_options ~address ~city ~region ~post_code ~country
94+ in
95+ let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in
96+ { family_names
97+ ; given_names
98+ ; name_particle
99+ ; name_suffix
100+ ; alias
101+ ; affiliation
102+ ; address
103+ ; contact
104+ })
105+ |> Jsont.Object.opt_mem "family-names" Jsont.string ~enc:(fun p -> p.family_names)
106+ |> Jsont.Object.opt_mem "given-names" Jsont.string ~enc:(fun p -> p.given_names)
107+ |> Jsont.Object.opt_mem "name-particle" Jsont.string ~enc:(fun p -> p.name_particle)
108+ |> Jsont.Object.opt_mem "name-suffix" Jsont.string ~enc:(fun p -> p.name_suffix)
109+ |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun p -> p.alias)
110+ |> Jsont.Object.opt_mem "affiliation" Jsont.string ~enc:(fun p -> p.affiliation)
111 |> Cff_address.Address.jsont_fields ~get:(fun p -> p.address)
112 |> Cff_address.Contact.jsont_fields ~get:(fun p -> p.contact)
113 |> Jsont.Object.skip_unknown
114 |> Jsont.Object.finish
115+ ;;
00000000000000000000000000116end
117118(** An entity (organization, team, conference, etc.). *)
119module Entity = struct
120+ type t =
121+ { name : string
122+ ; alias : string option
123+ ; address : Cff_address.Address.t
124+ ; contact : Cff_address.Contact.t
125+ ; date_start : Cff_date.t option
126+ ; date_end : Cff_date.t option
127+ ; location : string option
128+ }
129130 let make
131+ ~name
132+ ?alias
133+ ?(address = Cff_address.Address.empty)
134+ ?(contact = Cff_address.Contact.empty)
135+ ?date_start
136+ ?date_end
137+ ?location
138+ ()
139+ =
140+ { name; alias; address; contact; date_start; date_end; location }
141+ ;;
142143 let name t = t.name
144 let alias t = t.alias
145 let address t = t.address
146 let contact t = t.contact
147+ let date_start t = t.date_start
148+ let date_end t = t.date_end
149 let location t = t.location
0150 let email t = Cff_address.Contact.email t.contact
151 let orcid t = Cff_address.Contact.orcid t.contact
152 let website t = Cff_address.Contact.website t.contact
···154 let pp ppf t =
155 Format.pp_print_string ppf t.name;
156 Option.iter (Format.fprintf ppf " (%s)") t.alias
157+ ;;
158159 let jsont =
160+ Jsont.Object.map
161+ ~kind:"Entity"
162+ (fun
163+ name
164+ alias
165+ address
166+ city
167+ region
168+ post_code
169+ country
170+ email
171+ tel
172+ fax
173+ website
174+ orcid
175+ date_start
176+ date_end
177+ location
178+ ->
179+ let address =
180+ Cff_address.Address.of_options ~address ~city ~region ~post_code ~country
181+ in
182+ let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in
183+ { name; alias; address; contact; date_start; date_end; location })
184+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name)
185+ |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun e -> e.alias)
186 |> Cff_address.Address.jsont_fields ~get:(fun e -> e.address)
187 |> Cff_address.Contact.jsont_fields ~get:(fun e -> e.contact)
188+ |> Jsont.Object.opt_mem "date-start" Cff_date.jsont ~enc:(fun e -> e.date_start)
189+ |> Jsont.Object.opt_mem "date-end" Cff_date.jsont ~enc:(fun e -> e.date_end)
190+ |> Jsont.Object.opt_mem "location" Jsont.string ~enc:(fun e -> e.location)
000191 |> Jsont.Object.skip_unknown
192 |> Jsont.Object.finish
193+ ;;
194end
195196(** An author can be either a Person or an Entity. *)
197type t =
198+ [ `Person of Person.t
199+ | `Entity of Entity.t
200+ ]
201202+let person
203+ ?family_names
204+ ?given_names
205+ ?name_particle
206+ ?name_suffix
207+ ?alias
208+ ?affiliation
209+ ?address
210+ ?contact
211+ ()
212+ =
213+ `Person
214+ (Person.make
215+ ?family_names
216+ ?given_names
217+ ?name_particle
218+ ?name_suffix
219+ ?alias
220+ ?affiliation
221+ ?address
222+ ?contact
223+ ())
224+;;
225+226+let entity ~name ?alias ?address ?contact ?date_start ?date_end ?location () =
227+ `Entity (Entity.make ~name ?alias ?address ?contact ?date_start ?date_end ?location ())
228+;;
229230let name = function
231+ | `Person p -> Person.full_name p
232+ | `Entity e -> Entity.name e
233+;;
234235let orcid = function
236+ | `Person p -> Person.orcid p
237+ | `Entity e -> Entity.orcid e
238+;;
239240let email = function
241+ | `Person p -> Person.email p
242+ | `Entity e -> Entity.email e
243+;;
244245let pp ppf = function
246+ | `Person p -> Person.pp ppf p
247+ | `Entity e -> Entity.pp ppf e
248+;;
249250(* Jsont codec that discriminates based on "name" field presence.
251 If "name" is present -> Entity, otherwise -> Person *)
···256 | _ -> false
257 in
258 let dec_json j =
259+ if has_name_member j
260+ then (
261 match Jsont.Json.decode' Entity.jsont j with
262+ | Ok e -> `Entity e
263+ | Error err ->
264+ Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err))
265+ else (
266 match Jsont.Json.decode' Person.jsont j with
267+ | Ok p -> `Person p
268+ | Error err ->
269+ Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err))
270 in
271 let enc_author = function
272+ | `Person p ->
273 (match Jsont.Json.encode' Person.jsont p with
274 | Ok j -> j
275 | Error _ -> assert false)
276+ | `Entity e ->
277 (match Jsont.Json.encode' Entity.jsont e with
278 | Ok j -> j
279 | Error _ -> assert false)
280 in
281 Jsont.json |> Jsont.map ~dec:dec_json ~enc:enc_author
282+;;
+145-242
lib/cff_author.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···16 of a [name] field: if present, the entry is an entity; otherwise,
17 it's a person.
1819- {1 Name Components}
20-21- CFF follows academic citation conventions for person names:
22-23- - {b family-names}: Last name/surname (e.g., ["Smith"], ["van Rossum"])
24- - {b given-names}: First name(s) (e.g., ["Jane"], ["Guido"])
25- - {b name-particle}: Connector before family name (e.g., ["von"], ["van"], ["de"])
26- - {b name-suffix}: Generational suffix (e.g., ["Jr."], ["III"])
27- - {b alias}: Nickname or pseudonym
28-29- {1 Entity Types}
30-31- Entities can represent various organizations:
32-33- - Research institutions and universities
34- - Companies and corporations
35- - Government agencies
36- - Open source projects and communities
37- - Academic conferences (with date-start/date-end)
38- - Standards bodies
39-40- {1 Example}
4142 {[
43- (* A person author with contact info *)
44- let contact = Cff.Address.Contact.make
45- ~orcid:"https://orcid.org/0000-0001-2345-6789" () in
46- let jane = Cff.Author.Person (Cff.Author.Person.make
47 ~family_names:"Smith"
48- ~given_names:"Jane A."
49 ~affiliation:"MIT"
50- ~contact
51- ())
52-53- (* A person with name particle *)
54- let guido = Cff.Author.Person (Cff.Author.Person.make
55- ~family_names:"Rossum"
56- ~given_names:"Guido"
57- ~name_particle:"van"
58- ())
5960- (* An organization entity *)
61- let address = Cff.Address.Address.make
62- ~city:"San Francisco" ~country:"US" () in
63- let contact = Cff.Address.Contact.make
64- ~website:"https://mozilla.org" () in
65- let mozilla = Cff.Author.Entity (Cff.Author.Entity.make
66 ~name:"Mozilla Foundation"
67- ~address ~contact
68- ())
69-70- (* A conference entity with dates *)
71- let conf = Cff.Author.Entity (Cff.Author.Entity.make
72- ~name:"ICSE 2024"
73- ~date_start:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:14)
74- ~date_end:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:20)
75- ~location:"Lisbon, Portugal"
76- ())
77- ]}
78-79- {1 Name Components} *)
80-81-(** Name components for persons.
82-83- CFF name handling follows scholarly citation conventions to properly
84- represent names from various cultures and naming traditions. *)
85-module Name : sig
86- type t
87-88- val empty : t
89- (** Empty name with all components as [None]. *)
90-91- val make :
92- ?family_names:string ->
93- ?given_names:string ->
94- ?name_particle:string ->
95- ?name_suffix:string ->
96- ?alias:string ->
97- unit -> t
98- (** Create a name with optional components.
99-100- @param family_names Last name/surname
101- @param given_names First name(s)
102- @param name_particle Connector like ["von"], ["van"], ["de"]
103- @param name_suffix Generational suffix like ["Jr."], ["III"]
104- @param alias Nickname or pseudonym *)
105-106- val family_names : t -> string option
107- (** The person's family name (surname, last name). *)
108-109- val given_names : t -> string option
110- (** The person's given name(s) (first name, forenames). *)
111-112- val name_particle : t -> string option
113- (** Name connector appearing before family name.
114115- Examples: ["von"] in "Ludwig von Beethoven",
116- ["van"] in "Vincent van Gogh". *)
000117118- val name_suffix : t -> string option
119- (** Generational or honorary suffix.
120121- Examples: ["Jr."], ["Sr."], ["III"], ["PhD"]. *)
122-123- val alias : t -> string option
124- (** Nickname, pseudonym, or alternative name.
125-126- Example: ["Tim"] for "Timothy", ["DHH"] for "David Heinemeier Hansson". *)
127-128- val full_name : t -> string
129- (** Format name as "Given Particle Family, Suffix".
130-131- Examples:
132- - ["Jane Smith"]
133- - ["Guido van Rossum"]
134- - ["John Smith, Jr."] *)
135-136- val pp : Format.formatter -> t -> unit
137- (** Pretty-print the full name. *)
138-end
139-140-(** Individual person (author, contributor, editor, etc.).
141142 A person represents a human contributor with:
143- - Name components (required: at least family or given names)
144 - Optional affiliation (institution, company)
145 - Optional physical address
146 - Optional contact information (email, ORCID, website) *)
147module Person : sig
0148 type t
149150- val make :
151- ?family_names:string ->
152- ?given_names:string ->
153- ?name_particle:string ->
154- ?name_suffix:string ->
155- ?alias:string ->
156- ?affiliation:string ->
157- ?address:Cff_address.Address.t ->
158- ?contact:Cff_address.Contact.t ->
159- unit -> t
160 (** Create a person with optional fields.
161162 At minimum, provide [family_names] or [given_names].
163164 @param family_names Last name/surname
165 @param given_names First name(s)
166- @param name_particle Connector before family name
167- @param name_suffix Generational suffix
168 @param alias Nickname or pseudonym
169 @param affiliation Institution or organization name
170 @param address Physical address
171 @param contact Contact information (email, ORCID, website, etc.) *)
172-173- val name : t -> Name.t
174- (** The person's name components. *)
175-176- val affiliation : t -> string option
177- (** The person's institutional affiliation.
178-179- Example: ["Massachusetts Institute of Technology"]. *)
180-181- val address : t -> Cff_address.Address.t
182- (** Physical address information. *)
183-184- val contact : t -> Cff_address.Contact.t
185- (** Contact information (email, phone, web, ORCID). *)
186187- (** {2 Convenience Accessors for Name} *)
1880189 val family_names : t -> string option
190- (** Shortcut for [Name.family_names (name t)]. *)
1910192 val given_names : t -> string option
193- (** Shortcut for [Name.given_names (name t)]. *)
19400195 val name_particle : t -> string option
196- (** Shortcut for [Name.name_particle (name t)]. *)
19700198 val name_suffix : t -> string option
199- (** Shortcut for [Name.name_suffix (name t)]. *)
2000201 val alias : t -> string option
202- (** Shortcut for [Name.alias (name t)]. *)
20300204 val full_name : t -> string
205- (** Shortcut for [Name.full_name (name t)]. *)
000000000206207- (** {2 Convenience Accessors for Contact} *)
02080209 val email : t -> string option
210- (** The person's email address. *)
2110212 val orcid : t -> string option
213- (** The person's ORCID identifier URL.
214215- ORCID (Open Researcher and Contributor ID) provides persistent
216- digital identifiers for researchers. Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]. *)
217-218 val website : t -> string option
219- (** The person's website URL. *)
220221- val pp : Format.formatter -> t -> unit
0222 (** Pretty-print as "Full Name (affiliation)". *)
0223224- val jsont : t Jsont.t
225 (** JSON/YAML codec for person records. *)
0226end
227228-(** Event date range for entities like conferences.
229230- Some entities (particularly conferences) have associated dates
231- when they take place. *)
232-module Event_dates : sig
233- type t
234-235- val empty : t
236- (** Empty date range with both dates as [None]. *)
237-238- val make :
239- ?date_start:Cff_date.t ->
240- ?date_end:Cff_date.t ->
241- unit -> t
242- (** Create an event date range.
243-244- @param date_start When the event begins
245- @param date_end When the event ends *)
246-247- val date_start : t -> Cff_date.t option
248- (** The start date of the event. *)
249-250- val date_end : t -> Cff_date.t option
251- (** The end date of the event. *)
252-253- val is_empty : t -> bool
254- (** [true] if both dates are [None]. *)
255-256- val pp : Format.formatter -> t -> unit
257- (** Pretty-print as "YYYY-MM-DD - YYYY-MM-DD". *)
258-end
259-260-(** Organization, institution, project, or conference.
261262 An entity represents a non-person author or contributor, such as:
263 - Research institutions (["MIT"], ["CERN"])
264 - Companies (["Google"], ["Mozilla Foundation"])
265- - Government agencies (["NASA"], ["NIH"])
266 - Open source projects (["The Rust Project"])
267- - Academic conferences (["ICSE 2024"])
268- - Standards bodies (["IEEE"], ["W3C"])
269270 Entities are distinguished from persons in YAML by the presence
271- of a required [name] field (persons have [family-names]/[given-names]
272- instead). *)
273module Entity : sig
0274 type t
275276- val make :
277- name:string ->
278- ?alias:string ->
279- ?address:Cff_address.Address.t ->
280- ?contact:Cff_address.Contact.t ->
281- ?date_start:Cff_date.t ->
282- ?date_end:Cff_date.t ->
283- ?location:string ->
284- unit -> t
285 (** Create an entity.
286287 @param name The entity's official name (required)
···291 @param date_start Event start date (for conferences)
292 @param date_end Event end date (for conferences)
293 @param location Event location description *)
0000000000294000295 val name : t -> string
296- (** The entity's official name. This field distinguishes entities
297- from persons in the YAML format. *)
2980299 val alias : t -> string option
300- (** Short name, acronym, or alternative name.
301302- Example: ["MIT"] for "Massachusetts Institute of Technology". *)
3030304 val address : t -> Cff_address.Address.t
305- (** Physical address information. *)
00306307- val contact : t -> Cff_address.Contact.t
308- (** Contact information. *)
309310- val event_dates : t -> Event_dates.t
311- (** Event dates (for conferences). *)
312313- val location : t -> string option
314- (** Event location description (for conferences).
315316- Example: ["Lisbon, Portugal"]. *)
317318- (** {2 Convenience Accessors for Contact} *)
0319320- val email : t -> string option
321 (** The entity's contact email. *)
03220323 val orcid : t -> string option
324- (** The entity's ORCID (organizations can have ORCIDs). *)
325326- val website : t -> string option
327 (** The entity's official website URL. *)
0328329- val pp : Format.formatter -> t -> unit
0330 (** Pretty-print as "Name (alias)". *)
0331332- val jsont : t Jsont.t
333 (** JSON/YAML codec for entity records. *)
0334end
335336-(** {1 Author Discriminated Union}
337338- The main author type is a sum type that can hold either a person
339- or an entity. This matches the CFF specification where authors
340- can be either individuals or organizations. *)
341342-type t =
343- | Person of Person.t (** An individual person *)
344- | Entity of Entity.t (** An organization or entity *)
345(** An author: either a person or an entity. *)
0000346347-val person : Person.t -> t
348-(** Wrap a person as an author. *)
349350-val entity : Entity.t -> t
351-(** Wrap an entity as an author. *)
352353-val name : t -> string
0000000000000000000000000000354(** Get the display name.
355356 For persons, returns the full formatted name.
357 For entities, returns the entity name. *)
0358359-val orcid : t -> string option
360(** Get the ORCID if present. Works for both persons and entities. *)
03610362val email : t -> string option
363-(** Get the email if present. Works for both persons and entities. *)
364365-val pp : Format.formatter -> t -> unit
0366(** Pretty-print the author. *)
0367368-val jsont : t Jsont.t
369(** JSON/YAML codec that discriminates based on [name] field presence.
370371 When decoding:
372 - If the object has a [name] field -> Entity
373- - Otherwise -> Person
374-375- This matches the CFF specification where entities are distinguished
376- by having a [name] field while persons have [family-names] and
377- [given-names] fields. *)
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···16 of a [name] field: if present, the entry is an entity; otherwise,
17 it's a person.
1819+ {1 Quick Example}
0000000000000000000002021 {[
22+ (* Create a person author *)
23+ let jane = Cff.Author.person
0024 ~family_names:"Smith"
25+ ~given_names:"Jane"
26 ~affiliation:"MIT"
27+ ()
000000002829+ (* Create an entity author *)
30+ let mozilla = Cff.Author.entity
000031 ~name:"Mozilla Foundation"
32+ ()
00000000000000000000000000000000000000000000003334+ (* Pattern match on authors *)
35+ let show_author = function
36+ | `Person p -> Cff.Author.Person.full_name p
37+ | `Entity e -> Cff.Author.Entity.name e
38+ ]} *)
3940+(** {1 Person}
04142+ Individual person (author, contributor, editor, etc.).
00000000000000000004344 A person represents a human contributor with:
45+ - Name components (family names, given names, particle, suffix, alias)
46 - Optional affiliation (institution, company)
47 - Optional physical address
48 - Optional contact information (email, ORCID, website) *)
49module Person : sig
50+ (** A person record. *)
51 type t
52000000000053 (** Create a person with optional fields.
5455 At minimum, provide [family_names] or [given_names].
5657 @param family_names Last name/surname
58 @param given_names First name(s)
59+ @param name_particle Connector before family name (e.g., "von", "van")
60+ @param name_suffix Generational suffix (e.g., "Jr.", "III")
61 @param alias Nickname or pseudonym
62 @param affiliation Institution or organization name
63 @param address Physical address
64 @param contact Contact information (email, ORCID, website, etc.) *)
65+ val make
66+ : ?family_names:string
67+ -> ?given_names:string
68+ -> ?name_particle:string
69+ -> ?name_suffix:string
70+ -> ?alias:string
71+ -> ?affiliation:string
72+ -> ?address:Cff_address.Address.t
73+ -> ?contact:Cff_address.Contact.t
74+ -> unit
75+ -> t
0007677+ (** {2 Name Fields} *)
7879+ (** The person's family name (surname, last name). *)
80 val family_names : t -> string option
08182+ (** The person's given name(s) (first name, forenames). *)
83 val given_names : t -> string option
08485+ (** Name connector appearing before family name.
86+ Examples: ["von"] in "Ludwig von Beethoven". *)
87 val name_particle : t -> string option
08889+ (** Generational or honorary suffix.
90+ Examples: ["Jr."], ["Sr."], ["III"]. *)
91 val name_suffix : t -> string option
09293+ (** Nickname, pseudonym, or alternative name. *)
94 val alias : t -> string option
09596+ (** Format name as "Given Particle Family, Suffix".
97+ Examples: ["Jane Smith"], ["Guido van Rossum"]. *)
98 val full_name : t -> string
99+100+ (** {2 Affiliation and Location} *)
101+102+ (** The person's institutional affiliation. *)
103+ val affiliation : t -> string option
104+105+ (** Physical address information. *)
106+ val address : t -> Cff_address.Address.t
107+108+ (** {2 Contact Information} *)
109110+ (** Full contact information record. *)
111+ val contact : t -> Cff_address.Contact.t
112113+ (** The person's email address. *)
114 val email : t -> string option
0115116+ (** The person's ORCID identifier URL. *)
117 val orcid : t -> string option
0118119+ (** The person's website URL. *)
00120 val website : t -> string option
0121122+ (** {2 Formatting and Codec} *)
123+124 (** Pretty-print as "Full Name (affiliation)". *)
125+ val pp : Format.formatter -> t -> unit
1260127 (** JSON/YAML codec for person records. *)
128+ val jsont : t Jsont.t
129end
130131+(** {1 Entity}
132133+ Organization, institution, project, or conference.
000000000000000000000000000000134135 An entity represents a non-person author or contributor, such as:
136 - Research institutions (["MIT"], ["CERN"])
137 - Companies (["Google"], ["Mozilla Foundation"])
0138 - Open source projects (["The Rust Project"])
139+ - Academic conferences (["ICSE 2024"]) with dates
0140141 Entities are distinguished from persons in YAML by the presence
142+ of a required [name] field. *)
0143module Entity : sig
144+ (** An entity record. *)
145 type t
146000000000147 (** Create an entity.
148149 @param name The entity's official name (required)
···153 @param date_start Event start date (for conferences)
154 @param date_end Event end date (for conferences)
155 @param location Event location description *)
156+ val make
157+ : name:string
158+ -> ?alias:string
159+ -> ?address:Cff_address.Address.t
160+ -> ?contact:Cff_address.Contact.t
161+ -> ?date_start:Cff_date.t
162+ -> ?date_end:Cff_date.t
163+ -> ?location:string
164+ -> unit
165+ -> t
166167+ (** {2 Core Fields} *)
168+169+ (** The entity's official name. *)
170 val name : t -> string
00171172+ (** Short name, acronym, or alternative name. *)
173 val alias : t -> string option
0174175+ (** {2 Location} *)
176177+ (** Physical address information. *)
178 val address : t -> Cff_address.Address.t
179+180+ (** Event location description (for conferences). *)
181+ val location : t -> string option
182183+ (** {2 Event Dates} *)
0184185+ (** The start date of the event (for conferences). *)
186+ val date_start : t -> Cff_date.t option
187188+ (** The end date of the event (for conferences). *)
189+ val date_end : t -> Cff_date.t option
190191+ (** {2 Contact Information} *)
192193+ (** Full contact information record. *)
194+ val contact : t -> Cff_address.Contact.t
1950196 (** The entity's contact email. *)
197+ val email : t -> string option
198199+ (** The entity's ORCID (organizations can have ORCIDs). *)
200 val orcid : t -> string option
02010202 (** The entity's official website URL. *)
203+ val website : t -> string option
204205+ (** {2 Formatting and Codec} *)
206+207 (** Pretty-print as "Name (alias)". *)
208+ val pp : Format.formatter -> t -> unit
2090210 (** JSON/YAML codec for entity records. *)
211+ val jsont : t Jsont.t
212end
213214+(** {1 Author Type}
215216+ The main author type is a polymorphic variant that can hold either
217+ a person or an entity. *)
0218000219(** An author: either a person or an entity. *)
220+type t =
221+ [ `Person of Person.t
222+ | `Entity of Entity.t
223+ ]
224225+(** {2 Smart Constructors} *)
0226227+(** Create a person author directly.
0228229+ Equivalent to [`Person (Person.make ...)]. *)
230+val person
231+ : ?family_names:string
232+ -> ?given_names:string
233+ -> ?name_particle:string
234+ -> ?name_suffix:string
235+ -> ?alias:string
236+ -> ?affiliation:string
237+ -> ?address:Cff_address.Address.t
238+ -> ?contact:Cff_address.Contact.t
239+ -> unit
240+ -> t
241+242+(** Create an entity author directly.
243+244+ Equivalent to [`Entity (Entity.make ...)]. *)
245+val entity
246+ : name:string
247+ -> ?alias:string
248+ -> ?address:Cff_address.Address.t
249+ -> ?contact:Cff_address.Contact.t
250+ -> ?date_start:Cff_date.t
251+ -> ?date_end:Cff_date.t
252+ -> ?location:string
253+ -> unit
254+ -> t
255+256+(** {2 Common Accessors} *)
257+258(** Get the display name.
259260 For persons, returns the full formatted name.
261 For entities, returns the entity name. *)
262+val name : t -> string
2630264(** Get the ORCID if present. Works for both persons and entities. *)
265+val orcid : t -> string option
266267+(** Get the email if present. Works for both persons and entities. *)
268val email : t -> string option
0269270+(** {2 Formatting and Codec} *)
271+272(** Pretty-print the author. *)
273+val pp : Format.formatter -> t -> unit
2740275(** JSON/YAML codec that discriminates based on [name] field presence.
276277 When decoding:
278 - If the object has a [name] field -> Entity
279+ - Otherwise -> Person *)
280+val jsont : t Jsont.t
000
+10-13
lib/cff_country.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···13 try
14 let _ = ISO3166.alpha2_of_string s in
15 Ok s
16- with Invalid_argument _ ->
17- Error (`Invalid_country s)
01819let to_string t = t
2021let to_iso3166 t =
22- try
23- Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t))
24- with Invalid_argument _ ->
25- None
2627let name t = Option.map ISO3166.Country.name (to_iso3166 t)
28-29let equal = String.equal
30let compare = String.compare
31-32-let pp ppf t =
33- Format.pp_print_string ppf t
3435(* Jsont codec for country codes *)
36let jsont =
···41 Jsont.Error.msgf Jsont.Meta.none "Invalid ISO 3166-1 alpha-2 country code: %s" s
42 in
43 let enc t = to_string t in
44- Jsont.string
45- |> Jsont.map ~dec ~enc
4647(* Lenient codec that accepts any string *)
48let jsont_lenient = Jsont.string
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···13 try
14 let _ = ISO3166.alpha2_of_string s in
15 Ok s
16+ with
17+ | Invalid_argument _ -> Error (`Invalid_country s)
18+;;
1920let to_string t = t
2122let to_iso3166 t =
23+ try Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t)) with
24+ | Invalid_argument _ -> None
25+;;
02627let name t = Option.map ISO3166.Country.name (to_iso3166 t)
028let equal = String.equal
29let compare = String.compare
30+let pp ppf t = Format.pp_print_string ppf t
003132(* Jsont codec for country codes *)
33let jsont =
···38 Jsont.Error.msgf Jsont.Meta.none "Invalid ISO 3166-1 alpha-2 country code: %s" s
39 in
40 let enc t = to_string t in
41+ Jsont.string |> Jsont.map ~dec ~enc
42+;;
4344(* Lenient codec that accepts any string *)
45let jsont_lenient = Jsont.string
+11-11
lib/cff_country.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···39 country: DE
40 ]} *)
4142-type t = string
43(** An ISO 3166-1 alpha-2 country code (two uppercase letters). *)
04445-val of_string : string -> (t, [> `Invalid_country of string]) result
46(** Parse and validate a country code.
4748 Case-insensitive: ["us"], ["US"], and ["Us"] all produce ["US"].
49 Returns [Error (`Invalid_country s)] for unknown codes. *)
050051val to_string : t -> string
52-(** Return the uppercase country code. *)
5354-val to_iso3166 : t -> ISO3166.Country.t option
55(** Look up the full country record from {!ISO3166}.
5657 Returns [None] if the code is not in the ISO 3166-1 list. *)
05859-val name : t -> string option
60(** Get the country name if the code is valid.
6162 Examples:
63 - [name "US" = Some "United States of America"]
64 - [name "GB" = Some "United Kingdom of Great Britain and Northern Ireland"]
65 - [name "XX" = None] *)
066067val equal : t -> t -> bool
68-(** Country code equality (case-sensitive after normalization). *)
6970-val compare : t -> t -> int
71(** Alphabetical comparison of country codes. *)
072073val pp : Format.formatter -> t -> unit
74-(** Pretty-print the country code. *)
7576-val jsont : t Jsont.t
77(** JSON/YAML codec that validates country codes.
7879 Returns an error for invalid ISO 3166-1 alpha-2 codes. *)
08081-val jsont_lenient : t Jsont.t
82(** JSON/YAML codec that accepts any string.
8384 Use this when parsing CFF files that may contain non-standard
85 country codes. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···39 country: DE
40 ]} *)
41042(** An ISO 3166-1 alpha-2 country code (two uppercase letters). *)
43+type t = string
44045(** Parse and validate a country code.
4647 Case-insensitive: ["us"], ["US"], and ["Us"] all produce ["US"].
48 Returns [Error (`Invalid_country s)] for unknown codes. *)
49+val of_string : string -> (t, [> `Invalid_country of string ]) result
5051+(** Return the uppercase country code. *)
52val to_string : t -> string
053054(** Look up the full country record from {!ISO3166}.
5556 Returns [None] if the code is not in the ISO 3166-1 list. *)
57+val to_iso3166 : t -> ISO3166.Country.t option
58059(** Get the country name if the code is valid.
6061 Examples:
62 - [name "US" = Some "United States of America"]
63 - [name "GB" = Some "United Kingdom of Great Britain and Northern Ireland"]
64 - [name "XX" = None] *)
65+val name : t -> string option
6667+(** Country code equality (case-sensitive after normalization). *)
68val equal : t -> t -> bool
069070(** Alphabetical comparison of country codes. *)
71+val compare : t -> t -> int
7273+(** Pretty-print the country code. *)
74val pp : Format.formatter -> t -> unit
075076(** JSON/YAML codec that validates country codes.
7778 Returns an error for invalid ISO 3166-1 alpha-2 codes. *)
79+val jsont : t Jsont.t
80081(** JSON/YAML codec that accepts any string.
8283 Use this when parsing CFF files that may contain non-standard
84 country codes. *)
85+val jsont_lenient : t Jsont.t
+10-17
lib/cff_date.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···10let of_string s =
11 (* CFF dates are YYYY-MM-DD format *)
12 match String.split_on_char '-' s with
13- | [y; m; d] ->
14 (match int_of_string_opt y, int_of_string_opt m, int_of_string_opt d with
15 | Some year, Some month, Some day ->
16 (* Validate the date components *)
17- if year >= 0 && year <= 9999 &&
18- month >= 1 && month <= 12 &&
19- day >= 1 && day <= 31 then
20- Ok (year, month, day)
21- else
22- Error (`Invalid_date s)
23 | _ -> Error (`Invalid_date s))
24 | _ -> Error (`Invalid_date s)
02526-let to_string (year, month, day) =
27- Printf.sprintf "%04d-%02d-%02d" year month day
28-29let year (y, _, _) = y
30let month (_, m, _) = m
31let day (_, _, d) = d
32-33let equal a b = a = b
34let compare = Stdlib.compare
35-36-let pp ppf date =
37- Format.pp_print_string ppf (to_string date)
3839(* Jsont codec for dates *)
40let jsont =
···45 Jsont.Error.msgf Jsont.Meta.none "Invalid date format: %s" s
46 in
47 let enc date = to_string date in
48- Jsont.string
49- |> Jsont.map ~dec ~enc
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···10let of_string s =
11 (* CFF dates are YYYY-MM-DD format *)
12 match String.split_on_char '-' s with
13+ | [ y; m; d ] ->
14 (match int_of_string_opt y, int_of_string_opt m, int_of_string_opt d with
15 | Some year, Some month, Some day ->
16 (* Validate the date components *)
17+ if year >= 0 && year <= 9999 && month >= 1 && month <= 12 && day >= 1 && day <= 31
18+ then Ok (year, month, day)
19+ else Error (`Invalid_date s)
00020 | _ -> Error (`Invalid_date s))
21 | _ -> Error (`Invalid_date s)
22+;;
2324+let to_string (year, month, day) = Printf.sprintf "%04d-%02d-%02d" year month day
0025let year (y, _, _) = y
26let month (_, m, _) = m
27let day (_, _, d) = d
028let equal a b = a = b
29let compare = Stdlib.compare
30+let pp ppf date = Format.pp_print_string ppf (to_string date)
003132(* Jsont codec for dates *)
33let jsont =
···38 Jsont.Error.msgf Jsont.Meta.none "Invalid date format: %s" s
39 in
40 let enc date = to_string date in
41+ Jsont.string |> Jsont.map ~dec ~enc
42+;;
+11-11
lib/cff_date.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···45 For historical works or when only the year is known, use the [year]
46 field (an integer) instead of a full date. *)
4748-type t = Ptime.date
49(** A date as [(year, month, day)] tuple.
5051 The tuple contains:
52 - [year]: Four-digit year (e.g., [2024])
53 - [month]: Month number (1-12)
54 - [day]: Day of month (1-31) *)
05556-val of_string : string -> (t, [> `Invalid_date of string]) result
57(** Parse a date from [YYYY-MM-DD] format.
5859 Returns [Error (`Invalid_date s)] if the string is not a valid date.
60 Validates that the date is a real calendar date (e.g., rejects Feb 30). *)
06162-val to_string : t -> string
63(** Format a date as [YYYY-MM-DD]. *)
064065val year : t -> int
66-(** Extract the year component. *)
6768-val month : t -> int
69(** Extract the month component (1-12). *)
07071-val day : t -> int
72(** Extract the day component (1-31). *)
073074val equal : t -> t -> bool
75-(** Date equality. *)
7677-val compare : t -> t -> int
78(** Date comparison (chronological order). *)
079080val pp : Format.formatter -> t -> unit
81-(** Pretty-print a date in [YYYY-MM-DD] format. *)
8283-val jsont : t Jsont.t
84(** JSON/YAML codec for dates.
8586 Parses strings in [YYYY-MM-DD] format and serializes back to the
87 same format. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···45 For historical works or when only the year is known, use the [year]
46 field (an integer) instead of a full date. *)
47048(** A date as [(year, month, day)] tuple.
4950 The tuple contains:
51 - [year]: Four-digit year (e.g., [2024])
52 - [month]: Month number (1-12)
53 - [day]: Day of month (1-31) *)
54+type t = Ptime.date
55056(** Parse a date from [YYYY-MM-DD] format.
5758 Returns [Error (`Invalid_date s)] if the string is not a valid date.
59 Validates that the date is a real calendar date (e.g., rejects Feb 30). *)
60+val of_string : string -> (t, [> `Invalid_date of string ]) result
61062(** Format a date as [YYYY-MM-DD]. *)
63+val to_string : t -> string
6465+(** Extract the year component. *)
66val year : t -> int
067068(** Extract the month component (1-12). *)
69+val month : t -> int
70071(** Extract the day component (1-31). *)
72+val day : t -> int
7374+(** Date equality. *)
75val equal : t -> t -> bool
076077(** Date comparison (chronological order). *)
78+val compare : t -> t -> int
7980+(** Pretty-print a date in [YYYY-MM-DD] format. *)
81val pp : Format.formatter -> t -> unit
082083(** JSON/YAML codec for dates.
8485 Parses strings in [YYYY-MM-DD] format and serializes back to the
86 same format. *)
87+val jsont : t Jsont.t
+229-204
lib/cff_enums.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8(** Functor to generate common enum operations. *)
9module type STRING_ENUM = sig
10 type t
011 val of_string : string -> t option
12 val to_string : t -> string
13 val type_name : string
···1516module Make_enum (E : STRING_ENUM) = struct
17 include E
018 let equal (a : t) (b : t) = a = b
19 let compare = Stdlib.compare
20 let pp ppf t = Format.pp_print_string ppf (to_string t)
021 let jsont =
22- Jsont.string |> Jsont.map
23- ~dec:(fun s ->
24- match of_string s with
25- | Some t -> t
26- | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s)
27- ~enc:to_string
0028end
2930module Identifier_type = Make_enum (struct
31- type t = [ `Doi | `Url | `Swh | `Other ]
32- let type_name = "identifier type"
0000003334- let of_string = function
35- | "doi" -> Some `Doi
36- | "url" -> Some `Url
37- | "swh" -> Some `Swh
38- | "other" -> Some `Other
39- | _ -> None
04041- let to_string = function
42- | `Doi -> "doi"
43- | `Url -> "url"
44- | `Swh -> "swh"
45- | `Other -> "other"
46-end)
04748module Reference_type = Make_enum (struct
49- type t = [
50- | `Art
51- | `Article
52- | `Audiovisual
53- | `Bill
54- | `Blog
55- | `Book
56- | `Catalogue
57- | `Conference
58- | `Conference_paper
59- | `Data
60- | `Database
61- | `Dictionary
62- | `Edited_work
63- | `Encyclopedia
64- | `Film_broadcast
65- | `Generic
66- | `Government_document
67- | `Grant
68- | `Hearing
69- | `Historical_work
70- | `Legal_case
71- | `Legal_rule
72- | `Magazine_article
73- | `Manual
74- | `Map
75- | `Multimedia
76- | `Music
77- | `Newspaper_article
78- | `Pamphlet
79- | `Patent
80- | `Personal_communication
81- | `Proceedings
82- | `Report
83- | `Serial
84- | `Slides
85- | `Software
86- | `Software_code
87- | `Software_container
88- | `Software_executable
89- | `Software_virtual_machine
90- | `Sound_recording
91- | `Standard
92- | `Statute
93- | `Thesis
94- | `Unpublished
95- | `Video
96- | `Website
97- ]
98- let type_name = "reference type"
099100- let of_string = function
101- | "art" -> Some `Art
102- | "article" -> Some `Article
103- | "audiovisual" -> Some `Audiovisual
104- | "bill" -> Some `Bill
105- | "blog" -> Some `Blog
106- | "book" -> Some `Book
107- | "catalogue" -> Some `Catalogue
108- | "conference" -> Some `Conference
109- | "conference-paper" -> Some `Conference_paper
110- | "data" -> Some `Data
111- | "database" -> Some `Database
112- | "dictionary" -> Some `Dictionary
113- | "edited-work" -> Some `Edited_work
114- | "encyclopedia" -> Some `Encyclopedia
115- | "film-broadcast" -> Some `Film_broadcast
116- | "generic" -> Some `Generic
117- | "government-document" -> Some `Government_document
118- | "grant" -> Some `Grant
119- | "hearing" -> Some `Hearing
120- | "historical-work" -> Some `Historical_work
121- | "legal-case" -> Some `Legal_case
122- | "legal-rule" -> Some `Legal_rule
123- | "magazine-article" -> Some `Magazine_article
124- | "manual" -> Some `Manual
125- | "map" -> Some `Map
126- | "multimedia" -> Some `Multimedia
127- | "music" -> Some `Music
128- | "newspaper-article" -> Some `Newspaper_article
129- | "pamphlet" -> Some `Pamphlet
130- | "patent" -> Some `Patent
131- | "personal-communication" -> Some `Personal_communication
132- | "proceedings" -> Some `Proceedings
133- | "report" -> Some `Report
134- | "serial" -> Some `Serial
135- | "slides" -> Some `Slides
136- | "software" -> Some `Software
137- | "software-code" -> Some `Software_code
138- | "software-container" -> Some `Software_container
139- | "software-executable" -> Some `Software_executable
140- | "software-virtual-machine" -> Some `Software_virtual_machine
141- | "sound-recording" -> Some `Sound_recording
142- | "standard" -> Some `Standard
143- | "statute" -> Some `Statute
144- | "thesis" -> Some `Thesis
145- | "unpublished" -> Some `Unpublished
146- | "video" -> Some `Video
147- | "website" -> Some `Website
148- | _ -> None
0149150- let to_string = function
151- | `Art -> "art"
152- | `Article -> "article"
153- | `Audiovisual -> "audiovisual"
154- | `Bill -> "bill"
155- | `Blog -> "blog"
156- | `Book -> "book"
157- | `Catalogue -> "catalogue"
158- | `Conference -> "conference"
159- | `Conference_paper -> "conference-paper"
160- | `Data -> "data"
161- | `Database -> "database"
162- | `Dictionary -> "dictionary"
163- | `Edited_work -> "edited-work"
164- | `Encyclopedia -> "encyclopedia"
165- | `Film_broadcast -> "film-broadcast"
166- | `Generic -> "generic"
167- | `Government_document -> "government-document"
168- | `Grant -> "grant"
169- | `Hearing -> "hearing"
170- | `Historical_work -> "historical-work"
171- | `Legal_case -> "legal-case"
172- | `Legal_rule -> "legal-rule"
173- | `Magazine_article -> "magazine-article"
174- | `Manual -> "manual"
175- | `Map -> "map"
176- | `Multimedia -> "multimedia"
177- | `Music -> "music"
178- | `Newspaper_article -> "newspaper-article"
179- | `Pamphlet -> "pamphlet"
180- | `Patent -> "patent"
181- | `Personal_communication -> "personal-communication"
182- | `Proceedings -> "proceedings"
183- | `Report -> "report"
184- | `Serial -> "serial"
185- | `Slides -> "slides"
186- | `Software -> "software"
187- | `Software_code -> "software-code"
188- | `Software_container -> "software-container"
189- | `Software_executable -> "software-executable"
190- | `Software_virtual_machine -> "software-virtual-machine"
191- | `Sound_recording -> "sound-recording"
192- | `Standard -> "standard"
193- | `Statute -> "statute"
194- | `Thesis -> "thesis"
195- | `Unpublished -> "unpublished"
196- | `Video -> "video"
197- | `Website -> "website"
198-end)
0199200module Status = Make_enum (struct
201- type t = [
202- | `Abstract
203- | `Advance_online
204- | `In_preparation
205- | `In_press
206- | `Preprint
207- | `Submitted
208- ]
209- let type_name = "status"
210211- let of_string = function
212- | "abstract" -> Some `Abstract
213- | "advance-online" -> Some `Advance_online
214- | "in-preparation" -> Some `In_preparation
215- | "in-press" -> Some `In_press
216- | "preprint" -> Some `Preprint
217- | "submitted" -> Some `Submitted
218- | _ -> None
219220- let to_string = function
221- | `Abstract -> "abstract"
222- | `Advance_online -> "advance-online"
223- | `In_preparation -> "in-preparation"
224- | `In_press -> "in-press"
225- | `Preprint -> "preprint"
226- | `Submitted -> "submitted"
227-end)
00000000000228229module Cff_type = Make_enum (struct
230- type t = [ `Software | `Dataset ]
231- let type_name = "CFF type"
00232233- let of_string = function
234- | "software" -> Some `Software
235- | "dataset" -> Some `Dataset
236- | _ -> None
000237238- let to_string = function
239- | `Software -> "software"
240- | `Dataset -> "dataset"
241-end)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8(** Functor to generate common enum operations. *)
9module type STRING_ENUM = sig
10 type t
11+12 val of_string : string -> t option
13 val to_string : t -> string
14 val type_name : string
···1617module Make_enum (E : STRING_ENUM) = struct
18 include E
19+20 let equal (a : t) (b : t) = a = b
21 let compare = Stdlib.compare
22 let pp ppf t = Format.pp_print_string ppf (to_string t)
23+24 let jsont =
25+ Jsont.string
26+ |> Jsont.map
27+ ~dec:(fun s ->
28+ match of_string s with
29+ | Some t -> t
30+ | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s)
31+ ~enc:to_string
32+ ;;
33end
3435module Identifier_type = Make_enum (struct
36+ type t =
37+ [ `Doi
38+ | `Url
39+ | `Swh
40+ | `Other
41+ ]
42+43+ let type_name = "identifier type"
4445+ let of_string = function
46+ | "doi" -> Some `Doi
47+ | "url" -> Some `Url
48+ | "swh" -> Some `Swh
49+ | "other" -> Some `Other
50+ | _ -> None
51+ ;;
5253+ let to_string = function
54+ | `Doi -> "doi"
55+ | `Url -> "url"
56+ | `Swh -> "swh"
57+ | `Other -> "other"
58+ ;;
59+ end)
6061module Reference_type = Make_enum (struct
62+ type t =
63+ [ `Art
64+ | `Article
65+ | `Audiovisual
66+ | `Bill
67+ | `Blog
68+ | `Book
69+ | `Catalogue
70+ | `Conference
71+ | `Conference_paper
72+ | `Data
73+ | `Database
74+ | `Dictionary
75+ | `Edited_work
76+ | `Encyclopedia
77+ | `Film_broadcast
78+ | `Generic
79+ | `Government_document
80+ | `Grant
81+ | `Hearing
82+ | `Historical_work
83+ | `Legal_case
84+ | `Legal_rule
85+ | `Magazine_article
86+ | `Manual
87+ | `Map
88+ | `Multimedia
89+ | `Music
90+ | `Newspaper_article
91+ | `Pamphlet
92+ | `Patent
93+ | `Personal_communication
94+ | `Proceedings
95+ | `Report
96+ | `Serial
97+ | `Slides
98+ | `Software
99+ | `Software_code
100+ | `Software_container
101+ | `Software_executable
102+ | `Software_virtual_machine
103+ | `Sound_recording
104+ | `Standard
105+ | `Statute
106+ | `Thesis
107+ | `Unpublished
108+ | `Video
109+ | `Website
110+ ]
111+112+ let type_name = "reference type"
113114+ let of_string = function
115+ | "art" -> Some `Art
116+ | "article" -> Some `Article
117+ | "audiovisual" -> Some `Audiovisual
118+ | "bill" -> Some `Bill
119+ | "blog" -> Some `Blog
120+ | "book" -> Some `Book
121+ | "catalogue" -> Some `Catalogue
122+ | "conference" -> Some `Conference
123+ | "conference-paper" -> Some `Conference_paper
124+ | "data" -> Some `Data
125+ | "database" -> Some `Database
126+ | "dictionary" -> Some `Dictionary
127+ | "edited-work" -> Some `Edited_work
128+ | "encyclopedia" -> Some `Encyclopedia
129+ | "film-broadcast" -> Some `Film_broadcast
130+ | "generic" -> Some `Generic
131+ | "government-document" -> Some `Government_document
132+ | "grant" -> Some `Grant
133+ | "hearing" -> Some `Hearing
134+ | "historical-work" -> Some `Historical_work
135+ | "legal-case" -> Some `Legal_case
136+ | "legal-rule" -> Some `Legal_rule
137+ | "magazine-article" -> Some `Magazine_article
138+ | "manual" -> Some `Manual
139+ | "map" -> Some `Map
140+ | "multimedia" -> Some `Multimedia
141+ | "music" -> Some `Music
142+ | "newspaper-article" -> Some `Newspaper_article
143+ | "pamphlet" -> Some `Pamphlet
144+ | "patent" -> Some `Patent
145+ | "personal-communication" -> Some `Personal_communication
146+ | "proceedings" -> Some `Proceedings
147+ | "report" -> Some `Report
148+ | "serial" -> Some `Serial
149+ | "slides" -> Some `Slides
150+ | "software" -> Some `Software
151+ | "software-code" -> Some `Software_code
152+ | "software-container" -> Some `Software_container
153+ | "software-executable" -> Some `Software_executable
154+ | "software-virtual-machine" -> Some `Software_virtual_machine
155+ | "sound-recording" -> Some `Sound_recording
156+ | "standard" -> Some `Standard
157+ | "statute" -> Some `Statute
158+ | "thesis" -> Some `Thesis
159+ | "unpublished" -> Some `Unpublished
160+ | "video" -> Some `Video
161+ | "website" -> Some `Website
162+ | _ -> None
163+ ;;
164165+ let to_string = function
166+ | `Art -> "art"
167+ | `Article -> "article"
168+ | `Audiovisual -> "audiovisual"
169+ | `Bill -> "bill"
170+ | `Blog -> "blog"
171+ | `Book -> "book"
172+ | `Catalogue -> "catalogue"
173+ | `Conference -> "conference"
174+ | `Conference_paper -> "conference-paper"
175+ | `Data -> "data"
176+ | `Database -> "database"
177+ | `Dictionary -> "dictionary"
178+ | `Edited_work -> "edited-work"
179+ | `Encyclopedia -> "encyclopedia"
180+ | `Film_broadcast -> "film-broadcast"
181+ | `Generic -> "generic"
182+ | `Government_document -> "government-document"
183+ | `Grant -> "grant"
184+ | `Hearing -> "hearing"
185+ | `Historical_work -> "historical-work"
186+ | `Legal_case -> "legal-case"
187+ | `Legal_rule -> "legal-rule"
188+ | `Magazine_article -> "magazine-article"
189+ | `Manual -> "manual"
190+ | `Map -> "map"
191+ | `Multimedia -> "multimedia"
192+ | `Music -> "music"
193+ | `Newspaper_article -> "newspaper-article"
194+ | `Pamphlet -> "pamphlet"
195+ | `Patent -> "patent"
196+ | `Personal_communication -> "personal-communication"
197+ | `Proceedings -> "proceedings"
198+ | `Report -> "report"
199+ | `Serial -> "serial"
200+ | `Slides -> "slides"
201+ | `Software -> "software"
202+ | `Software_code -> "software-code"
203+ | `Software_container -> "software-container"
204+ | `Software_executable -> "software-executable"
205+ | `Software_virtual_machine -> "software-virtual-machine"
206+ | `Sound_recording -> "sound-recording"
207+ | `Standard -> "standard"
208+ | `Statute -> "statute"
209+ | `Thesis -> "thesis"
210+ | `Unpublished -> "unpublished"
211+ | `Video -> "video"
212+ | `Website -> "website"
213+ ;;
214+ end)
215216module Status = Make_enum (struct
217+ type t =
218+ [ `Abstract
219+ | `Advance_online
220+ | `In_preparation
221+ | `In_press
222+ | `Preprint
223+ | `Submitted
224+ ]
0225226+ let type_name = "status"
0000000227228+ let of_string = function
229+ | "abstract" -> Some `Abstract
230+ | "advance-online" -> Some `Advance_online
231+ | "in-preparation" -> Some `In_preparation
232+ | "in-press" -> Some `In_press
233+ | "preprint" -> Some `Preprint
234+ | "submitted" -> Some `Submitted
235+ | _ -> None
236+ ;;
237+238+ let to_string = function
239+ | `Abstract -> "abstract"
240+ | `Advance_online -> "advance-online"
241+ | `In_preparation -> "in-preparation"
242+ | `In_press -> "in-press"
243+ | `Preprint -> "preprint"
244+ | `Submitted -> "submitted"
245+ ;;
246+ end)
247248module Cff_type = Make_enum (struct
249+ type t =
250+ [ `Software
251+ | `Dataset
252+ ]
253254+ let type_name = "CFF type"
255+256+ let of_string = function
257+ | "software" -> Some `Software
258+ | "dataset" -> Some `Dataset
259+ | _ -> None
260+ ;;
261262+ let to_string = function
263+ | `Software -> "software"
264+ | `Dataset -> "dataset"
265+ ;;
266+ end)
+31-23
lib/cff_enums.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···48 description: Software Heritage archive
49 ]} *)
50module Identifier_type : sig
51- type t = [ `Doi | `Url | `Swh | `Other ]
52 (** Identifier types. *)
00000053054 val of_string : string -> t option
55- (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *)
5657- val to_string : t -> string
58 (** Convert to YAML string representation. *)
05960 val equal : t -> t -> bool
61 val compare : t -> t -> int
62 val pp : Format.formatter -> t -> unit
63064 val jsont : t Jsont.t
65- (** JSON/YAML codec. *)
66end
6768(** Reference type for bibliographic entries.
···138 - [`Standard] - Technical standard
139 - [`Unpublished] - Unpublished work *)
140module Reference_type : sig
141- type t = [
142- | `Art
0143 | `Article
144 | `Audiovisual
145 | `Bill
···186 | `Unpublished
187 | `Video
188 | `Website
189- ]
190- (** All supported reference types. *)
191192- val of_string : string -> t option
193 (** Parse from YAML string. Hyphenated names like ["conference-paper"]
194 map to underscored variants like [`Conference_paper]. *)
0195196- val to_string : t -> string
197 (** Convert to YAML string representation.
198 Underscored variants like [`Conference_paper] become ["conference-paper"]. *)
0199200 val equal : t -> t -> bool
201 val compare : t -> t -> int
202 val pp : Format.formatter -> t -> unit
203204- val jsont : t Jsont.t
205 (** JSON/YAML codec. *)
0206end
207208(** Publication status for works in progress.
···230 status: submitted
231 ]} *)
232module Status : sig
233- type t = [
234- | `Abstract
0235 | `Advance_online
236 | `In_preparation
237 | `In_press
238 | `Preprint
239 | `Submitted
240- ]
241- (** Publication status values. *)
242243- val of_string : string -> t option
244 (** Parse from YAML string: ["abstract"], ["advance-online"], etc. *)
02450246 val to_string : t -> string
247- (** Convert to YAML string representation. *)
248249 val equal : t -> t -> bool
250 val compare : t -> t -> int
251 val pp : Format.formatter -> t -> unit
252253- val jsont : t Jsont.t
254 (** JSON/YAML codec. *)
0255end
256257(** CFF file type: software or dataset.
···271 # ...
272 ]} *)
273module Cff_type : sig
274- type t = [ `Software | `Dataset ]
275 (** CFF file types. *)
0000276277- val of_string : string -> t option
278 (** Parse from YAML string: ["software"] or ["dataset"]. *)
0279280- val to_string : t -> string
281 (** Convert to YAML string representation. *)
0282283 val equal : t -> t -> bool
284 val compare : t -> t -> int
285 val pp : Format.formatter -> t -> unit
286287- val jsont : t Jsont.t
288 (** JSON/YAML codec. *)
0289end
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···48 description: Software Heritage archive
49 ]} *)
50module Identifier_type : sig
051 (** Identifier types. *)
52+ type t =
53+ [ `Doi
54+ | `Url
55+ | `Swh
56+ | `Other
57+ ]
5859+ (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *)
60 val of_string : string -> t option
061062 (** Convert to YAML string representation. *)
63+ val to_string : t -> string
6465 val equal : t -> t -> bool
66 val compare : t -> t -> int
67 val pp : Format.formatter -> t -> unit
6869+ (** JSON/YAML codec. *)
70 val jsont : t Jsont.t
071end
7273(** Reference type for bibliographic entries.
···143 - [`Standard] - Technical standard
144 - [`Unpublished] - Unpublished work *)
145module Reference_type : sig
146+ (** All supported reference types. *)
147+ type t =
148+ [ `Art
149 | `Article
150 | `Audiovisual
151 | `Bill
···192 | `Unpublished
193 | `Video
194 | `Website
195+ ]
01960197 (** Parse from YAML string. Hyphenated names like ["conference-paper"]
198 map to underscored variants like [`Conference_paper]. *)
199+ val of_string : string -> t option
2000201 (** Convert to YAML string representation.
202 Underscored variants like [`Conference_paper] become ["conference-paper"]. *)
203+ val to_string : t -> string
204205 val equal : t -> t -> bool
206 val compare : t -> t -> int
207 val pp : Format.formatter -> t -> unit
2080209 (** JSON/YAML codec. *)
210+ val jsont : t Jsont.t
211end
212213(** Publication status for works in progress.
···235 status: submitted
236 ]} *)
237module Status : sig
238+ (** Publication status values. *)
239+ type t =
240+ [ `Abstract
241 | `Advance_online
242 | `In_preparation
243 | `In_press
244 | `Preprint
245 | `Submitted
246+ ]
02470248 (** Parse from YAML string: ["abstract"], ["advance-online"], etc. *)
249+ val of_string : string -> t option
250251+ (** Convert to YAML string representation. *)
252 val to_string : t -> string
0253254 val equal : t -> t -> bool
255 val compare : t -> t -> int
256 val pp : Format.formatter -> t -> unit
2570258 (** JSON/YAML codec. *)
259+ val jsont : t Jsont.t
260end
261262(** CFF file type: software or dataset.
···276 # ...
277 ]} *)
278module Cff_type : sig
0279 (** CFF file types. *)
280+ type t =
281+ [ `Software
282+ | `Dataset
283+ ]
2840285 (** Parse from YAML string: ["software"] or ["dataset"]. *)
286+ val of_string : string -> t option
2870288 (** Convert to YAML string representation. *)
289+ val to_string : t -> string
290291 val equal : t -> t -> bool
292 val compare : t -> t -> int
293 val pp : Format.formatter -> t -> unit
2940295 (** JSON/YAML codec. *)
296+ val jsont : t Jsont.t
297end
+17-23
lib/cff_identifier.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Identifier type for CFF. *)
78-type t = {
9- type_ : Cff_enums.Identifier_type.t;
10- value : string;
11- description : string option;
12-}
13-14-let make ~type_ ~value ?description () =
15- { type_; value; description }
16017let type_ t = t.type_
18let value t = t.value
19let description t = t.description
2021let equal a b =
22- Cff_enums.Identifier_type.equal a.type_ b.type_ &&
23- String.equal a.value b.value
2425let compare a b =
26 match Cff_enums.Identifier_type.compare a.type_ b.type_ with
27 | 0 -> String.compare a.value b.value
28 | n -> n
02930-let pp ppf t =
31- Format.fprintf ppf "%a: %s"
32- Cff_enums.Identifier_type.pp t.type_
33- t.value
3435let jsont =
36- Jsont.Object.map ~kind:"Identifier"
37- (fun type_ value description -> { type_; value; description })
38- |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont
39- ~enc:(fun i -> i.type_)
40- |> Jsont.Object.mem "value" Jsont.string
41- ~enc:(fun i -> i.value)
42- |> Jsont.Object.opt_mem "description" Jsont.string
43- ~enc:(fun i -> i.description)
44 |> Jsont.Object.skip_unknown
45 |> Jsont.Object.finish
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** Identifier type for CFF. *)
78+type t =
9+ { type_ : Cff_enums.Identifier_type.t
10+ ; value : string
11+ ; description : string option
12+ }
0001314+let make ~type_ ~value ?description () = { type_; value; description }
15let type_ t = t.type_
16let value t = t.value
17let description t = t.description
1819let equal a b =
20+ Cff_enums.Identifier_type.equal a.type_ b.type_ && String.equal a.value b.value
21+;;
2223let compare a b =
24 match Cff_enums.Identifier_type.compare a.type_ b.type_ with
25 | 0 -> String.compare a.value b.value
26 | n -> n
27+;;
2829+let pp ppf t = Format.fprintf ppf "%a: %s" Cff_enums.Identifier_type.pp t.type_ t.value
0003031let jsont =
32+ Jsont.Object.map ~kind:"Identifier" (fun type_ value description ->
33+ { type_; value; description })
34+ |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont ~enc:(fun i -> i.type_)
35+ |> Jsont.Object.mem "value" Jsont.string ~enc:(fun i -> i.value)
36+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun i -> i.description)
00037 |> Jsont.Object.skip_unknown
38 |> Jsont.Object.finish
39+;;
+15-14
lib/cff_identifier.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···66 - [rel]: Release
67 - [snp]: Snapshot *)
6869-type t
70(** An identifier with type, value, and optional description. *)
07172-val make :
73- type_:Cff_enums.Identifier_type.t ->
74- value:string ->
75- ?description:string ->
76- unit -> t
77(** Create an identifier.
7879 @param type_ The identifier type ([`Doi], [`Url], [`Swh], or [`Other])
80 @param value The identifier value (DOI, URL, SWH ID, etc.)
81 @param description Optional human-readable description *)
00000082083val type_ : t -> Cff_enums.Identifier_type.t
84-(** The identifier type. *)
8586-val value : t -> string
87(** The identifier value.
8889 For DOIs, this is just the DOI (e.g., ["10.5281/zenodo.1234567"]),
90 not the full URL. *)
09192-val description : t -> string option
93(** Optional description explaining what this identifier refers to.
9495 Examples:
96 - ["The concept DOI for all versions"]
97 - ["Version 1.0.0 archive"]
98 - ["Release on GitHub"] *)
0990100val equal : t -> t -> bool
101-(** Identifier equality (compares all fields). *)
102103-val compare : t -> t -> int
104(** Identifier comparison. *)
0105106-val pp : Format.formatter -> t -> unit
107(** Pretty-print as "[type]: value (description)". *)
01080109val jsont : t Jsont.t
110-(** JSON/YAML codec for identifiers. *)
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···66 - [rel]: Release
67 - [snp]: Snapshot *)
68069(** An identifier with type, value, and optional description. *)
70+type t
710000072(** Create an identifier.
7374 @param type_ The identifier type ([`Doi], [`Url], [`Swh], or [`Other])
75 @param value The identifier value (DOI, URL, SWH ID, etc.)
76 @param description Optional human-readable description *)
77+val make
78+ : type_:Cff_enums.Identifier_type.t
79+ -> value:string
80+ -> ?description:string
81+ -> unit
82+ -> t
8384+(** The identifier type. *)
85val type_ : t -> Cff_enums.Identifier_type.t
086087(** The identifier value.
8889 For DOIs, this is just the DOI (e.g., ["10.5281/zenodo.1234567"]),
90 not the full URL. *)
91+val value : t -> string
92093(** Optional description explaining what this identifier refers to.
9495 Examples:
96 - ["The concept DOI for all versions"]
97 - ["Version 1.0.0 archive"]
98 - ["Release on GitHub"] *)
99+val description : t -> string option
100101+(** Identifier equality (compares all fields). *)
102val equal : t -> t -> bool
01030104(** Identifier comparison. *)
105+val compare : t -> t -> int
1060107(** Pretty-print as "[type]: value (description)". *)
108+val pp : Format.formatter -> t -> unit
109110+(** JSON/YAML codec for identifiers. *)
111val jsont : t Jsont.t
0
+63-35
lib/cff_license.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** SPDX license handling for CFF. *)
78-type t = [ `Expr of Spdx_licenses.t | `Raw of string list ]
000910-let of_spdx spdx = `Expr spdx
1112let of_string s =
13 match Spdx_licenses.parse s with
14- | Ok spdx -> `Expr spdx
15- | Error _ -> `Raw [s]
01617let of_strings ss =
18- (* Try to parse as OR combination, fall back to Raw *)
19 let try_parse_all () =
20 let rec build = function
21 | [] -> None
22- | [s] ->
23 (match Spdx_licenses.parse s with
24 | Ok spdx -> Some spdx
25 | Error _ -> None)
···31 build ss
32 in
33 match try_parse_all () with
34- | Some spdx -> `Expr spdx
35- | None -> `Raw ss
0000000000003637let to_spdx = function
38- | `Expr spdx -> Some spdx
39- | `Raw _ -> None
04041let to_strings = function
42- | `Expr spdx -> [Spdx_licenses.to_string spdx]
43- | `Raw ss -> ss
0000004445let pp ppf = function
46- | `Expr spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx)
47- | `Raw ss ->
48- match ss with
49- | [s] -> Format.pp_print_string ppf s
50- | _ ->
51- Format.fprintf ppf "[%a]"
52- (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
53- Format.pp_print_string) ss
0000005455-(* Jsont codec - lenient, accepts any string/array *)
56let jsont =
57 let string_codec =
58- Jsont.string |> Jsont.map
59- ~dec:(fun s -> of_string s)
60- ~enc:(function
61- | `Expr spdx -> Spdx_licenses.to_string spdx
62- | `Raw [s] -> s
63- | `Raw _ -> assert false)
64 in
65 let array_codec =
66- Jsont.(array string) |> Jsont.map
67- ~dec:(fun ss -> of_strings (Array.to_list ss))
68- ~enc:(fun t -> Array.of_list (to_strings t))
069 in
70 Jsont.any
71 ~dec_string:string_codec
72 ~dec_array:array_codec
73- ~enc:(fun t ->
74- match t with
75- | `Expr (Spdx_licenses.Simple _) -> string_codec
76- | `Raw [_] -> string_codec
77 | _ -> array_codec)
78 ()
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** SPDX license handling for CFF. *)
78+type t =
9+ [ `Spdx of Spdx_licenses.t
10+ | `Other of string list * string option
11+ ]
1213+let of_spdx spdx = `Spdx spdx
1415let of_string s =
16 match Spdx_licenses.parse s with
17+ | Ok spdx -> `Spdx spdx
18+ | Error _ -> `Other ([ s ], None)
19+;;
2021let of_strings ss =
22+ (* Try to parse as OR combination, fall back to Other *)
23 let try_parse_all () =
24 let rec build = function
25 | [] -> None
26+ | [ s ] ->
27 (match Spdx_licenses.parse s with
28 | Ok spdx -> Some spdx
29 | Error _ -> None)
···35 build ss
36 in
37 match try_parse_all () with
38+ | Some spdx -> `Spdx spdx
39+ | None -> `Other (ss, None)
40+;;
41+42+let with_url url = function
43+ | `Spdx _ as t -> t (* SPDX licenses have well-known URLs, ignore provided URL *)
44+ | `Other (ids, _) -> `Other (ids, Some url)
45+;;
46+47+let with_url_opt url_opt t =
48+ match url_opt with
49+ | None -> t
50+ | Some url -> with_url url t
51+;;
5253let to_spdx = function
54+ | `Spdx spdx -> Some spdx
55+ | `Other _ -> None
56+;;
5758let to_strings = function
59+ | `Spdx spdx -> [ Spdx_licenses.to_string spdx ]
60+ | `Other (ss, _) -> ss
61+;;
62+63+let url = function
64+ | `Spdx _ -> None
65+ | `Other (_, url) -> url
66+;;
6768let pp ppf = function
69+ | `Spdx spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx)
70+ | `Other (ss, url_opt) ->
71+ (match ss with
72+ | [ s ] -> Format.pp_print_string ppf s
73+ | _ ->
74+ Format.fprintf
75+ ppf
76+ "[%a]"
77+ (Format.pp_print_list
78+ ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
79+ Format.pp_print_string)
80+ ss);
81+ Option.iter (Format.fprintf ppf " <%s>") url_opt
82+;;
83084let jsont =
85 let string_codec =
86+ Jsont.string
87+ |> Jsont.map ~dec:of_string ~enc:(function
88+ | `Spdx spdx -> Spdx_licenses.to_string spdx
89+ | `Other ([ s ], _) -> s
90+ | `Other _ -> assert false)
091 in
92 let array_codec =
93+ Jsont.(array string)
94+ |> Jsont.map
95+ ~dec:(fun ss -> of_strings (Array.to_list ss))
96+ ~enc:(fun t -> Array.of_list (to_strings t))
97 in
98 Jsont.any
99 ~dec_string:string_codec
100 ~dec_array:array_codec
101+ ~enc:(function
102+ | `Spdx (Spdx_licenses.Simple _) -> string_codec
103+ | `Other ([ _ ], _) -> string_codec
0104 | _ -> array_codec)
105 ()
106+;;
+58-30
lib/cff_license.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** SPDX license expressions for CFF.
78 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers}
9- for the [license] field. This module wraps {!Spdx_licenses.t} with
10- support for invalid/unknown licenses to enable round-tripping.
1112 {1 License Representation}
1314 Licenses are represented as either:
15- - [`Expr spdx] - A valid, parsed SPDX license expression
16- - [`Raw strings] - Unparsed strings for invalid/unknown licenses
1718 The parser is lenient: it tries to parse as SPDX but preserves
19- invalid strings for round-tripping.
00000000002021 {1 Examples}
2223- {2 Single License}
24 {[
25 license: MIT
26 ]}
02728 {2 SPDX Expression}
29 {[
30- license: GPL-3.0-or-later WITH Classpath-exception-2.0
31 ]}
03233- {2 Multiple Licenses (OR)}
34 {[
35- license:
36- - Apache-2.0
37- - MIT
38 ]}
39- This is parsed as [Apache-2.0 OR MIT]. *)
4041-type t = [ `Expr of Spdx_licenses.t | `Raw of string list ]
42-(** The license type: either a valid SPDX expression or raw strings. *)
0004344(** {1 Construction} *)
45046val of_spdx : Spdx_licenses.t -> t
47-(** [of_spdx spdx] wraps a valid SPDX expression. *)
4849-val of_string : string -> t
50(** [of_string s] parses [s] as an SPDX expression.
51- Returns [`Expr] on success, [`Raw [s]] on parse failure. *)
05253-val of_strings : string list -> t
54(** [of_strings ss] parses a list of license strings.
55- If all strings are valid license IDs, returns an [`Expr] with OR combination.
56- Otherwise returns [`Raw ss] to preserve the original strings. *)
00000000005758(** {1 Access} *)
59060val to_spdx : t -> Spdx_licenses.t option
61-(** [to_spdx t] returns [Some spdx] if [t] is a valid expression,
62- [None] if it contains unparsed raw strings. *)
63064val to_strings : t -> string list
65-(** [to_strings t] returns the license as a list of strings.
66- For [`Expr], returns the normalized SPDX string.
67- For [`Raw], returns the original strings. *)
006869(** {1 Formatting} *)
7071-val pp : Format.formatter -> t -> unit
72(** Pretty-print the license. *)
07374(** {1 Codec} *)
7576-val jsont : t Jsont.t
77-(** JSON/YAML codec for licenses.
0007879- Handles both single string and array of strings.
80 Lenient: accepts any string without validation for round-tripping. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
56(** SPDX license expressions for CFF.
78 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers}
9+ for the [license] field. This module combines license identification
10+ with optional URLs for non-standard licenses.
1112 {1 License Representation}
1314 Licenses are represented as either:
15+ - [`Spdx expr] - A valid SPDX expression (URL is implicit/well-known)
16+ - [`Other (ids, url_opt)] - Unknown license ID(s) with optional URL
1718 The parser is lenient: it tries to parse as SPDX but preserves
19+ invalid/unknown identifiers for round-tripping.
20+21+ {1 Why Combined?}
22+23+ The CFF spec has separate [license] and [license-url] fields, but they
24+ have a hidden relationship:
25+ - SPDX licenses have well-known URLs (e.g., MIT → https://spdx.org/licenses/MIT.html)
26+ - [license-url] is only meaningful for non-SPDX licenses
27+28+ This type makes that relationship explicit: [`Spdx] licenses don't need
29+ a URL, while [`Other] licenses can optionally include one.
3031 {1 Examples}
3233+ {2 Standard SPDX License}
34 {[
35 license: MIT
36 ]}
37+ Parsed as [`Spdx (Simple (LicenseID "MIT"))].
3839 {2 SPDX Expression}
40 {[
41+ license: Apache-2.0 OR MIT
42 ]}
43+ Parsed as [`Spdx (OR ...)].
4445+ {2 Custom License with URL}
46 {[
47+ license: ACME-Proprietary-1.0
48+ license-url: https://acme.com/license
049 ]}
50+ Parsed as [`Other (["ACME-Proprietary-1.0"], Some "https://acme.com/license")]. *)
5152+(** License type: SPDX expression or unknown ID(s) with optional URL. *)
53+type t =
54+ [ `Spdx of Spdx_licenses.t
55+ | `Other of string list * string option
56+ ]
5758(** {1 Construction} *)
5960+(** [of_spdx expr] wraps a valid SPDX expression. *)
61val of_spdx : Spdx_licenses.t -> t
062063(** [of_string s] parses [s] as an SPDX expression.
64+ Returns [`Spdx] on success, [`Other ([s], None)] on parse failure. *)
65+val of_string : string -> t
66067(** [of_strings ss] parses a list of license strings.
68+ If all are valid SPDX IDs, returns [`Spdx] with OR combination.
69+ Otherwise returns [`Other (ss, None)]. *)
70+val of_strings : string list -> t
71+72+(** [with_url url t] adds a URL to the license.
73+ - For [`Spdx], returns unchanged (SPDX URLs are well-known)
74+ - For [`Other (ids, _)], returns [`Other (ids, Some url)] *)
75+val with_url : string -> t -> t
76+77+(** [with_url_opt url_opt t] optionally adds a URL.
78+ Convenience for combining during jsont decoding. *)
79+val with_url_opt : string option -> t -> t
8081(** {1 Access} *)
8283+(** [to_spdx t] returns [Some expr] if valid SPDX, [None] otherwise. *)
84val to_spdx : t -> Spdx_licenses.t option
008586+(** [to_strings t] returns the license identifier(s) as strings. *)
87val to_strings : t -> string list
88+89+(** [url t] returns the license URL.
90+ - For [`Spdx], always [None] (use SPDX's well-known URLs)
91+ - For [`Other], returns the URL if provided *)
92+val url : t -> string option
9394(** {1 Formatting} *)
95096(** Pretty-print the license. *)
97+val pp : Format.formatter -> t -> unit
9899(** {1 Codec} *)
100101+(** JSON/YAML codec for the license field only.
102+103+ This handles just the [license] field. The [license-url] field
104+ is handled separately by the parent codec which combines them
105+ using {!with_url}.
1060107 Lenient: accepts any string without validation for round-tripping. *)
108+val jsont : t Jsont.t
+665-385
lib/cff_reference.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···78(** Core identity of a reference. *)
9module Core = struct
10- type t = {
11- type_ : Cff_enums.Reference_type.t;
12- title : string;
13- authors : Cff_author.t list;
14- abstract : string option;
15- abbreviation : string option;
16- }
1718 let make ~type_ ~title ~authors ?abstract ?abbreviation () =
19 { type_; title; authors; abstract; abbreviation }
02021 let type_ t = t.type_
22 let title t = t.title
23 let authors t = t.authors
24 let abstract t = t.abstract
25 let abbreviation t = t.abbreviation
26-27- let pp ppf t =
28- Format.fprintf ppf "%s (%a)"
29- t.title Cff_enums.Reference_type.pp t.type_
30end
3132(** Publication information (journal, volume, pages, etc.). *)
33module Publication = struct
34- type t = {
35- journal : string option;
36- volume : string option;
37- issue : string option;
38- pages : string option;
39- start : string option;
40- end_ : string option;
41- edition : string option;
42- section : string option;
43- status : Cff_enums.Status.t option;
44- }
4546- let empty = {
47- journal = None; volume = None; issue = None; pages = None;
48- start = None; end_ = None; edition = None; section = None;
49- status = None;
50- }
00000005152- let make ?journal ?volume ?issue ?pages ?start ?end_ ?edition
53- ?section ?status () =
54 { journal; volume; issue; pages; start; end_; edition; section; status }
05556 let journal t = t.journal
57 let volume t = t.volume
···64 let status t = t.status
6566 let is_empty t =
67- t.journal = None && t.volume = None && t.issue = None &&
68- t.pages = None && t.start = None && t.end_ = None &&
69- t.edition = None && t.section = None && t.status = None
000000070end
7172(** Collection information (proceedings, book series, etc.). *)
73module Collection = struct
74- type t = {
75- collection_title : string option;
76- collection_type : string option;
77- collection_doi : string option;
78- volume_title : string option;
79- number_volumes : string option;
80- }
8182- let empty = {
83- collection_title = None; collection_type = None;
84- collection_doi = None; volume_title = None; number_volumes = None;
85- }
00008687- let make ?collection_title ?collection_type ?collection_doi
88- ?volume_title ?number_volumes () =
89- { collection_title; collection_type; collection_doi;
90- volume_title; number_volumes }
0000009192 let collection_title t = t.collection_title
93 let collection_type t = t.collection_type
···96 let number_volumes t = t.number_volumes
9798 let is_empty t =
99- t.collection_title = None && t.collection_type = None &&
100- t.collection_doi = None && t.volume_title = None &&
101- t.number_volumes = None
000102end
103104(** Date information. *)
105module Dates = struct
106- type t = {
107- date_accessed : Cff_date.t option;
108- date_downloaded : Cff_date.t option;
109- date_published : Cff_date.t option;
110- date_released : Cff_date.t option;
111- year : int option;
112- year_original : int option;
113- month : int option;
114- issue_date : string option;
115- }
116117- let empty = {
118- date_accessed = None; date_downloaded = None;
119- date_published = None; date_released = None;
120- year = None; year_original = None; month = None; issue_date = None;
121- }
000000122123- let make ?date_accessed ?date_downloaded ?date_published ?date_released
124- ?year ?year_original ?month ?issue_date () =
125- { date_accessed; date_downloaded; date_published; date_released;
126- year; year_original; month; issue_date }
00000000000000000127128 let date_accessed t = t.date_accessed
129 let date_downloaded t = t.date_downloaded
···135 let issue_date t = t.issue_date
136137 let is_empty t =
138- t.date_accessed = None && t.date_downloaded = None &&
139- t.date_published = None && t.date_released = None &&
140- t.year = None && t.year_original = None &&
141- t.month = None && t.issue_date = None
00000142end
143144(** Identifiers and links. *)
145module Identifiers = struct
146- type t = {
147- doi : string option;
148- url : string option;
149- repository : string option;
150- repository_code : string option;
151- repository_artifact : string option;
152- isbn : string option;
153- issn : string option;
154- pmcid : string option;
155- nihmsid : string option;
156- identifiers : Cff_identifier.t list option;
157- }
158159- let empty = {
160- doi = None; url = None; repository = None;
161- repository_code = None; repository_artifact = None;
162- isbn = None; issn = None; pmcid = None; nihmsid = None;
163- identifiers = None;
164- }
0000000165166- let make ?doi ?url ?repository ?repository_code ?repository_artifact
167- ?isbn ?issn ?pmcid ?nihmsid ?identifiers () =
168- { doi; url; repository; repository_code; repository_artifact;
169- isbn; issn; pmcid; nihmsid; identifiers }
000000000000000000000170171 let doi t = t.doi
172 let url t = t.url
···180 let identifiers t = t.identifiers
181182 let is_empty t =
183- t.doi = None && t.url = None && t.repository = None &&
184- t.repository_code = None && t.repository_artifact = None &&
185- t.isbn = None && t.issn = None && t.pmcid = None &&
186- t.nihmsid = None && t.identifiers = None
0000000187end
188189(** Related entities (editors, publisher, etc.). *)
190module Entities = struct
191- type t = {
192- editors : Cff_author.t list option;
193- editors_series : Cff_author.t list option;
194- translators : Cff_author.t list option;
195- recipients : Cff_author.t list option;
196- senders : Cff_author.t list option;
197- contact : Cff_author.t list option;
198- publisher : Cff_author.Entity.t option;
199- institution : Cff_author.Entity.t option;
200- conference : Cff_author.Entity.t option;
201- database_provider : Cff_author.Entity.t option;
202- location : Cff_author.Entity.t option;
203- }
204205- let empty = {
206- editors = None; editors_series = None; translators = None;
207- recipients = None; senders = None; contact = None;
208- publisher = None; institution = None; conference = None;
209- database_provider = None; location = None;
210- }
00000000211212- let make ?editors ?editors_series ?translators ?recipients ?senders
213- ?contact ?publisher ?institution ?conference ?database_provider
214- ?location () =
215- { editors; editors_series; translators; recipients; senders;
216- contact; publisher; institution; conference; database_provider;
217- location }
000000000000000000000218219 let editors t = t.editors
220 let editors_series t = t.editors_series
···229 let location t = t.location
230231 let is_empty t =
232- t.editors = None && t.editors_series = None && t.translators = None &&
233- t.recipients = None && t.senders = None && t.contact = None &&
234- t.publisher = None && t.institution = None && t.conference = None &&
235- t.database_provider = None && t.location = None
00000000236end
237238(** Metadata and description. *)
239module Metadata = struct
240- type t = {
241- keywords : string list option;
242- languages : string list option;
243- license : Cff_license.t option;
244- license_url : string option;
245- copyright : string option;
246- scope : string option;
247- notes : string option;
248- }
249250- let empty = {
251- keywords = None; languages = None; license = None;
252- license_url = None; copyright = None; scope = None; notes = None;
253- }
00000254255- let make ?keywords ?languages ?license ?license_url ?copyright
256- ?scope ?notes () =
257- { keywords; languages; license; license_url; copyright; scope; notes }
258259 let keywords t = t.keywords
260 let languages t = t.languages
261 let license t = t.license
262- let license_url t = t.license_url
263 let copyright t = t.copyright
264 let scope t = t.scope
265 let notes t = t.notes
266267 let is_empty t =
268- t.keywords = None && t.languages = None && t.license = None &&
269- t.license_url = None && t.copyright = None &&
270- t.scope = None && t.notes = None
0000271end
272273(** Technical and domain-specific fields. *)
274module Technical = struct
275- type t = {
276- commit : string option;
277- version : string option;
278- filename : string option;
279- format : string option;
280- medium : string option;
281- data_type : string option;
282- database : string option;
283- number : string option;
284- patent_states : string list option;
285- thesis_type : string option;
286- term : string option;
287- entry : string option;
288- department : string option;
289- loc_start : string option;
290- loc_end : string option;
291- }
292293- let empty = {
294- commit = None; version = None; filename = None; format = None;
295- medium = None; data_type = None; database = None; number = None;
296- patent_states = None; thesis_type = None; term = None; entry = None;
297- department = None; loc_start = None; loc_end = None;
298- }
000000000000299300- let make ?commit ?version ?filename ?format ?medium ?data_type
301- ?database ?number ?patent_states ?thesis_type ?term ?entry
302- ?department ?loc_start ?loc_end () =
303- { commit; version; filename; format; medium; data_type; database;
304- number; patent_states; thesis_type; term; entry; department;
305- loc_start; loc_end }
00000000000000000000000000000306307 let commit t = t.commit
308 let version t = t.version
···321 let loc_end t = t.loc_end
322323 let is_empty t =
324- t.commit = None && t.version = None && t.filename = None &&
325- t.format = None && t.medium = None && t.data_type = None &&
326- t.database = None && t.number = None && t.patent_states = None &&
327- t.thesis_type = None && t.term = None && t.entry = None &&
328- t.department = None && t.loc_start = None && t.loc_end = None
00000000000329end
330331(** Complete reference type. *)
332-type t = {
333- core : Core.t;
334- publication : Publication.t;
335- collection : Collection.t;
336- dates : Dates.t;
337- identifiers : Identifiers.t;
338- entities : Entities.t;
339- metadata : Metadata.t;
340- technical : Technical.t;
341-}
342343-let make ~core
344- ?(publication = Publication.empty)
345- ?(collection = Collection.empty)
346- ?(dates = Dates.empty)
347- ?(identifiers = Identifiers.empty)
348- ?(entities = Entities.empty)
349- ?(metadata = Metadata.empty)
350- ?(technical = Technical.empty)
351- () =
352- { core; publication; collection; dates; identifiers;
353- entities; metadata; technical }
00354355let make_simple ~type_ ~title ~authors ?doi ?year ?journal () =
356 let core = Core.make ~type_ ~title ~authors () in
···358 let dates = Dates.make ?year () in
359 let identifiers = Identifiers.make ?doi () in
360 make ~core ~publication ~dates ~identifiers ()
0361362(* Accessors for sub-records *)
363let core t = t.core
···375let authors t = Core.authors t.core
376let doi t = Identifiers.doi t.identifiers
377let year t = Dates.year t.dates
378-379-let pp ppf t =
380- Core.pp ppf t.core
381382(* Helper for string that can also be int (for pages, etc.) *)
383let string_or_int_jsont =
384 Jsont.any
385- ~dec_number:(Jsont.number |> Jsont.map
386- ~dec:(fun f -> string_of_int (int_of_float f))
387- ~enc:float_of_string)
388 ~dec_string:Jsont.string
389 ~enc:(fun s ->
390 match float_of_string_opt s with
391- | Some _ -> Jsont.number |> Jsont.map ~dec:(fun _ -> assert false) ~enc:float_of_string
0392 | None -> Jsont.string)
393 ()
0394395(* Helper to convert array jsont to list jsont *)
396let list_jsont elt =
397 Jsont.(array elt |> map ~dec:Stdlib.Array.to_list ~enc:Stdlib.Array.of_list)
0398399(* Jsont codec for the full reference type *)
400let jsont =
···402 let identifiers_list_jsont = list_jsont Cff_identifier.jsont in
403 let string_list_jsont = list_jsont Jsont.string in
404 (* We need to decode all 60+ fields and then group into sub-records *)
405- Jsont.Object.map ~kind:"Reference"
406- (fun type_ title authors abstract abbreviation
000000407 (* Publication *)
408- journal volume issue pages start end_ edition section status
00000000409 (* Collection *)
410- collection_title collection_type collection_doi volume_title number_volumes
0000411 (* Dates *)
412- date_accessed date_downloaded date_published date_released
413- year year_original month issue_date
000000414 (* Identifiers *)
415- doi url repository repository_code repository_artifact
416- isbn issn pmcid nihmsid identifiers_list
00000000417 (* Entities *)
418- editors editors_series translators recipients senders contact
419- publisher institution conference database_provider location_entity
000000000420 (* Metadata *)
421- keywords languages license license_url copyright scope notes
000000422 (* Technical *)
423- commit version filename format medium data_type database
424- number patent_states thesis_type term entry department
425- loc_start loc_end ->
426- let core = { Core.type_; title; authors; abstract; abbreviation } in
427- let publication = { Publication.journal; volume; issue; pages;
428- start; end_; edition; section; status } in
429- let collection = { Collection.collection_title; collection_type;
430- collection_doi; volume_title; number_volumes } in
431- let dates = { Dates.date_accessed; date_downloaded; date_published;
432- date_released; year; year_original; month; issue_date } in
433- let identifiers = { Identifiers.doi; url; repository; repository_code;
434- repository_artifact; isbn; issn; pmcid; nihmsid;
435- identifiers = identifiers_list } in
436- let entities = { Entities.editors; editors_series; translators;
437- recipients; senders; contact; publisher; institution;
438- conference; database_provider; location = location_entity } in
439- let metadata = { Metadata.keywords; languages; license; license_url;
440- copyright; scope; notes } in
441- let technical = { Technical.commit; version; filename; format; medium;
442- data_type; database; number; patent_states; thesis_type;
443- term; entry; department; loc_start; loc_end } in
444- { core; publication; collection; dates; identifiers;
445- entities; metadata; technical })
00000000000000000000000000000000000000000000000000000000000000000000000000000000000446 (* Core fields *)
447- |> Jsont.Object.mem "type" Cff_enums.Reference_type.jsont
448- ~enc:(fun r -> r.core.type_)
449- |> Jsont.Object.mem "title" Jsont.string
450- ~enc:(fun r -> r.core.title)
451- |> Jsont.Object.mem "authors" authors_list_jsont
452- ~enc:(fun r -> r.core.authors)
453- |> Jsont.Object.opt_mem "abstract" Jsont.string
454- ~enc:(fun r -> r.core.abstract)
455- |> Jsont.Object.opt_mem "abbreviation" Jsont.string
456- ~enc:(fun r -> r.core.abbreviation)
457 (* Publication fields *)
458- |> Jsont.Object.opt_mem "journal" Jsont.string
459- ~enc:(fun r -> r.publication.journal)
460- |> Jsont.Object.opt_mem "volume" string_or_int_jsont
461- ~enc:(fun r -> r.publication.volume)
462- |> Jsont.Object.opt_mem "issue" string_or_int_jsont
463- ~enc:(fun r -> r.publication.issue)
464- |> Jsont.Object.opt_mem "pages" string_or_int_jsont
465- ~enc:(fun r -> r.publication.pages)
466- |> Jsont.Object.opt_mem "start" string_or_int_jsont
467- ~enc:(fun r -> r.publication.start)
468- |> Jsont.Object.opt_mem "end" string_or_int_jsont
469- ~enc:(fun r -> r.publication.end_)
470- |> Jsont.Object.opt_mem "edition" Jsont.string
471- ~enc:(fun r -> r.publication.edition)
472- |> Jsont.Object.opt_mem "section" string_or_int_jsont
473- ~enc:(fun r -> r.publication.section)
474- |> Jsont.Object.opt_mem "status" Cff_enums.Status.jsont
475- ~enc:(fun r -> r.publication.status)
476 (* Collection fields *)
477- |> Jsont.Object.opt_mem "collection-title" Jsont.string
478- ~enc:(fun r -> r.collection.collection_title)
479- |> Jsont.Object.opt_mem "collection-type" Jsont.string
480- ~enc:(fun r -> r.collection.collection_type)
481- |> Jsont.Object.opt_mem "collection-doi" Jsont.string
482- ~enc:(fun r -> r.collection.collection_doi)
483- |> Jsont.Object.opt_mem "volume-title" Jsont.string
484- ~enc:(fun r -> r.collection.volume_title)
485- |> Jsont.Object.opt_mem "number-volumes" string_or_int_jsont
486- ~enc:(fun r -> r.collection.number_volumes)
487 (* Date fields *)
488- |> Jsont.Object.opt_mem "date-accessed" Cff_date.jsont
489- ~enc:(fun r -> r.dates.date_accessed)
490- |> Jsont.Object.opt_mem "date-downloaded" Cff_date.jsont
491- ~enc:(fun r -> r.dates.date_downloaded)
492- |> Jsont.Object.opt_mem "date-published" Cff_date.jsont
493- ~enc:(fun r -> r.dates.date_published)
494- |> Jsont.Object.opt_mem "date-released" Cff_date.jsont
495- ~enc:(fun r -> r.dates.date_released)
496- |> Jsont.Object.opt_mem "year" Jsont.int
497- ~enc:(fun r -> r.dates.year)
498- |> Jsont.Object.opt_mem "year-original" Jsont.int
499- ~enc:(fun r -> r.dates.year_original)
500- |> Jsont.Object.opt_mem "month" Jsont.int
501- ~enc:(fun r -> r.dates.month)
502- |> Jsont.Object.opt_mem "issue-date" Jsont.string
503- ~enc:(fun r -> r.dates.issue_date)
504 (* Identifier fields *)
505- |> Jsont.Object.opt_mem "doi" Jsont.string
506- ~enc:(fun r -> r.identifiers.doi)
507- |> Jsont.Object.opt_mem "url" Jsont.string
508- ~enc:(fun r -> r.identifiers.url)
509- |> Jsont.Object.opt_mem "repository" Jsont.string
510- ~enc:(fun r -> r.identifiers.repository)
511- |> Jsont.Object.opt_mem "repository-code" Jsont.string
512- ~enc:(fun r -> r.identifiers.repository_code)
513- |> Jsont.Object.opt_mem "repository-artifact" Jsont.string
514- ~enc:(fun r -> r.identifiers.repository_artifact)
515- |> Jsont.Object.opt_mem "isbn" Jsont.string
516- ~enc:(fun r -> r.identifiers.isbn)
517- |> Jsont.Object.opt_mem "issn" string_or_int_jsont
518- ~enc:(fun r -> r.identifiers.issn)
519- |> Jsont.Object.opt_mem "pmcid" Jsont.string
520- ~enc:(fun r -> r.identifiers.pmcid)
521- |> Jsont.Object.opt_mem "nihmsid" Jsont.string
522- ~enc:(fun r -> r.identifiers.nihmsid)
523- |> Jsont.Object.opt_mem "identifiers" identifiers_list_jsont
524- ~enc:(fun r -> r.identifiers.identifiers)
525 (* Entity fields *)
526- |> Jsont.Object.opt_mem "editors" authors_list_jsont
527- ~enc:(fun r -> r.entities.editors)
528- |> Jsont.Object.opt_mem "editors-series" authors_list_jsont
529- ~enc:(fun r -> r.entities.editors_series)
530- |> Jsont.Object.opt_mem "translators" authors_list_jsont
531- ~enc:(fun r -> r.entities.translators)
532- |> Jsont.Object.opt_mem "recipients" authors_list_jsont
533- ~enc:(fun r -> r.entities.recipients)
534- |> Jsont.Object.opt_mem "senders" authors_list_jsont
535- ~enc:(fun r -> r.entities.senders)
536- |> Jsont.Object.opt_mem "contact" authors_list_jsont
537- ~enc:(fun r -> r.entities.contact)
538- |> Jsont.Object.opt_mem "publisher" Cff_author.Entity.jsont
539- ~enc:(fun r -> r.entities.publisher)
540- |> Jsont.Object.opt_mem "institution" Cff_author.Entity.jsont
541- ~enc:(fun r -> r.entities.institution)
542- |> Jsont.Object.opt_mem "conference" Cff_author.Entity.jsont
543- ~enc:(fun r -> r.entities.conference)
544- |> Jsont.Object.opt_mem "database-provider" Cff_author.Entity.jsont
545- ~enc:(fun r -> r.entities.database_provider)
546- |> Jsont.Object.opt_mem "location" Cff_author.Entity.jsont
547- ~enc:(fun r -> r.entities.location)
548 (* Metadata fields *)
549- |> Jsont.Object.opt_mem "keywords" string_list_jsont
550- ~enc:(fun r -> r.metadata.keywords)
551- |> Jsont.Object.opt_mem "languages" string_list_jsont
552- ~enc:(fun r -> r.metadata.languages)
553- |> Jsont.Object.opt_mem "license" Cff_license.jsont
554- ~enc:(fun r -> r.metadata.license)
555- |> Jsont.Object.opt_mem "license-url" Jsont.string
556- ~enc:(fun r -> r.metadata.license_url)
557- |> Jsont.Object.opt_mem "copyright" Jsont.string
558- ~enc:(fun r -> r.metadata.copyright)
559- |> Jsont.Object.opt_mem "scope" Jsont.string
560- ~enc:(fun r -> r.metadata.scope)
561- |> Jsont.Object.opt_mem "notes" Jsont.string
562- ~enc:(fun r -> r.metadata.notes)
563 (* Technical fields *)
564- |> Jsont.Object.opt_mem "commit" Jsont.string
565- ~enc:(fun r -> r.technical.commit)
566- |> Jsont.Object.opt_mem "version" string_or_int_jsont
567- ~enc:(fun r -> r.technical.version)
568- |> Jsont.Object.opt_mem "filename" Jsont.string
569- ~enc:(fun r -> r.technical.filename)
570- |> Jsont.Object.opt_mem "format" Jsont.string
571- ~enc:(fun r -> r.technical.format)
572- |> Jsont.Object.opt_mem "medium" Jsont.string
573- ~enc:(fun r -> r.technical.medium)
574- |> Jsont.Object.opt_mem "data-type" Jsont.string
575- ~enc:(fun r -> r.technical.data_type)
576- |> Jsont.Object.opt_mem "database" Jsont.string
577- ~enc:(fun r -> r.technical.database)
578- |> Jsont.Object.opt_mem "number" string_or_int_jsont
579- ~enc:(fun r -> r.technical.number)
580- |> Jsont.Object.opt_mem "patent-states" string_list_jsont
581- ~enc:(fun r -> r.technical.patent_states)
582- |> Jsont.Object.opt_mem "thesis-type" Jsont.string
583- ~enc:(fun r -> r.technical.thesis_type)
584- |> Jsont.Object.opt_mem "term" Jsont.string
585- ~enc:(fun r -> r.technical.term)
586- |> Jsont.Object.opt_mem "entry" Jsont.string
587- ~enc:(fun r -> r.technical.entry)
588- |> Jsont.Object.opt_mem "department" Jsont.string
589- ~enc:(fun r -> r.technical.department)
590- |> Jsont.Object.opt_mem "loc-start" string_or_int_jsont
591- ~enc:(fun r -> r.technical.loc_start)
592- |> Jsont.Object.opt_mem "loc-end" string_or_int_jsont
593- ~enc:(fun r -> r.technical.loc_end)
594 |> Jsont.Object.skip_unknown
595 |> Jsont.Object.finish
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···78(** Core identity of a reference. *)
9module Core = struct
10+ type t =
11+ { type_ : Cff_enums.Reference_type.t
12+ ; title : string
13+ ; authors : Cff_author.t list
14+ ; abstract : string option
15+ ; abbreviation : string option
16+ }
1718 let make ~type_ ~title ~authors ?abstract ?abbreviation () =
19 { type_; title; authors; abstract; abbreviation }
20+ ;;
2122 let type_ t = t.type_
23 let title t = t.title
24 let authors t = t.authors
25 let abstract t = t.abstract
26 let abbreviation t = t.abbreviation
27+ let pp ppf t = Format.fprintf ppf "%s (%a)" t.title Cff_enums.Reference_type.pp t.type_
00028end
2930(** Publication information (journal, volume, pages, etc.). *)
31module Publication = struct
32+ type t =
33+ { journal : string option
34+ ; volume : string option
35+ ; issue : string option
36+ ; pages : string option
37+ ; start : string option
38+ ; end_ : string option
39+ ; edition : string option
40+ ; section : string option
41+ ; status : Cff_enums.Status.t option
42+ }
4344+ let empty =
45+ { journal = None
46+ ; volume = None
47+ ; issue = None
48+ ; pages = None
49+ ; start = None
50+ ; end_ = None
51+ ; edition = None
52+ ; section = None
53+ ; status = None
54+ }
55+ ;;
5657+ let make ?journal ?volume ?issue ?pages ?start ?end_ ?edition ?section ?status () =
058 { journal; volume; issue; pages; start; end_; edition; section; status }
59+ ;;
6061 let journal t = t.journal
62 let volume t = t.volume
···69 let status t = t.status
7071 let is_empty t =
72+ t.journal = None
73+ && t.volume = None
74+ && t.issue = None
75+ && t.pages = None
76+ && t.start = None
77+ && t.end_ = None
78+ && t.edition = None
79+ && t.section = None
80+ && t.status = None
81+ ;;
82end
8384(** Collection information (proceedings, book series, etc.). *)
85module Collection = struct
86+ type t =
87+ { collection_title : string option
88+ ; collection_type : string option
89+ ; collection_doi : string option
90+ ; volume_title : string option
91+ ; number_volumes : string option
92+ }
9394+ let empty =
95+ { collection_title = None
96+ ; collection_type = None
97+ ; collection_doi = None
98+ ; volume_title = None
99+ ; number_volumes = None
100+ }
101+ ;;
102103+ let make
104+ ?collection_title
105+ ?collection_type
106+ ?collection_doi
107+ ?volume_title
108+ ?number_volumes
109+ ()
110+ =
111+ { collection_title; collection_type; collection_doi; volume_title; number_volumes }
112+ ;;
113114 let collection_title t = t.collection_title
115 let collection_type t = t.collection_type
···118 let number_volumes t = t.number_volumes
119120 let is_empty t =
121+ t.collection_title = None
122+ && t.collection_type = None
123+ && t.collection_doi = None
124+ && t.volume_title = None
125+ && t.number_volumes = None
126+ ;;
127end
128129(** Date information. *)
130module Dates = struct
131+ type t =
132+ { date_accessed : Cff_date.t option
133+ ; date_downloaded : Cff_date.t option
134+ ; date_published : Cff_date.t option
135+ ; date_released : Cff_date.t option
136+ ; year : int option
137+ ; year_original : int option
138+ ; month : int option
139+ ; issue_date : string option
140+ }
141142+ let empty =
143+ { date_accessed = None
144+ ; date_downloaded = None
145+ ; date_published = None
146+ ; date_released = None
147+ ; year = None
148+ ; year_original = None
149+ ; month = None
150+ ; issue_date = None
151+ }
152+ ;;
153154+ let make
155+ ?date_accessed
156+ ?date_downloaded
157+ ?date_published
158+ ?date_released
159+ ?year
160+ ?year_original
161+ ?month
162+ ?issue_date
163+ ()
164+ =
165+ { date_accessed
166+ ; date_downloaded
167+ ; date_published
168+ ; date_released
169+ ; year
170+ ; year_original
171+ ; month
172+ ; issue_date
173+ }
174+ ;;
175176 let date_accessed t = t.date_accessed
177 let date_downloaded t = t.date_downloaded
···183 let issue_date t = t.issue_date
184185 let is_empty t =
186+ t.date_accessed = None
187+ && t.date_downloaded = None
188+ && t.date_published = None
189+ && t.date_released = None
190+ && t.year = None
191+ && t.year_original = None
192+ && t.month = None
193+ && t.issue_date = None
194+ ;;
195end
196197(** Identifiers and links. *)
198module Identifiers = struct
199+ type t =
200+ { doi : string option
201+ ; url : string option
202+ ; repository : string option
203+ ; repository_code : string option
204+ ; repository_artifact : string option
205+ ; isbn : string option
206+ ; issn : string option
207+ ; pmcid : string option
208+ ; nihmsid : string option
209+ ; identifiers : Cff_identifier.t list option
210+ }
211212+ let empty =
213+ { doi = None
214+ ; url = None
215+ ; repository = None
216+ ; repository_code = None
217+ ; repository_artifact = None
218+ ; isbn = None
219+ ; issn = None
220+ ; pmcid = None
221+ ; nihmsid = None
222+ ; identifiers = None
223+ }
224+ ;;
225226+ let make
227+ ?doi
228+ ?url
229+ ?repository
230+ ?repository_code
231+ ?repository_artifact
232+ ?isbn
233+ ?issn
234+ ?pmcid
235+ ?nihmsid
236+ ?identifiers
237+ ()
238+ =
239+ { doi
240+ ; url
241+ ; repository
242+ ; repository_code
243+ ; repository_artifact
244+ ; isbn
245+ ; issn
246+ ; pmcid
247+ ; nihmsid
248+ ; identifiers
249+ }
250+ ;;
251252 let doi t = t.doi
253 let url t = t.url
···261 let identifiers t = t.identifiers
262263 let is_empty t =
264+ t.doi = None
265+ && t.url = None
266+ && t.repository = None
267+ && t.repository_code = None
268+ && t.repository_artifact = None
269+ && t.isbn = None
270+ && t.issn = None
271+ && t.pmcid = None
272+ && t.nihmsid = None
273+ && t.identifiers = None
274+ ;;
275end
276277(** Related entities (editors, publisher, etc.). *)
278module Entities = struct
279+ type t =
280+ { editors : Cff_author.t list option
281+ ; editors_series : Cff_author.t list option
282+ ; translators : Cff_author.t list option
283+ ; recipients : Cff_author.t list option
284+ ; senders : Cff_author.t list option
285+ ; contact : Cff_author.t list option
286+ ; publisher : Cff_author.Entity.t option
287+ ; institution : Cff_author.Entity.t option
288+ ; conference : Cff_author.Entity.t option
289+ ; database_provider : Cff_author.Entity.t option
290+ ; location : Cff_author.Entity.t option
291+ }
292293+ let empty =
294+ { editors = None
295+ ; editors_series = None
296+ ; translators = None
297+ ; recipients = None
298+ ; senders = None
299+ ; contact = None
300+ ; publisher = None
301+ ; institution = None
302+ ; conference = None
303+ ; database_provider = None
304+ ; location = None
305+ }
306+ ;;
307308+ let make
309+ ?editors
310+ ?editors_series
311+ ?translators
312+ ?recipients
313+ ?senders
314+ ?contact
315+ ?publisher
316+ ?institution
317+ ?conference
318+ ?database_provider
319+ ?location
320+ ()
321+ =
322+ { editors
323+ ; editors_series
324+ ; translators
325+ ; recipients
326+ ; senders
327+ ; contact
328+ ; publisher
329+ ; institution
330+ ; conference
331+ ; database_provider
332+ ; location
333+ }
334+ ;;
335336 let editors t = t.editors
337 let editors_series t = t.editors_series
···346 let location t = t.location
347348 let is_empty t =
349+ t.editors = None
350+ && t.editors_series = None
351+ && t.translators = None
352+ && t.recipients = None
353+ && t.senders = None
354+ && t.contact = None
355+ && t.publisher = None
356+ && t.institution = None
357+ && t.conference = None
358+ && t.database_provider = None
359+ && t.location = None
360+ ;;
361end
362363(** Metadata and description. *)
364module Metadata = struct
365+ type t =
366+ { keywords : string list option
367+ ; languages : string list option
368+ ; license : Cff_license.t option
369+ ; copyright : string option
370+ ; scope : string option
371+ ; notes : string option
372+ }
0373374+ let empty =
375+ { keywords = None
376+ ; languages = None
377+ ; license = None
378+ ; copyright = None
379+ ; scope = None
380+ ; notes = None
381+ }
382+ ;;
383384+ let make ?keywords ?languages ?license ?copyright ?scope ?notes () =
385+ { keywords; languages; license; copyright; scope; notes }
386+ ;;
387388 let keywords t = t.keywords
389 let languages t = t.languages
390 let license t = t.license
0391 let copyright t = t.copyright
392 let scope t = t.scope
393 let notes t = t.notes
394395 let is_empty t =
396+ t.keywords = None
397+ && t.languages = None
398+ && t.license = None
399+ && t.copyright = None
400+ && t.scope = None
401+ && t.notes = None
402+ ;;
403end
404405(** Technical and domain-specific fields. *)
406module Technical = struct
407+ type t =
408+ { commit : string option
409+ ; version : string option
410+ ; filename : string option
411+ ; format : string option
412+ ; medium : string option
413+ ; data_type : string option
414+ ; database : string option
415+ ; number : string option
416+ ; patent_states : string list option
417+ ; thesis_type : string option
418+ ; term : string option
419+ ; entry : string option
420+ ; department : string option
421+ ; loc_start : string option
422+ ; loc_end : string option
423+ }
424425+ let empty =
426+ { commit = None
427+ ; version = None
428+ ; filename = None
429+ ; format = None
430+ ; medium = None
431+ ; data_type = None
432+ ; database = None
433+ ; number = None
434+ ; patent_states = None
435+ ; thesis_type = None
436+ ; term = None
437+ ; entry = None
438+ ; department = None
439+ ; loc_start = None
440+ ; loc_end = None
441+ }
442+ ;;
443444+ let make
445+ ?commit
446+ ?version
447+ ?filename
448+ ?format
449+ ?medium
450+ ?data_type
451+ ?database
452+ ?number
453+ ?patent_states
454+ ?thesis_type
455+ ?term
456+ ?entry
457+ ?department
458+ ?loc_start
459+ ?loc_end
460+ ()
461+ =
462+ { commit
463+ ; version
464+ ; filename
465+ ; format
466+ ; medium
467+ ; data_type
468+ ; database
469+ ; number
470+ ; patent_states
471+ ; thesis_type
472+ ; term
473+ ; entry
474+ ; department
475+ ; loc_start
476+ ; loc_end
477+ }
478+ ;;
479480 let commit t = t.commit
481 let version t = t.version
···494 let loc_end t = t.loc_end
495496 let is_empty t =
497+ t.commit = None
498+ && t.version = None
499+ && t.filename = None
500+ && t.format = None
501+ && t.medium = None
502+ && t.data_type = None
503+ && t.database = None
504+ && t.number = None
505+ && t.patent_states = None
506+ && t.thesis_type = None
507+ && t.term = None
508+ && t.entry = None
509+ && t.department = None
510+ && t.loc_start = None
511+ && t.loc_end = None
512+ ;;
513end
514515(** Complete reference type. *)
516+type t =
517+ { core : Core.t
518+ ; publication : Publication.t
519+ ; collection : Collection.t
520+ ; dates : Dates.t
521+ ; identifiers : Identifiers.t
522+ ; entities : Entities.t
523+ ; metadata : Metadata.t
524+ ; technical : Technical.t
525+ }
526527+let make
528+ ~core
529+ ?(publication = Publication.empty)
530+ ?(collection = Collection.empty)
531+ ?(dates = Dates.empty)
532+ ?(identifiers = Identifiers.empty)
533+ ?(entities = Entities.empty)
534+ ?(metadata = Metadata.empty)
535+ ?(technical = Technical.empty)
536+ ()
537+ =
538+ { core; publication; collection; dates; identifiers; entities; metadata; technical }
539+;;
540541let make_simple ~type_ ~title ~authors ?doi ?year ?journal () =
542 let core = Core.make ~type_ ~title ~authors () in
···544 let dates = Dates.make ?year () in
545 let identifiers = Identifiers.make ?doi () in
546 make ~core ~publication ~dates ~identifiers ()
547+;;
548549(* Accessors for sub-records *)
550let core t = t.core
···562let authors t = Core.authors t.core
563let doi t = Identifiers.doi t.identifiers
564let year t = Dates.year t.dates
565+let pp ppf t = Core.pp ppf t.core
00566567(* Helper for string that can also be int (for pages, etc.) *)
568let string_or_int_jsont =
569 Jsont.any
570+ ~dec_number:
571+ (Jsont.number
572+ |> Jsont.map ~dec:(fun f -> string_of_int (int_of_float f)) ~enc:float_of_string)
573 ~dec_string:Jsont.string
574 ~enc:(fun s ->
575 match float_of_string_opt s with
576+ | Some _ ->
577+ Jsont.number |> Jsont.map ~dec:(fun _ -> assert false) ~enc:float_of_string
578 | None -> Jsont.string)
579 ()
580+;;
581582(* Helper to convert array jsont to list jsont *)
583let list_jsont elt =
584 Jsont.(array elt |> map ~dec:Stdlib.Array.to_list ~enc:Stdlib.Array.of_list)
585+;;
586587(* Jsont codec for the full reference type *)
588let jsont =
···590 let identifiers_list_jsont = list_jsont Cff_identifier.jsont in
591 let string_list_jsont = list_jsont Jsont.string in
592 (* We need to decode all 60+ fields and then group into sub-records *)
593+ Jsont.Object.map
594+ ~kind:"Reference"
595+ (fun
596+ type_
597+ title
598+ authors
599+ abstract
600+ abbreviation
601 (* Publication *)
602+ journal
603+ volume
604+ issue
605+ pages
606+ start
607+ end_
608+ edition
609+ section
610+ status
611 (* Collection *)
612+ collection_title
613+ collection_type
614+ collection_doi
615+ volume_title
616+ number_volumes
617 (* Dates *)
618+ date_accessed
619+ date_downloaded
620+ date_published
621+ date_released
622+ year
623+ year_original
624+ month
625+ issue_date
626 (* Identifiers *)
627+ doi
628+ url
629+ repository
630+ repository_code
631+ repository_artifact
632+ isbn
633+ issn
634+ pmcid
635+ nihmsid
636+ identifiers_list
637 (* Entities *)
638+ editors
639+ editors_series
640+ translators
641+ recipients
642+ senders
643+ contact
644+ publisher
645+ institution
646+ conference
647+ database_provider
648+ location_entity
649 (* Metadata *)
650+ keywords
651+ languages
652+ license
653+ license_url
654+ copyright
655+ scope
656+ notes
657 (* Technical *)
658+ commit
659+ version
660+ filename
661+ format
662+ medium
663+ data_type
664+ database
665+ number
666+ patent_states
667+ thesis_type
668+ term
669+ entry
670+ department
671+ loc_start
672+ loc_end
673+ ->
674+ let core = { Core.type_; title; authors; abstract; abbreviation } in
675+ let publication =
676+ { Publication.journal
677+ ; volume
678+ ; issue
679+ ; pages
680+ ; start
681+ ; end_
682+ ; edition
683+ ; section
684+ ; status
685+ }
686+ in
687+ let collection =
688+ { Collection.collection_title
689+ ; collection_type
690+ ; collection_doi
691+ ; volume_title
692+ ; number_volumes
693+ }
694+ in
695+ let dates =
696+ { Dates.date_accessed
697+ ; date_downloaded
698+ ; date_published
699+ ; date_released
700+ ; year
701+ ; year_original
702+ ; month
703+ ; issue_date
704+ }
705+ in
706+ let identifiers =
707+ { Identifiers.doi
708+ ; url
709+ ; repository
710+ ; repository_code
711+ ; repository_artifact
712+ ; isbn
713+ ; issn
714+ ; pmcid
715+ ; nihmsid
716+ ; identifiers = identifiers_list
717+ }
718+ in
719+ let entities =
720+ { Entities.editors
721+ ; editors_series
722+ ; translators
723+ ; recipients
724+ ; senders
725+ ; contact
726+ ; publisher
727+ ; institution
728+ ; conference
729+ ; database_provider
730+ ; location = location_entity
731+ }
732+ in
733+ let license = Option.map (Cff_license.with_url_opt license_url) license in
734+ let metadata =
735+ { Metadata.keywords; languages; license; copyright; scope; notes }
736+ in
737+ let technical =
738+ { Technical.commit
739+ ; version
740+ ; filename
741+ ; format
742+ ; medium
743+ ; data_type
744+ ; database
745+ ; number
746+ ; patent_states
747+ ; thesis_type
748+ ; term
749+ ; entry
750+ ; department
751+ ; loc_start
752+ ; loc_end
753+ }
754+ in
755+ { core
756+ ; publication
757+ ; collection
758+ ; dates
759+ ; identifiers
760+ ; entities
761+ ; metadata
762+ ; technical
763+ })
764 (* Core fields *)
765+ |> Jsont.Object.mem "type" Cff_enums.Reference_type.jsont ~enc:(fun r -> r.core.type_)
766+ |> Jsont.Object.mem "title" Jsont.string ~enc:(fun r -> r.core.title)
767+ |> Jsont.Object.mem "authors" authors_list_jsont ~enc:(fun r -> r.core.authors)
768+ |> Jsont.Object.opt_mem "abstract" Jsont.string ~enc:(fun r -> r.core.abstract)
769+ |> Jsont.Object.opt_mem "abbreviation" Jsont.string ~enc:(fun r -> r.core.abbreviation)
00000770 (* Publication fields *)
771+ |> Jsont.Object.opt_mem "journal" Jsont.string ~enc:(fun r -> r.publication.journal)
772+ |> Jsont.Object.opt_mem "volume" string_or_int_jsont ~enc:(fun r ->
773+ r.publication.volume)
774+ |> Jsont.Object.opt_mem "issue" string_or_int_jsont ~enc:(fun r -> r.publication.issue)
775+ |> Jsont.Object.opt_mem "pages" string_or_int_jsont ~enc:(fun r -> r.publication.pages)
776+ |> Jsont.Object.opt_mem "start" string_or_int_jsont ~enc:(fun r -> r.publication.start)
777+ |> Jsont.Object.opt_mem "end" string_or_int_jsont ~enc:(fun r -> r.publication.end_)
778+ |> Jsont.Object.opt_mem "edition" Jsont.string ~enc:(fun r -> r.publication.edition)
779+ |> Jsont.Object.opt_mem "section" string_or_int_jsont ~enc:(fun r ->
780+ r.publication.section)
781+ |> Jsont.Object.opt_mem "status" Cff_enums.Status.jsont ~enc:(fun r ->
782+ r.publication.status)
000000783 (* Collection fields *)
784+ |> Jsont.Object.opt_mem "collection-title" Jsont.string ~enc:(fun r ->
785+ r.collection.collection_title)
786+ |> Jsont.Object.opt_mem "collection-type" Jsont.string ~enc:(fun r ->
787+ r.collection.collection_type)
788+ |> Jsont.Object.opt_mem "collection-doi" Jsont.string ~enc:(fun r ->
789+ r.collection.collection_doi)
790+ |> Jsont.Object.opt_mem "volume-title" Jsont.string ~enc:(fun r ->
791+ r.collection.volume_title)
792+ |> Jsont.Object.opt_mem "number-volumes" string_or_int_jsont ~enc:(fun r ->
793+ r.collection.number_volumes)
794 (* Date fields *)
795+ |> Jsont.Object.opt_mem "date-accessed" Cff_date.jsont ~enc:(fun r ->
796+ r.dates.date_accessed)
797+ |> Jsont.Object.opt_mem "date-downloaded" Cff_date.jsont ~enc:(fun r ->
798+ r.dates.date_downloaded)
799+ |> Jsont.Object.opt_mem "date-published" Cff_date.jsont ~enc:(fun r ->
800+ r.dates.date_published)
801+ |> Jsont.Object.opt_mem "date-released" Cff_date.jsont ~enc:(fun r ->
802+ r.dates.date_released)
803+ |> Jsont.Object.opt_mem "year" Jsont.int ~enc:(fun r -> r.dates.year)
804+ |> Jsont.Object.opt_mem "year-original" Jsont.int ~enc:(fun r -> r.dates.year_original)
805+ |> Jsont.Object.opt_mem "month" Jsont.int ~enc:(fun r -> r.dates.month)
806+ |> Jsont.Object.opt_mem "issue-date" Jsont.string ~enc:(fun r -> r.dates.issue_date)
0000807 (* Identifier fields *)
808+ |> Jsont.Object.opt_mem "doi" Jsont.string ~enc:(fun r -> r.identifiers.doi)
809+ |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun r -> r.identifiers.url)
810+ |> Jsont.Object.opt_mem "repository" Jsont.string ~enc:(fun r ->
811+ r.identifiers.repository)
812+ |> Jsont.Object.opt_mem "repository-code" Jsont.string ~enc:(fun r ->
813+ r.identifiers.repository_code)
814+ |> Jsont.Object.opt_mem "repository-artifact" Jsont.string ~enc:(fun r ->
815+ r.identifiers.repository_artifact)
816+ |> Jsont.Object.opt_mem "isbn" Jsont.string ~enc:(fun r -> r.identifiers.isbn)
817+ |> Jsont.Object.opt_mem "issn" string_or_int_jsont ~enc:(fun r -> r.identifiers.issn)
818+ |> Jsont.Object.opt_mem "pmcid" Jsont.string ~enc:(fun r -> r.identifiers.pmcid)
819+ |> Jsont.Object.opt_mem "nihmsid" Jsont.string ~enc:(fun r -> r.identifiers.nihmsid)
820+ |> Jsont.Object.opt_mem "identifiers" identifiers_list_jsont ~enc:(fun r ->
821+ r.identifiers.identifiers)
000000822 (* Entity fields *)
823+ |> Jsont.Object.opt_mem "editors" authors_list_jsont ~enc:(fun r -> r.entities.editors)
824+ |> Jsont.Object.opt_mem "editors-series" authors_list_jsont ~enc:(fun r ->
825+ r.entities.editors_series)
826+ |> Jsont.Object.opt_mem "translators" authors_list_jsont ~enc:(fun r ->
827+ r.entities.translators)
828+ |> Jsont.Object.opt_mem "recipients" authors_list_jsont ~enc:(fun r ->
829+ r.entities.recipients)
830+ |> Jsont.Object.opt_mem "senders" authors_list_jsont ~enc:(fun r -> r.entities.senders)
831+ |> Jsont.Object.opt_mem "contact" authors_list_jsont ~enc:(fun r -> r.entities.contact)
832+ |> Jsont.Object.opt_mem "publisher" Cff_author.Entity.jsont ~enc:(fun r ->
833+ r.entities.publisher)
834+ |> Jsont.Object.opt_mem "institution" Cff_author.Entity.jsont ~enc:(fun r ->
835+ r.entities.institution)
836+ |> Jsont.Object.opt_mem "conference" Cff_author.Entity.jsont ~enc:(fun r ->
837+ r.entities.conference)
838+ |> Jsont.Object.opt_mem "database-provider" Cff_author.Entity.jsont ~enc:(fun r ->
839+ r.entities.database_provider)
840+ |> Jsont.Object.opt_mem "location" Cff_author.Entity.jsont ~enc:(fun r ->
841+ r.entities.location)
000842 (* Metadata fields *)
843+ |> Jsont.Object.opt_mem "keywords" string_list_jsont ~enc:(fun r -> r.metadata.keywords)
844+ |> Jsont.Object.opt_mem "languages" string_list_jsont ~enc:(fun r ->
845+ r.metadata.languages)
846+ |> Jsont.Object.opt_mem "license" Cff_license.jsont ~enc:(fun r -> r.metadata.license)
847+ |> Jsont.Object.opt_mem "license-url" Jsont.string ~enc:(fun r ->
848+ Option.bind r.metadata.license Cff_license.url)
849+ |> Jsont.Object.opt_mem "copyright" Jsont.string ~enc:(fun r -> r.metadata.copyright)
850+ |> Jsont.Object.opt_mem "scope" Jsont.string ~enc:(fun r -> r.metadata.scope)
851+ |> Jsont.Object.opt_mem "notes" Jsont.string ~enc:(fun r -> r.metadata.notes)
00000852 (* Technical fields *)
853+ |> Jsont.Object.opt_mem "commit" Jsont.string ~enc:(fun r -> r.technical.commit)
854+ |> Jsont.Object.opt_mem "version" string_or_int_jsont ~enc:(fun r ->
855+ r.technical.version)
856+ |> Jsont.Object.opt_mem "filename" Jsont.string ~enc:(fun r -> r.technical.filename)
857+ |> Jsont.Object.opt_mem "format" Jsont.string ~enc:(fun r -> r.technical.format)
858+ |> Jsont.Object.opt_mem "medium" Jsont.string ~enc:(fun r -> r.technical.medium)
859+ |> Jsont.Object.opt_mem "data-type" Jsont.string ~enc:(fun r -> r.technical.data_type)
860+ |> Jsont.Object.opt_mem "database" Jsont.string ~enc:(fun r -> r.technical.database)
861+ |> Jsont.Object.opt_mem "number" string_or_int_jsont ~enc:(fun r -> r.technical.number)
862+ |> Jsont.Object.opt_mem "patent-states" string_list_jsont ~enc:(fun r ->
863+ r.technical.patent_states)
864+ |> Jsont.Object.opt_mem "thesis-type" Jsont.string ~enc:(fun r ->
865+ r.technical.thesis_type)
866+ |> Jsont.Object.opt_mem "term" Jsont.string ~enc:(fun r -> r.technical.term)
867+ |> Jsont.Object.opt_mem "entry" Jsont.string ~enc:(fun r -> r.technical.entry)
868+ |> Jsont.Object.opt_mem "department" Jsont.string ~enc:(fun r -> r.technical.department)
869+ |> Jsont.Object.opt_mem "loc-start" string_or_int_jsont ~enc:(fun r ->
870+ r.technical.loc_start)
871+ |> Jsont.Object.opt_mem "loc-end" string_or_int_jsont ~enc:(fun r ->
872+ r.technical.loc_end)
0000000000873 |> Jsont.Object.skip_unknown
874 |> Jsont.Object.finish
875+;;
+200-194
lib/cff_reference.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···76module Core : sig
77 type t
7879- val make :
80- type_:Cff_enums.Reference_type.t ->
81- title:string ->
82- authors:Cff_author.t list ->
83- ?abstract:string ->
84- ?abbreviation:string ->
85- unit -> t
86 (** Create a core record.
8788 @param type_ The reference type (article, book, software, etc.)
89 @param title The title of the work
90 @param authors List of persons and/or entities *)
000000009192- val type_ : t -> Cff_enums.Reference_type.t
93 (** The reference type. Determines which other fields are applicable. *)
09495- val title : t -> string
96 (** The title of the referenced work. *)
097098 val authors : t -> Cff_author.t list
99- (** The authors/creators of the work. *)
100101- val abstract : t -> string option
102 (** A description or abstract of the work. *)
0103104- val abbreviation : t -> string option
105 (** Abbreviated form of the title (e.g., for journal names). *)
0106107 val pp : Format.formatter -> t -> unit
108end
···115module Publication : sig
116 type t
117118- val empty : t
119 (** Empty publication record with all fields as [None]. *)
0120121- val make :
122- ?journal:string ->
123- ?volume:string ->
124- ?issue:string ->
125- ?pages:string ->
126- ?start:string ->
127- ?end_:string ->
128- ?edition:string ->
129- ?section:string ->
130- ?status:Cff_enums.Status.t ->
131- unit -> t
0132133- val journal : t -> string option
134 (** The name of the journal or magazine. *)
0135136- val volume : t -> string option
137 (** The volume number of the journal. *)
01380139 val issue : t -> string option
140- (** The issue number within the volume. *)
1410142 val pages : t -> string option
143- (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *)
144145- val start : t -> string option
146 (** Starting page number. *)
01470148 val end_ : t -> string option
149- (** Ending page number. *)
1500151 val edition : t -> string option
152- (** The edition of the work (e.g., ["2nd edition"]). *)
153154- val section : t -> string option
155 (** The section of a work (e.g., newspaper section). *)
0156157- val status : t -> Cff_enums.Status.t option
158 (** Publication status: preprint, in-press, submitted, etc. *)
01590160 val is_empty : t -> bool
161- (** [true] if all fields are [None]. *)
162end
163164(** Collection metadata for works in edited volumes.
···170171 val empty : t
172173- val make :
174- ?collection_title:string ->
175- ?collection_type:string ->
176- ?collection_doi:string ->
177- ?volume_title:string ->
178- ?number_volumes:string ->
179- unit -> t
0180181- val collection_title : t -> string option
182 (** Title of the collection (proceedings, book series, etc.). *)
01830184 val collection_type : t -> string option
185- (** Type of collection (e.g., ["proceedings"], ["book series"]). *)
186187- val collection_doi : t -> string option
188 (** DOI of the collection itself (not the individual work). *)
01890190 val volume_title : t -> string option
191- (** Title of the specific volume within a multi-volume collection. *)
192193- val number_volumes : t -> string option
194 (** Total number of volumes in the collection. *)
0195196 val is_empty : t -> bool
197end
···211212 val empty : t
213214- val make :
215- ?date_accessed:Cff_date.t ->
216- ?date_downloaded:Cff_date.t ->
217- ?date_published:Cff_date.t ->
218- ?date_released:Cff_date.t ->
219- ?year:int ->
220- ?year_original:int ->
221- ?month:int ->
222- ?issue_date:string ->
223- unit -> t
0224225- val date_accessed : t -> Cff_date.t option
226 (** Date when an online resource was accessed for citation. *)
0227228- val date_downloaded : t -> Cff_date.t option
229 (** Date when a resource was downloaded. *)
02300231 val date_published : t -> Cff_date.t option
232- (** Formal publication date. *)
233234- val date_released : t -> Cff_date.t option
235 (** Release date (typically for software). *)
02360237 val year : t -> int option
238- (** Publication year when full date is unknown. *)
239240- val year_original : t -> int option
241 (** Year of original publication (for reprints, translations). *)
02420243 val month : t -> int option
244- (** Publication month (1-12) when only month/year is known. *)
245246- val issue_date : t -> string option
247 (** Issue date as a string (for periodicals with specific dates). *)
0248249 val is_empty : t -> bool
250end
···263264 val empty : t
265266- val make :
267- ?doi:string ->
268- ?url:string ->
269- ?repository:string ->
270- ?repository_code:string ->
271- ?repository_artifact:string ->
272- ?isbn:string ->
273- ?issn:string ->
274- ?pmcid:string ->
275- ?nihmsid:string ->
276- ?identifiers:Cff_identifier.t list ->
277- unit -> t
02780279 val doi : t -> string option
280- (** Digital Object Identifier (e.g., ["10.1234/example"]). *)
281282- val url : t -> string option
283 (** URL where the work can be accessed. *)
0284285- val repository : t -> string option
286 (** General repository URL. *)
02870288 val repository_code : t -> string option
289- (** Source code repository (GitHub, GitLab, etc.). *)
290291- val repository_artifact : t -> string option
292 (** Built artifact repository (npm, PyPI, Docker Hub, etc.). *)
0293294- val isbn : t -> string option
295 (** International Standard Book Number. *)
02960297 val issn : t -> string option
298- (** International Standard Serial Number (for journals). *)
299300- val pmcid : t -> string option
301 (** PubMed Central identifier. *)
0302303- val nihmsid : t -> string option
304 (** NIH Manuscript Submission System identifier. *)
03050306 val identifiers : t -> Cff_identifier.t list option
307- (** Additional typed identifiers (DOI, URL, SWH, other). *)
308309 val is_empty : t -> bool
310end
···321322 val empty : t
323324- val make :
325- ?editors:Cff_author.t list ->
326- ?editors_series:Cff_author.t list ->
327- ?translators:Cff_author.t list ->
328- ?recipients:Cff_author.t list ->
329- ?senders:Cff_author.t list ->
330- ?contact:Cff_author.t list ->
331- ?publisher:Cff_author.Entity.t ->
332- ?institution:Cff_author.Entity.t ->
333- ?conference:Cff_author.Entity.t ->
334- ?database_provider:Cff_author.Entity.t ->
335- ?location:Cff_author.Entity.t ->
336- unit -> t
0337338- val editors : t -> Cff_author.t list option
339 (** Editors of the work (for edited volumes). *)
03400341 val editors_series : t -> Cff_author.t list option
342- (** Series editors (for book series). *)
343344- val translators : t -> Cff_author.t list option
345 (** Translators of the work. *)
0346347- val recipients : t -> Cff_author.t list option
348 (** Recipients (for personal communications). *)
03490350 val senders : t -> Cff_author.t list option
351- (** Senders (for personal communications). *)
352353- val contact : t -> Cff_author.t list option
354 (** Contact persons for the work. *)
0355356- val publisher : t -> Cff_author.Entity.t option
357 (** Publishing organization. *)
03580359 val institution : t -> Cff_author.Entity.t option
360- (** Academic/research institution (for theses, reports). *)
361362- val conference : t -> Cff_author.Entity.t option
363 (** Conference where the work was presented. *)
0364365- val database_provider : t -> Cff_author.Entity.t option
366 (** Provider of a database (for data references). *)
03670368 val location : t -> Cff_author.Entity.t option
369- (** Location entity (city, venue for conferences). *)
370371 val is_empty : t -> bool
372end
···379380 val empty : t
381382- val make :
383- ?keywords:string list ->
384- ?languages:string list ->
385- ?license:Cff_license.t ->
386- ?license_url:string ->
387- ?copyright:string ->
388- ?scope:string ->
389- ?notes:string ->
390- unit -> t
3910392 val keywords : t -> string list option
393- (** Descriptive keywords for the work. *)
394395- val languages : t -> string list option
396 (** Languages the work is available in (ISO 639 codes). *)
03970398 val license : t -> Cff_license.t option
399- (** SPDX license identifier(s). *)
400401- val license_url : t -> string option
402- (** URL to license text (for non-SPDX licenses). *)
403-404 val copyright : t -> string option
405- (** Copyright statement. *)
406407- val scope : t -> string option
408 (** Scope of the reference (what aspect it covers). *)
04090410 val notes : t -> string option
411- (** Additional notes or comments. *)
412413 val is_empty : t -> bool
414end
···426427 val empty : t
428429- val make :
430- ?commit:string ->
431- ?version:string ->
432- ?filename:string ->
433- ?format:string ->
434- ?medium:string ->
435- ?data_type:string ->
436- ?database:string ->
437- ?number:string ->
438- ?patent_states:string list ->
439- ?thesis_type:string ->
440- ?term:string ->
441- ?entry:string ->
442- ?department:string ->
443- ?loc_start:string ->
444- ?loc_end:string ->
445- unit -> t
0446447- val commit : t -> string option
448 (** Git commit hash or VCS revision. *)
0449450- val version : t -> string option
451 (** Version string of the software/data. *)
0452453- val filename : t -> string option
454 (** Name of the file being referenced. *)
04550456 val format : t -> string option
457- (** Format of the work (e.g., ["PDF"], ["HTML"]). *)
4580459 val medium : t -> string option
460- (** Physical medium (e.g., ["CD-ROM"], ["print"]). *)
461462- val data_type : t -> string option
463 (** Type of data (for datasets). *)
04640465 val database : t -> string option
466- (** Name of the database. *)
4670468 val number : t -> string option
469- (** Report/patent/standard number. *)
470471- val patent_states : t -> string list option
472 (** Countries where a patent is held. *)
0473474- val thesis_type : t -> string option
475 (** Type of thesis (["PhD"], ["Master's"], etc.). *)
04760477 val term : t -> string option
478- (** Dictionary/encyclopedia term being referenced. *)
479480- val entry : t -> string option
481 (** Encyclopedia entry name. *)
04820483 val department : t -> string option
484- (** Academic department (for theses). *)
4850486 val loc_start : t -> string option
487- (** Starting line/location in source code. *)
488489- val loc_end : t -> string option
490 (** Ending line/location in source code. *)
0491492 val is_empty : t -> bool
493end
···497(** The complete reference type combining all sub-records. *)
498type t
499500-val make :
501- core:Core.t ->
502- ?publication:Publication.t ->
503- ?collection:Collection.t ->
504- ?dates:Dates.t ->
505- ?identifiers:Identifiers.t ->
506- ?entities:Entities.t ->
507- ?metadata:Metadata.t ->
508- ?technical:Technical.t ->
509- unit -> t
510(** Construct a reference from sub-records.
511512 Only [core] is required; other sub-records default to empty. *)
00000000000513514-val make_simple :
515- type_:Cff_enums.Reference_type.t ->
516- title:string ->
517- authors:Cff_author.t list ->
518- ?doi:string ->
519- ?year:int ->
520- ?journal:string ->
521- unit -> t
522(** Convenience constructor for simple references.
523524 Creates a reference with just the most common fields. Suitable
525 for quick article or software references. *)
000000000526527(** {2 Sub-record Accessors} *)
528529-val core : t -> Core.t
530(** The core identity fields. *)
0531532-val publication : t -> Publication.t
533(** Publication metadata (journal, volume, pages). *)
05340535val collection : t -> Collection.t
536-(** Collection metadata (proceedings, book series). *)
537538-val dates : t -> Dates.t
539(** Date-related fields. *)
0540541-val identifiers : t -> Identifiers.t
542(** Identifiers and links. *)
05430544val entities : t -> Entities.t
545-(** Related entities (editors, publisher). *)
546547-val metadata : t -> Metadata.t
548(** Descriptive metadata (keywords, license). *)
0549550-val technical : t -> Technical.t
551(** Technical fields (commit, version, format). *)
0552553(** {2 Direct Accessors for Common Fields}
554555 Convenience accessors that delegate to sub-records. *)
556557-val type_ : t -> Cff_enums.Reference_type.t
558(** Shortcut for [Core.type_ (core t)]. *)
0559560-val title : t -> string
561(** Shortcut for [Core.title (core t)]. *)
0562563-val authors : t -> Cff_author.t list
564(** Shortcut for [Core.authors (core t)]. *)
05650566val doi : t -> string option
567-(** Shortcut for [Identifiers.doi (identifiers t)]. *)
568569-val year : t -> int option
570(** Shortcut for [Dates.year (dates t)]. *)
0571572(** {1 Formatting and Codec} *)
573574-val pp : Format.formatter -> t -> unit
575(** Pretty-print a reference in a human-readable format. *)
05760577val jsont : t Jsont.t
578-(** JSON/YAML codec for serialization. *)
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···76module Core : sig
77 type t
78000000079 (** Create a core record.
8081 @param type_ The reference type (article, book, software, etc.)
82 @param title The title of the work
83 @param authors List of persons and/or entities *)
84+ val make
85+ : type_:Cff_enums.Reference_type.t
86+ -> title:string
87+ -> authors:Cff_author.t list
88+ -> ?abstract:string
89+ -> ?abbreviation:string
90+ -> unit
91+ -> t
92093 (** The reference type. Determines which other fields are applicable. *)
94+ val type_ : t -> Cff_enums.Reference_type.t
95096 (** The title of the referenced work. *)
97+ val title : t -> string
9899+ (** The authors/creators of the work. *)
100 val authors : t -> Cff_author.t list
01010102 (** A description or abstract of the work. *)
103+ val abstract : t -> string option
1040105 (** Abbreviated form of the title (e.g., for journal names). *)
106+ val abbreviation : t -> string option
107108 val pp : Format.formatter -> t -> unit
109end
···116module Publication : sig
117 type t
1180119 (** Empty publication record with all fields as [None]. *)
120+ val empty : t
121122+ val make
123+ : ?journal:string
124+ -> ?volume:string
125+ -> ?issue:string
126+ -> ?pages:string
127+ -> ?start:string
128+ -> ?end_:string
129+ -> ?edition:string
130+ -> ?section:string
131+ -> ?status:Cff_enums.Status.t
132+ -> unit
133+ -> t
1340135 (** The name of the journal or magazine. *)
136+ val journal : t -> string option
1370138 (** The volume number of the journal. *)
139+ val volume : t -> string option
140141+ (** The issue number within the volume. *)
142 val issue : t -> string option
0143144+ (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *)
145 val pages : t -> string option
01460147 (** Starting page number. *)
148+ val start : t -> string option
149150+ (** Ending page number. *)
151 val end_ : t -> string option
0152153+ (** The edition of the work (e.g., ["2nd edition"]). *)
154 val edition : t -> string option
01550156 (** The section of a work (e.g., newspaper section). *)
157+ val section : t -> string option
1580159 (** Publication status: preprint, in-press, submitted, etc. *)
160+ val status : t -> Cff_enums.Status.t option
161162+ (** [true] if all fields are [None]. *)
163 val is_empty : t -> bool
0164end
165166(** Collection metadata for works in edited volumes.
···172173 val empty : t
174175+ val make
176+ : ?collection_title:string
177+ -> ?collection_type:string
178+ -> ?collection_doi:string
179+ -> ?volume_title:string
180+ -> ?number_volumes:string
181+ -> unit
182+ -> t
1830184 (** Title of the collection (proceedings, book series, etc.). *)
185+ val collection_title : t -> string option
186187+ (** Type of collection (e.g., ["proceedings"], ["book series"]). *)
188 val collection_type : t -> string option
01890190 (** DOI of the collection itself (not the individual work). *)
191+ val collection_doi : t -> string option
192193+ (** Title of the specific volume within a multi-volume collection. *)
194 val volume_title : t -> string option
01950196 (** Total number of volumes in the collection. *)
197+ val number_volumes : t -> string option
198199 val is_empty : t -> bool
200end
···214215 val empty : t
216217+ val make
218+ : ?date_accessed:Cff_date.t
219+ -> ?date_downloaded:Cff_date.t
220+ -> ?date_published:Cff_date.t
221+ -> ?date_released:Cff_date.t
222+ -> ?year:int
223+ -> ?year_original:int
224+ -> ?month:int
225+ -> ?issue_date:string
226+ -> unit
227+ -> t
2280229 (** Date when an online resource was accessed for citation. *)
230+ val date_accessed : t -> Cff_date.t option
2310232 (** Date when a resource was downloaded. *)
233+ val date_downloaded : t -> Cff_date.t option
234235+ (** Formal publication date. *)
236 val date_published : t -> Cff_date.t option
02370238 (** Release date (typically for software). *)
239+ val date_released : t -> Cff_date.t option
240241+ (** Publication year when full date is unknown. *)
242 val year : t -> int option
02430244 (** Year of original publication (for reprints, translations). *)
245+ val year_original : t -> int option
246247+ (** Publication month (1-12) when only month/year is known. *)
248 val month : t -> int option
02490250 (** Issue date as a string (for periodicals with specific dates). *)
251+ val issue_date : t -> string option
252253 val is_empty : t -> bool
254end
···267268 val empty : t
269270+ val make
271+ : ?doi:string
272+ -> ?url:string
273+ -> ?repository:string
274+ -> ?repository_code:string
275+ -> ?repository_artifact:string
276+ -> ?isbn:string
277+ -> ?issn:string
278+ -> ?pmcid:string
279+ -> ?nihmsid:string
280+ -> ?identifiers:Cff_identifier.t list
281+ -> unit
282+ -> t
283284+ (** Digital Object Identifier (e.g., ["10.1234/example"]). *)
285 val doi : t -> string option
02860287 (** URL where the work can be accessed. *)
288+ val url : t -> string option
2890290 (** General repository URL. *)
291+ val repository : t -> string option
292293+ (** Source code repository (GitHub, GitLab, etc.). *)
294 val repository_code : t -> string option
02950296 (** Built artifact repository (npm, PyPI, Docker Hub, etc.). *)
297+ val repository_artifact : t -> string option
2980299 (** International Standard Book Number. *)
300+ val isbn : t -> string option
301302+ (** International Standard Serial Number (for journals). *)
303 val issn : t -> string option
03040305 (** PubMed Central identifier. *)
306+ val pmcid : t -> string option
3070308 (** NIH Manuscript Submission System identifier. *)
309+ val nihmsid : t -> string option
310311+ (** Additional typed identifiers (DOI, URL, SWH, other). *)
312 val identifiers : t -> Cff_identifier.t list option
0313314 val is_empty : t -> bool
315end
···326327 val empty : t
328329+ val make
330+ : ?editors:Cff_author.t list
331+ -> ?editors_series:Cff_author.t list
332+ -> ?translators:Cff_author.t list
333+ -> ?recipients:Cff_author.t list
334+ -> ?senders:Cff_author.t list
335+ -> ?contact:Cff_author.t list
336+ -> ?publisher:Cff_author.Entity.t
337+ -> ?institution:Cff_author.Entity.t
338+ -> ?conference:Cff_author.Entity.t
339+ -> ?database_provider:Cff_author.Entity.t
340+ -> ?location:Cff_author.Entity.t
341+ -> unit
342+ -> t
3430344 (** Editors of the work (for edited volumes). *)
345+ val editors : t -> Cff_author.t list option
346347+ (** Series editors (for book series). *)
348 val editors_series : t -> Cff_author.t list option
03490350 (** Translators of the work. *)
351+ val translators : t -> Cff_author.t list option
3520353 (** Recipients (for personal communications). *)
354+ val recipients : t -> Cff_author.t list option
355356+ (** Senders (for personal communications). *)
357 val senders : t -> Cff_author.t list option
03580359 (** Contact persons for the work. *)
360+ val contact : t -> Cff_author.t list option
3610362 (** Publishing organization. *)
363+ val publisher : t -> Cff_author.Entity.t option
364365+ (** Academic/research institution (for theses, reports). *)
366 val institution : t -> Cff_author.Entity.t option
03670368 (** Conference where the work was presented. *)
369+ val conference : t -> Cff_author.Entity.t option
3700371 (** Provider of a database (for data references). *)
372+ val database_provider : t -> Cff_author.Entity.t option
373374+ (** Location entity (city, venue for conferences). *)
375 val location : t -> Cff_author.Entity.t option
0376377 val is_empty : t -> bool
378end
···385386 val empty : t
387388+ val make
389+ : ?keywords:string list
390+ -> ?languages:string list
391+ -> ?license:Cff_license.t
392+ -> ?copyright:string
393+ -> ?scope:string
394+ -> ?notes:string
395+ -> unit
396+ -> t
397398+ (** Descriptive keywords for the work. *)
399 val keywords : t -> string list option
04000401 (** Languages the work is available in (ISO 639 codes). *)
402+ val languages : t -> string list option
403404+ (** SPDX license identifier(s), or unknown license with optional URL. *)
405 val license : t -> Cff_license.t option
0406407+ (** Copyright statement. *)
00408 val copyright : t -> string option
04090410 (** Scope of the reference (what aspect it covers). *)
411+ val scope : t -> string option
412413+ (** Additional notes or comments. *)
414 val notes : t -> string option
0415416 val is_empty : t -> bool
417end
···429430 val empty : t
431432+ val make
433+ : ?commit:string
434+ -> ?version:string
435+ -> ?filename:string
436+ -> ?format:string
437+ -> ?medium:string
438+ -> ?data_type:string
439+ -> ?database:string
440+ -> ?number:string
441+ -> ?patent_states:string list
442+ -> ?thesis_type:string
443+ -> ?term:string
444+ -> ?entry:string
445+ -> ?department:string
446+ -> ?loc_start:string
447+ -> ?loc_end:string
448+ -> unit
449+ -> t
4500451 (** Git commit hash or VCS revision. *)
452+ val commit : t -> string option
4530454 (** Version string of the software/data. *)
455+ val version : t -> string option
4560457 (** Name of the file being referenced. *)
458+ val filename : t -> string option
459460+ (** Format of the work (e.g., ["PDF"], ["HTML"]). *)
461 val format : t -> string option
0462463+ (** Physical medium (e.g., ["CD-ROM"], ["print"]). *)
464 val medium : t -> string option
04650466 (** Type of data (for datasets). *)
467+ val data_type : t -> string option
468469+ (** Name of the database. *)
470 val database : t -> string option
0471472+ (** Report/patent/standard number. *)
473 val number : t -> string option
04740475 (** Countries where a patent is held. *)
476+ val patent_states : t -> string list option
4770478 (** Type of thesis (["PhD"], ["Master's"], etc.). *)
479+ val thesis_type : t -> string option
480481+ (** Dictionary/encyclopedia term being referenced. *)
482 val term : t -> string option
04830484 (** Encyclopedia entry name. *)
485+ val entry : t -> string option
486487+ (** Academic department (for theses). *)
488 val department : t -> string option
0489490+ (** Starting line/location in source code. *)
491 val loc_start : t -> string option
04920493 (** Ending line/location in source code. *)
494+ val loc_end : t -> string option
495496 val is_empty : t -> bool
497end
···501(** The complete reference type combining all sub-records. *)
502type t
5030000000000504(** Construct a reference from sub-records.
505506 Only [core] is required; other sub-records default to empty. *)
507+val make
508+ : core:Core.t
509+ -> ?publication:Publication.t
510+ -> ?collection:Collection.t
511+ -> ?dates:Dates.t
512+ -> ?identifiers:Identifiers.t
513+ -> ?entities:Entities.t
514+ -> ?metadata:Metadata.t
515+ -> ?technical:Technical.t
516+ -> unit
517+ -> t
51800000000519(** Convenience constructor for simple references.
520521 Creates a reference with just the most common fields. Suitable
522 for quick article or software references. *)
523+val make_simple
524+ : type_:Cff_enums.Reference_type.t
525+ -> title:string
526+ -> authors:Cff_author.t list
527+ -> ?doi:string
528+ -> ?year:int
529+ -> ?journal:string
530+ -> unit
531+ -> t
532533(** {2 Sub-record Accessors} *)
5340535(** The core identity fields. *)
536+val core : t -> Core.t
5370538(** Publication metadata (journal, volume, pages). *)
539+val publication : t -> Publication.t
540541+(** Collection metadata (proceedings, book series). *)
542val collection : t -> Collection.t
05430544(** Date-related fields. *)
545+val dates : t -> Dates.t
5460547(** Identifiers and links. *)
548+val identifiers : t -> Identifiers.t
549550+(** Related entities (editors, publisher). *)
551val entities : t -> Entities.t
05520553(** Descriptive metadata (keywords, license). *)
554+val metadata : t -> Metadata.t
5550556(** Technical fields (commit, version, format). *)
557+val technical : t -> Technical.t
558559(** {2 Direct Accessors for Common Fields}
560561 Convenience accessors that delegate to sub-records. *)
5620563(** Shortcut for [Core.type_ (core t)]. *)
564+val type_ : t -> Cff_enums.Reference_type.t
5650566(** Shortcut for [Core.title (core t)]. *)
567+val title : t -> string
5680569(** Shortcut for [Core.authors (core t)]. *)
570+val authors : t -> Cff_author.t list
571572+(** Shortcut for [Identifiers.doi (identifiers t)]. *)
573val doi : t -> string option
05740575(** Shortcut for [Dates.year (dates t)]. *)
576+val year : t -> int option
577578(** {1 Formatting and Codec} *)
5790580(** Pretty-print a reference in a human-readable format. *)
581+val pp : Format.formatter -> t -> unit
582583+(** JSON/YAML codec for serialization. *)
584val jsont : t Jsont.t
0
+15-5
lib_eio/cff_eio.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8(* Custom error type for CFF parsing errors *)
9type Eio.Exn.err += E of string
1011-let () = Eio.Exn.register_pp (fun f -> function
12- | E msg -> Format.fprintf f "Cff %s" msg; true
00013 | _ -> false)
01415let err msg = Eio.Exn.create (E msg)
16···19 match Yamlt.decode ~layout:true Cff.jsont reader with
20 | Ok cff -> cff
21 | Error msg -> raise (err msg)
02223let to_yaml_string t =
24 let buf = Buffer.create 1024 in
···26 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
27 | Ok () -> Buffer.contents buf
28 | Error msg -> raise (err msg)
02930let of_yaml_flow flow =
31 let reader = Bytesrw_eio.bytes_reader_of_flow flow in
32 match Yamlt.decode ~layout:true Cff.jsont reader with
33 | Ok cff -> cff
34 | Error msg -> raise (err msg)
03536let to_yaml_flow flow t =
37 let writer = Bytesrw_eio.bytes_writer_of_flow flow in
38 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
39 | Ok () -> ()
40 | Error msg -> raise (err msg)
04142let of_file ~fs path =
43 let data = Eio.Path.load Eio.Path.(fs / path) in
44- try of_yaml_string data
45- with Eio.Exn.Io _ as ex ->
46 let bt = Printexc.get_raw_backtrace () in
47 Eio.Exn.reraise_with_context ex bt "parsing CFF file %S" path
04849let to_file ~fs path t =
50 let data = to_yaml_string t in
51 Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) data
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8(* Custom error type for CFF parsing errors *)
9type Eio.Exn.err += E of string
1011+let () =
12+ Eio.Exn.register_pp (fun f -> function
13+ | E msg ->
14+ Format.fprintf f "Cff %s" msg;
15+ true
16 | _ -> false)
17+;;
1819let err msg = Eio.Exn.create (E msg)
20···23 match Yamlt.decode ~layout:true Cff.jsont reader with
24 | Ok cff -> cff
25 | Error msg -> raise (err msg)
26+;;
2728let to_yaml_string t =
29 let buf = Buffer.create 1024 in
···31 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
32 | Ok () -> Buffer.contents buf
33 | Error msg -> raise (err msg)
34+;;
3536let of_yaml_flow flow =
37 let reader = Bytesrw_eio.bytes_reader_of_flow flow in
38 match Yamlt.decode ~layout:true Cff.jsont reader with
39 | Ok cff -> cff
40 | Error msg -> raise (err msg)
41+;;
4243let to_yaml_flow flow t =
44 let writer = Bytesrw_eio.bytes_writer_of_flow flow in
45 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
46 | Ok () -> ()
47 | Error msg -> raise (err msg)
48+;;
4950let of_file ~fs path =
51 let data = Eio.Path.load Eio.Path.(fs / path) in
52+ try of_yaml_string data with
53+ | Eio.Exn.Io _ as ex ->
54 let bt = Printexc.get_raw_backtrace () in
55 Eio.Exn.reraise_with_context ex bt "parsing CFF file %S" path
56+;;
5758let to_file ~fs path t =
59 let data = to_yaml_string t in
60 Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) data
61+;;
+10-9
lib_eio/cff_eio.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···28 Parsing and encoding errors are raised as {!Eio.Exn.Io} exceptions
29 with the error type {!E}. *)
3031-type Eio.Exn.err += E of string
32-(** CFF parsing or encoding error. The string contains the error message. *)
03334(** {1 String Functions} *)
3536-val of_yaml_string : string -> Cff.t
37(** [of_yaml_string s] parses a CFF from YAML string [s].
3839 @raise Eio.Exn.Io on parse error. *)
04041-val to_yaml_string : Cff.t -> string
42(** [to_yaml_string cff] serializes [cff] to a YAML string.
4344 The output uses YAML block style for readability.
4546 @raise Eio.Exn.Io on encoding error. *)
04748(** {1 Flow Functions} *)
4950-val of_yaml_flow : _ Eio.Flow.source -> Cff.t
51(** [of_yaml_flow flow] parses a CFF from an Eio source flow.
5253 Reads directly from the flow using bytesrw-eio.
5455 @raise Eio.Exn.Io on parse error. *)
05657-val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit
58(** [to_yaml_flow flow cff] serializes [cff] to an Eio sink flow.
5960 Writes directly to the flow using bytesrw-eio.
6162 @raise Eio.Exn.Io on encoding error. *)
06364(** {1 File Functions} *)
6566-val of_file : fs:_ Eio.Path.t -> string -> Cff.t
67(** [of_file ~fs path] reads and parses a [CITATION.cff] file.
6869 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
70 @param path Path to the CFF file
71 @raise Eio.Exn.Io if the file cannot be read or contains invalid CFF data.
72 The exception context includes the file path. *)
07374-val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit
75(** [to_file ~fs path cff] writes [cff] to a file at [path].
7677 Creates or overwrites the file.
···79 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
80 @param path Path to write the CFF file
81 @raise Eio.Exn.Io on I/O or encoding failure. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···28 Parsing and encoding errors are raised as {!Eio.Exn.Io} exceptions
29 with the error type {!E}. *)
3031+type Eio.Exn.err +=
32+ | E of string
33+ (** CFF parsing or encoding error. The string contains the error message. *)
3435(** {1 String Functions} *)
36037(** [of_yaml_string s] parses a CFF from YAML string [s].
3839 @raise Eio.Exn.Io on parse error. *)
40+val of_yaml_string : string -> Cff.t
41042(** [to_yaml_string cff] serializes [cff] to a YAML string.
4344 The output uses YAML block style for readability.
4546 @raise Eio.Exn.Io on encoding error. *)
47+val to_yaml_string : Cff.t -> string
4849(** {1 Flow Functions} *)
50051(** [of_yaml_flow flow] parses a CFF from an Eio source flow.
5253 Reads directly from the flow using bytesrw-eio.
5455 @raise Eio.Exn.Io on parse error. *)
56+val of_yaml_flow : _ Eio.Flow.source -> Cff.t
57058(** [to_yaml_flow flow cff] serializes [cff] to an Eio sink flow.
5960 Writes directly to the flow using bytesrw-eio.
6162 @raise Eio.Exn.Io on encoding error. *)
63+val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit
6465(** {1 File Functions} *)
66067(** [of_file ~fs path] reads and parses a [CITATION.cff] file.
6869 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
70 @param path Path to the CFF file
71 @raise Eio.Exn.Io if the file cannot be read or contains invalid CFF data.
72 The exception context includes the file path. *)
73+val of_file : fs:_ Eio.Path.t -> string -> Cff.t
74075(** [to_file ~fs path cff] writes [cff] to a file at [path].
7677 Creates or overwrites the file.
···79 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
80 @param path Path to write the CFF file
81 @raise Eio.Exn.Io on I/O or encoding failure. *)
82+val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit
+8-4
lib_unix/cff_unix.ml
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8let of_yaml_string s =
9 let reader = Bytesrw.Bytes.Reader.of_string s in
10 Yamlt.decode ~layout:true Cff.jsont reader
01112let to_yaml_string t =
13 let buf = Buffer.create 1024 in
···15 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
16 | Ok () -> Ok (Buffer.contents buf)
17 | Error e -> Error e
01819let of_file path =
20 match In_channel.with_open_text path In_channel.input_all with
21 | s -> of_yaml_string s
22 | exception Sys_error e -> Error e
02324let to_file path t =
25 match to_yaml_string t with
26 | Error e -> Error e
27 | Ok s ->
28- match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with
29- | () -> Ok ()
30- | exception Sys_error e -> Error e
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···8let of_yaml_string s =
9 let reader = Bytesrw.Bytes.Reader.of_string s in
10 Yamlt.decode ~layout:true Cff.jsont reader
11+;;
1213let to_yaml_string t =
14 let buf = Buffer.create 1024 in
···16 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
17 | Ok () -> Ok (Buffer.contents buf)
18 | Error e -> Error e
19+;;
2021let of_file path =
22 match In_channel.with_open_text path In_channel.input_all with
23 | s -> of_yaml_string s
24 | exception Sys_error e -> Error e
25+;;
2627let to_file path t =
28 match to_yaml_string t with
29 | Error e -> Error e
30 | Ok s ->
31+ (match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with
32+ | () -> Ok ()
33+ | exception Sys_error e -> Error e)
34+;;
+5-5
lib_unix/cff_unix.mli
···1(*---------------------------------------------------------------------------
2- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···2223 {1 Functions} *)
2425-val of_yaml_string : string -> (Cff.t, string) result
26(** [of_yaml_string s] parses a CFF from YAML string [s].
2728 Returns [Ok cff] on success or [Error msg] with a descriptive error
29 message on failure. *)
03031-val to_yaml_string : Cff.t -> (string, string) result
32(** [to_yaml_string cff] serializes [cff] to a YAML string.
3334 The output uses YAML block style for readability. *)
03536-val of_file : string -> (Cff.t, string) result
37(** [of_file path] reads and parses a [CITATION.cff] file.
3839 Returns [Ok cff] on success or [Error msg] if the file cannot be
40 read or contains invalid CFF data. *)
04142-val to_file : string -> Cff.t -> (unit, string) result
43(** [to_file path cff] writes [cff] to a file at [path].
4445 Creates or overwrites the file. Returns [Error msg] on I/O failure. *)
0
···1(*---------------------------------------------------------------------------
2+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5···2223 {1 Functions} *)
24025(** [of_yaml_string s] parses a CFF from YAML string [s].
2627 Returns [Ok cff] on success or [Error msg] with a descriptive error
28 message on failure. *)
29+val of_yaml_string : string -> (Cff.t, string) result
30031(** [to_yaml_string cff] serializes [cff] to a YAML string.
3233 The output uses YAML block style for readability. *)
34+val to_yaml_string : Cff.t -> (string, string) result
35036(** [of_file path] reads and parses a [CITATION.cff] file.
3738 Returns [Ok cff] on success or [Error msg] if the file cannot be
39 read or contains invalid CFF data. *)
40+val of_file : string -> (Cff.t, string) result
41042(** [to_file path cff] writes [cff] to a file at [path].
4344 Creates or overwrites the file. Returns [Error msg] on I/O failure. *)
45+val to_file : string -> Cff.t -> (unit, string) result