···11+ISC License
22+33+Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1010+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1111+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1212+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1313+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1414+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+82
README.md
···11+# ocaml-cff
22+33+Citation File Format (CFF) codec for OCaml.
44+55+A library for parsing and generating `CITATION.cff` files following the
66+[CFF 1.2.0 specification](https://github.com/citation-file-format/citation-file-format).
77+CFF is a human- and machine-readable format for software and dataset citation metadata.
88+99+## Features
1010+1111+- Full CFF 1.2.0 specification support
1212+- SPDX license expression parsing via `spdx_licenses`
1313+- Lenient parsing (accepts unknown fields, preserves non-SPDX licenses for round-tripping)
1414+- Multiple I/O backends:
1515+ - `cff.unix` - Unix file I/O using `In_channel`/`Out_channel`
1616+ - `cff.eio` - Eio-based async I/O
1717+1818+## Installation
1919+2020+```
2121+opam install cff
2222+```
2323+2424+## Usage
2525+2626+### Reading a CITATION.cff file
2727+2828+```ocaml
2929+(* Using cff.unix *)
3030+match Cff_unix.of_file "CITATION.cff" with
3131+| Ok cff ->
3232+ Printf.printf "Title: %s\n" (Cff.title cff);
3333+ Printf.printf "Authors: %d\n" (List.length (Cff.authors cff))
3434+| Error msg ->
3535+ Printf.eprintf "Error: %s\n" msg
3636+```
3737+3838+### Creating a CFF record programmatically
3939+4040+```ocaml
4141+let author = Cff.Author.person
4242+ ~family_names:"Smith"
4343+ ~given_names:"Jane"
4444+ ~orcid:"https://orcid.org/0000-0001-2345-6789"
4545+ () in
4646+let cff = Cff.make
4747+ ~title:"My Research Software"
4848+ ~authors:[author]
4949+ ~version:"1.0.0"
5050+ ~doi:"10.5281/zenodo.1234567"
5151+ ~license:(`Spdx (Spdx_licenses.parse_exn "MIT"))
5252+ ()
5353+```
5454+5555+### Writing a CITATION.cff file
5656+5757+```ocaml
5858+match Cff_unix.to_file "CITATION.cff" cff with
5959+| Ok () -> print_endline "Written successfully"
6060+| Error msg -> Printf.eprintf "Error: %s\n" msg
6161+```
6262+6363+## API Overview
6464+6565+### Core Types
6666+6767+- `Cff.t` - A complete CFF document
6868+- `Cff.Author.t` - Person or entity (polymorphic variant)
6969+- `Cff.Reference.t` - Bibliographic reference
7070+- `Cff.License.t` - SPDX expression or custom license with optional URL
7171+- `Cff.Identifier.t` - DOI, URL, SWH, or custom identifier
7272+7373+### Submodules
7474+7575+- `Cff.Person` - Individual author with name fields
7676+- `Cff.Entity` - Organization, team, or conference
7777+- `Cff.Date` - ISO 8601 date as `(year, month, day)` tuple
7878+- `Cff.Reference` - Full bibliographic reference with 60+ fields
7979+8080+## License
8181+8282+ISC License. See [LICENSE.md](LICENSE.md).
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566(** Citation File Format (CFF) codec for OCaml.
7788- This library provides types and codecs for the
88+ This library provides codecs for the
99 {{:https://citation-file-format.github.io/}Citation File Format (CFF)}
1010 version 1.2.0, a human- and machine-readable format for software and
1111 dataset citation metadata.
···2222 - [title]: The name of the software or dataset
2323 - [authors]: A list of persons and/or entities
24242525- {1 Quick Start}
2626-2725 {2 Creating a CFF record}
28262927 {[
3030- let author = Cff.Author.Person
3131- (Cff.Person.make ~family_names:"Smith" ~given_names:"Jane" ()) in
2828+ let author = Cff.Author.person
2929+ ~family_names:"Smith" ~given_names:"Jane" () in
3230 let cff = Cff.make
3331 ~title:"My Research Software"
3432 ~authors:[author]
···3735 ()
3836 ]}
39374040- {2 File I/O}
3838+ {2 I/O}
41394240 For file operations, use the backend-specific subpackages:
4341 - [cff.unix] - Unix file I/O using {!In_channel}/{!Out_channel}
···54525553 This implementation follows the
5654 {{:https://github.com/citation-file-format/citation-file-format}CFF 1.2.0 specification}.
5757- Key concepts include:
5555+ Useful modules include:
58565957 - {!module:Author}: Can be persons (with family/given names) or entities
6058 (organizations, identified by a [name] field)
···10098 Used for author and entity addresses. *)
10199module Country = Cff_country
102100101101+(** Physical address information.
102102+103103+ Address fields used for persons and entities: street address, city,
104104+ region (state/province), postal code, and country code. *)
105105+module Address = Cff_address.Address
106106+107107+(** Contact information.
108108+109109+ Contact fields used for persons and entities: email, telephone, fax,
110110+ website URL, and ORCID identifier. *)
111111+module Contact = Cff_address.Contact
112112+103113(** SPDX license identifiers.
104114105115 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers} for
···160170161171(** {1 Construction} *)
162172163163-val default_cff_version : string
164173(** The default CFF version used when not specified: ["1.2.0"]. *)
174174+val default_cff_version : string
165175166166-val default_message : string
167176(** The default citation message:
168177 ["If you use this software, please cite it using the metadata from this file."] *)
178178+val default_message : string
169179170170-val make :
171171- ?cff_version:string ->
172172- ?message:string ->
173173- title:string ->
174174- authors:Author.t list ->
175175- ?abstract:string ->
176176- ?commit:string ->
177177- ?contact:Author.t list ->
178178- ?date_released:Date.t ->
179179- ?doi:string ->
180180- ?identifiers:Identifier.t list ->
181181- ?keywords:string list ->
182182- ?license:License.t ->
183183- ?license_url:string ->
184184- ?preferred_citation:Reference.t ->
185185- ?references:Reference.t list ->
186186- ?repository:string ->
187187- ?repository_artifact:string ->
188188- ?repository_code:string ->
189189- ?type_:Cff_type.t ->
190190- ?url:string ->
191191- ?version:string ->
192192- unit -> t
193180(** [make ~title ~authors ...] constructs a CFF value.
194181195182 @param cff_version The CFF schema version (default: {!default_cff_version})
196183 @param message Instructions for users on how to cite (default: {!default_message})
197184 @param title The name of the software or dataset
198185 @param authors List of persons and/or entities who created the work *)
186186+val make
187187+ : ?cff_version:string
188188+ -> ?message:string
189189+ -> title:string
190190+ -> authors:Author.t list
191191+ -> ?abstract:string
192192+ -> ?commit:string
193193+ -> ?contact:Author.t list
194194+ -> ?date_released:Date.t
195195+ -> ?doi:string
196196+ -> ?identifiers:Identifier.t list
197197+ -> ?keywords:string list
198198+ -> ?license:License.t
199199+ -> ?preferred_citation:Reference.t
200200+ -> ?references:Reference.t list
201201+ -> ?repository:string
202202+ -> ?repository_artifact:string
203203+ -> ?repository_code:string
204204+ -> ?type_:Cff_type.t
205205+ -> ?url:string
206206+ -> ?version:string
207207+ -> unit
208208+ -> t
199209200210(** {2 Required Fields} *)
201211202202-val cff_version : t -> string
203212(** The CFF schema version that this file adheres to.
204213205214 For CFF 1.2.0 files, this should be ["1.2.0"]. The version determines
206215 which keys are valid and how they should be interpreted. *)
216216+val cff_version : t -> string
207217208208-val message : t -> string
209218(** A message to readers explaining how to cite the work.
210219211220 Common examples:
···213222 - ["Please cite this software using the metadata from 'preferred-citation'."]
214223215224 The message should guide users toward the preferred citation method. *)
225225+val message : t -> string
216226217217-val title : t -> string
218227(** The name of the software or dataset.
219228220229 This is the title that should appear in citations. For software, it's
221230 typically the project name; for datasets, the dataset title. *)
231231+val title : t -> string
222232223223-val authors : t -> Author.t list
224233(** The creators of the software or dataset.
225234226235 Authors can be persons (individuals) or entities (organizations).
227236 At least one author is required for a valid CFF file. The order
228237 typically reflects contribution significance. *)
238238+val authors : t -> Author.t list
229239230240(** {2 Optional Fields} *)
231241232232-val abstract : t -> string option
233242(** A description of the software or dataset.
234243235244 Provides context about what the work does, its purpose, and scope. *)
245245+val abstract : t -> string option
236246237237-val commit : t -> string option
238247(** The commit hash or revision number of the software version.
239248240249 Useful for precise version identification beyond semantic versioning.
241250 Example: ["1ff847d81f29c45a3a1a5ce73d38e45c2f319bba"] *)
251251+val commit : t -> string option
242252243243-val contact : t -> Author.t list option
244253(** Contact persons or entities for the software or dataset.
245254246255 May differ from authors; useful when the primary contact is a
247256 project maintainer rather than the original author. *)
257257+val contact : t -> Author.t list option
248258249249-val date_released : t -> Date.t option
250259(** The date when the software or dataset was released.
251260252261 Format is [(year, month, day)], corresponding to ISO 8601 [YYYY-MM-DD]. *)
262262+val date_released : t -> Date.t option
253263254254-val doi : t -> string option
255264(** The Digital Object Identifier for the software or dataset.
256265257266 DOIs provide persistent, citable identifiers. This is a shorthand
258267 for a single DOI; use {!identifiers} for multiple DOIs or other
259268 identifier types. Example: ["10.5281/zenodo.1234567"] *)
269269+val doi : t -> string option
260270261261-val identifiers : t -> Identifier.t list option
262271(** Additional identifiers beyond the primary DOI.
263272264273 Each identifier has a type (DOI, URL, SWH, other), value, and
265274 optional description. Useful for versioned DOIs, Software Heritage
266275 identifiers, or repository URLs. *)
276276+val identifiers : t -> Identifier.t list option
267277268268-val keywords : t -> string list option
269278(** Descriptive keywords for the work.
270279271280 Help with discoverability and categorization. Example:
272281 [["machine learning"; "image processing"; "python"]] *)
282282+val keywords : t -> string list option
273283274274-val license : t -> License.t option
275284(** The SPDX license identifier(s) for the work.
276285277286 Uses {{:https://spdx.org/licenses/}SPDX identifiers}. Multiple
278287 licenses imply an OR relationship (user may choose any).
279288 Example: ["MIT"], ["Apache-2.0"], or [["GPL-3.0-only"; "MIT"]]. *)
280280-281281-val license_url : t -> string option
282282-(** URL to the license text for non-standard licenses.
283283-284284- Only needed for licenses not in the SPDX list. Standard SPDX
285285- licenses have well-known URLs. *)
289289+val license : t -> License.t option
286290287287-val preferred_citation : t -> Reference.t option
288291(** A reference to cite instead of the software itself.
289292290293 Used for "credit redirection" when authors prefer citation of
291294 a related publication (e.g., a methods paper) over the software.
292295 Note: Software citation principles recommend citing software
293296 directly; use this field judiciously. *)
297297+val preferred_citation : t -> Reference.t option
294298295295-val references : t -> Reference.t list option
296299(** Works that this software cites or depends upon.
297300298301 Functions like a bibliography, listing dependencies, foundational
299302 works, or related publications. Each reference includes full
300303 bibliographic metadata. *)
304304+val references : t -> Reference.t list option
301305302302-val repository : t -> string option
303306(** URL to the repository where the software is developed.
304307305308 Typically a version control system URL. For source code repositories,
306309 prefer {!repository_code}. *)
310310+val repository : t -> string option
307311308308-val repository_artifact : t -> string option
309312(** URL to the built/compiled artifact repository.
310313311314 For binary distributions, package registries (npm, PyPI, CRAN),
312315 or container registries. *)
316316+val repository_artifact : t -> string option
313317314314-val repository_code : t -> string option
315318(** URL to the source code repository.
316319317320 Typically a GitHub, GitLab, or similar URL where the source
318321 code is publicly accessible. *)
322322+val repository_code : t -> string option
319323320320-val type_ : t -> Cff_type.t option
321324(** The type of work: [`Software] (default) or [`Dataset].
322325323326 Most CFF files describe software; use [`Dataset] for data packages. *)
327327+val type_ : t -> Cff_type.t option
324328325325-val url : t -> string option
326329(** The URL of the software or dataset homepage.
327330328331 A general landing page, documentation site, or project website. *)
332332+val url : t -> string option
329333330330-val version : t -> string option
331334(** The version string of the software or dataset.
332335333336 Can be any version format: semantic versioning (["1.2.3"]),
334337 date-based (["2024.01"]), or other schemes. *)
338338+val version : t -> string option
335339336340(** {1 Formatting and Codec} *)
337341342342+(** Pretty-print a CFF value in a human-readable YAML-like format. *)
338343val pp : Format.formatter -> t -> unit
339339-(** Pretty-print a CFF value in a human-readable YAML-like format. *)
340344341341-val jsont : t Jsont.t
342345(** JSON/YAML codec for serialization and deserialization.
343346344347 Used internally by the YAML codec functions. *)
348348+val jsont : t Jsont.t
+43-55
lib/cff_address.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···7788(** Physical address information. *)
99module Address = struct
1010- type t = {
1111- address : string option;
1212- city : string option;
1313- region : string option;
1414- post_code : string option;
1515- country : string option; (* ISO 3166-1 alpha-2 *)
1616- }
1010+ type t =
1111+ { address : string option
1212+ ; city : string option
1313+ ; region : string option
1414+ ; post_code : string option
1515+ ; country : string option (* ISO 3166-1 alpha-2 *)
1616+ }
17171818- let empty = {
1919- address = None;
2020- city = None;
2121- region = None;
2222- post_code = None;
2323- country = None;
2424- }
1818+ let empty =
1919+ { address = None; city = None; region = None; post_code = None; country = None }
2020+ ;;
25212622 let make ?address ?city ?region ?post_code ?country () =
2723 { address; city; region; post_code; country }
2424+ ;;
28252926 let of_options ~address ~city ~region ~post_code ~country =
3027 { address; city; region; post_code; country }
2828+ ;;
31293230 let address t = t.address
3331 let city t = t.city
···3634 let country t = t.country
37353836 let is_empty t =
3939- t.address = None && t.city = None && t.region = None &&
4040- t.post_code = None && t.country = None
3737+ t.address = None
3838+ && t.city = None
3939+ && t.region = None
4040+ && t.post_code = None
4141+ && t.country = None
4242+ ;;
41434244 let pp ppf t =
4343- let parts = List.filter_map Fun.id [
4444- t.address;
4545- t.city;
4646- t.region;
4747- t.post_code;
4848- t.country;
4949- ] in
4545+ let parts =
4646+ List.filter_map Fun.id [ t.address; t.city; t.region; t.post_code; t.country ]
4747+ in
5048 Format.pp_print_string ppf (String.concat ", " parts)
4949+ ;;
51505251 let jsont_fields ~get obj =
5352 obj
···5655 |> Jsont.Object.opt_mem "region" Jsont.string ~enc:(fun x -> (get x).region)
5756 |> Jsont.Object.opt_mem "post-code" Jsont.string ~enc:(fun x -> (get x).post_code)
5857 |> Jsont.Object.opt_mem "country" Jsont.string ~enc:(fun x -> (get x).country)
5858+ ;;
5959end
60606161(** Contact information. *)
6262module Contact = struct
6363- type t = {
6464- email : string option;
6565- tel : string option;
6666- fax : string option;
6767- website : string option;
6868- orcid : string option;
6969- }
6363+ type t =
6464+ { email : string option
6565+ ; tel : string option
6666+ ; fax : string option
6767+ ; website : string option
6868+ ; orcid : string option
6969+ }
70707171- let empty = {
7272- email = None;
7373- tel = None;
7474- fax = None;
7575- website = None;
7676- orcid = None;
7777- }
7878-7979- let make ?email ?tel ?fax ?website ?orcid () =
8080- { email; tel; fax; website; orcid }
8181-8282- let of_options ~email ~tel ~fax ~website ~orcid =
8383- { email; tel; fax; website; orcid }
8484-7171+ let empty = { email = None; tel = None; fax = None; website = None; orcid = None }
7272+ let make ?email ?tel ?fax ?website ?orcid () = { email; tel; fax; website; orcid }
7373+ let of_options ~email ~tel ~fax ~website ~orcid = { email; tel; fax; website; orcid }
8574 let email t = t.email
8675 let tel t = t.tel
8776 let fax t = t.fax
···8978 let orcid t = t.orcid
90799180 let is_empty t =
9292- t.email = None && t.tel = None && t.fax = None &&
9393- t.website = None && t.orcid = None
8181+ t.email = None && t.tel = None && t.fax = None && t.website = None && t.orcid = None
8282+ ;;
94839584 let pp ppf t =
9696- let parts = List.filter_map (fun (k, v) ->
9797- Option.map (fun v -> k ^ ": " ^ v) v
9898- ) [
9999- ("email", t.email);
100100- ("tel", t.tel);
101101- ("website", t.website);
102102- ("orcid", t.orcid);
103103- ] in
8585+ let parts =
8686+ List.filter_map
8787+ (fun (k, v) -> Option.map (fun v -> k ^ ": " ^ v) v)
8888+ [ "email", t.email; "tel", t.tel; "website", t.website; "orcid", t.orcid ]
8989+ in
10490 Format.pp_print_string ppf (String.concat ", " parts)
9191+ ;;
1059210693 let jsont_fields ~get obj =
10794 obj
···11097 |> Jsont.Object.opt_mem "fax" Jsont.string ~enc:(fun x -> (get x).fax)
11198 |> Jsont.Object.opt_mem "website" Jsont.string ~enc:(fun x -> (get x).website)
11299 |> Jsont.Object.opt_mem "orcid" Jsont.string ~enc:(fun x -> (get x).orcid)
100100+ ;;
113101end
+71-57
lib/cff_address.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···48484949 All fields are optional; an empty address is valid. *)
5050module Address : sig
5151- type t
5251 (** Physical address record. *)
5252+ type t
53535454- val empty : t
5554 (** Empty address with all fields [None]. *)
5555+ val empty : t
56565757- val make :
5858- ?address:string ->
5959- ?city:string ->
6060- ?region:string ->
6161- ?post_code:string ->
6262- ?country:string ->
6363- unit -> t
6457 (** Create an address with optional fields.
65586659 @param address Street address
···6861 @param region State, province, or administrative region
6962 @param post_code Postal code, ZIP code, or postcode
7063 @param country ISO 3166-1 alpha-2 country code *)
6464+ val make
6565+ : ?address:string
6666+ -> ?city:string
6767+ -> ?region:string
6868+ -> ?post_code:string
6969+ -> ?country:string
7070+ -> unit
7171+ -> t
71727272- val of_options :
7373- address:string option ->
7474- city:string option ->
7575- region:string option ->
7676- post_code:string option ->
7777- country:string option ->
7878- t
7973 (** Create an address from option values directly.
80748175 Used internally by jsont decoders where fields are decoded as options. *)
7676+ val of_options
7777+ : address:string option
7878+ -> city:string option
7979+ -> region:string option
8080+ -> post_code:string option
8181+ -> country:string option
8282+ -> t
82838383- val address : t -> string option
8484 (** Street address (e.g., ["77 Massachusetts Avenue"]). *)
8585+ val address : t -> string option
85868686- val city : t -> string option
8787 (** City name (e.g., ["Cambridge"], ["London"]). *)
8888+ val city : t -> string option
88898989- val region : t -> string option
9090 (** State, province, or region (e.g., ["Massachusetts"], ["Bavaria"]). *)
9191+ val region : t -> string option
91929292- val post_code : t -> string option
9393 (** Postal or ZIP code (e.g., ["02139"], ["W1A 1AA"]). *)
9494+ val post_code : t -> string option
94959595- val country : t -> string option
9696 (** ISO 3166-1 alpha-2 country code (e.g., ["US"], ["DE"], ["GB"]). *)
9797+ val country : t -> string option
97989898- val is_empty : t -> bool
9999 (** [true] if all fields are [None]. *)
100100+ val is_empty : t -> bool
100101101101- val pp : Format.formatter -> t -> unit
102102 (** Pretty-print the address. *)
103103+ val pp : Format.formatter -> t -> unit
103104104104- val jsont_fields :
105105- get:('a -> t) ->
106106- ('a, string option -> string option -> string option ->
107107- string option -> string option -> 'b) Jsont.Object.map ->
108108- ('a, 'b) Jsont.Object.map
109105 (** Add address fields to a jsont object builder.
110106111107 This adds the five address fields (address, city, region, post-code,
···113109 [string option] arguments in that order.
114110115111 @param get Extracts the address from the parent type for encoding *)
112112+ val jsont_fields
113113+ : get:('a -> t)
114114+ -> ( 'a
115115+ , string option
116116+ -> string option
117117+ -> string option
118118+ -> string option
119119+ -> string option
120120+ -> 'b )
121121+ Jsont.Object.map
122122+ -> ('a, 'b) Jsont.Object.map
116123end
117124118125(** Contact information.
···120127 Electronic contact details for persons and entities. All fields
121128 are optional. *)
122129module Contact : sig
123123- type t
124130 (** Contact information record. *)
131131+ type t
125132126126- val empty : t
127133 (** Empty contact with all fields [None]. *)
134134+ val empty : t
128135129129- val make :
130130- ?email:string ->
131131- ?tel:string ->
132132- ?fax:string ->
133133- ?website:string ->
134134- ?orcid:string ->
135135- unit -> t
136136 (** Create contact information with optional fields.
137137138138 @param email Email address
···140140 @param fax Fax number (any format)
141141 @param website Website URL
142142 @param orcid ORCID identifier URL *)
143143+ val make
144144+ : ?email:string
145145+ -> ?tel:string
146146+ -> ?fax:string
147147+ -> ?website:string
148148+ -> ?orcid:string
149149+ -> unit
150150+ -> t
143151144144- val of_options :
145145- email:string option ->
146146- tel:string option ->
147147- fax:string option ->
148148- website:string option ->
149149- orcid:string option ->
150150- t
151152 (** Create contact info from option values directly.
152153153154 Used internally by jsont decoders where fields are decoded as options. *)
155155+ val of_options
156156+ : email:string option
157157+ -> tel:string option
158158+ -> fax:string option
159159+ -> website:string option
160160+ -> orcid:string option
161161+ -> t
154162155155- val email : t -> string option
156163 (** Email address (e.g., ["jane.smith\@example.org"]). *)
164164+ val email : t -> string option
157165158158- val tel : t -> string option
159166 (** Telephone number. No specific format is required. *)
167167+ val tel : t -> string option
160168161161- val fax : t -> string option
162169 (** Fax number. No specific format is required. *)
170170+ val fax : t -> string option
163171164164- val website : t -> string option
165172 (** Website URL (e.g., ["https://example.org/~jsmith"]). *)
173173+ val website : t -> string option
166174167167- val orcid : t -> string option
168175 (** ORCID identifier as a URL.
169176170177 ORCID (Open Researcher and Contributor ID) provides persistent
···173180 Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]
174181175182 Example: ["https://orcid.org/0000-0001-2345-6789"] *)
183183+ val orcid : t -> string option
176184177177- val is_empty : t -> bool
178185 (** [true] if all fields are [None]. *)
186186+ val is_empty : t -> bool
179187180180- val pp : Format.formatter -> t -> unit
181188 (** Pretty-print the contact information. *)
189189+ val pp : Format.formatter -> t -> unit
182190183183- val jsont_fields :
184184- get:('a -> t) ->
185185- ('a, string option -> string option -> string option ->
186186- string option -> string option -> 'b) Jsont.Object.map ->
187187- ('a, 'b) Jsont.Object.map
188191 (** Add contact fields to a jsont object builder.
189192190193 This adds the five contact fields (email, tel, fax, website, orcid)
···192195 [string option] arguments in that order.
193196194197 @param get Extracts the contact from the parent type for encoding *)
198198+ val jsont_fields
199199+ : get:('a -> t)
200200+ -> ( 'a
201201+ , string option
202202+ -> string option
203203+ -> string option
204204+ -> string option
205205+ -> string option
206206+ -> 'b )
207207+ Jsont.Object.map
208208+ -> ('a, 'b) Jsont.Object.map
195209end
+189-166
lib/cff_author.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566(** Person, Entity, and Author types for CFF. *)
7788-(** Person name components. *)
99-module Name = struct
1010- type t = {
1111- family_names : string option;
1212- given_names : string option;
1313- name_particle : string option; (* e.g., "von" *)
1414- name_suffix : string option; (* e.g., "Jr." *)
1515- alias : string option;
1616- }
88+(** A person (individual author/contributor). *)
99+module Person = struct
1010+ type t =
1111+ { family_names : string option
1212+ ; given_names : string option
1313+ ; name_particle : string option
1414+ ; name_suffix : string option
1515+ ; alias : string option
1616+ ; affiliation : string option
1717+ ; address : Cff_address.Address.t
1818+ ; contact : Cff_address.Contact.t
1919+ }
17201818- let empty = {
1919- family_names = None;
2020- given_names = None;
2121- name_particle = None;
2222- name_suffix = None;
2323- alias = None;
2424- }
2525-2626- let make ?family_names ?given_names ?name_particle ?name_suffix ?alias () =
2727- { family_names; given_names; name_particle; name_suffix; alias }
2121+ let make
2222+ ?family_names
2323+ ?given_names
2424+ ?name_particle
2525+ ?name_suffix
2626+ ?alias
2727+ ?affiliation
2828+ ?(address = Cff_address.Address.empty)
2929+ ?(contact = Cff_address.Contact.empty)
3030+ ()
3131+ =
3232+ { family_names
3333+ ; given_names
3434+ ; name_particle
3535+ ; name_suffix
3636+ ; alias
3737+ ; affiliation
3838+ ; address
3939+ ; contact
4040+ }
4141+ ;;
28422943 let family_names t = t.family_names
3044 let given_names t = t.given_names
3145 let name_particle t = t.name_particle
3246 let name_suffix t = t.name_suffix
3347 let alias t = t.alias
4848+ let affiliation t = t.affiliation
4949+ let address t = t.address
5050+ let contact t = t.contact
34513552 let full_name t =
3636- let parts = List.filter_map Fun.id [
3737- t.given_names;
3838- t.name_particle;
3939- t.family_names;
4040- ] in
5353+ let parts =
5454+ List.filter_map Fun.id [ t.given_names; t.name_particle; t.family_names ]
5555+ in
4156 let base = String.concat " " parts in
4257 match t.name_suffix with
4358 | Some suffix -> base ^ ", " ^ suffix
4459 | None -> base
4545-4646- let pp ppf t =
4747- Format.pp_print_string ppf (full_name t)
4848-end
4949-5050-(** A person (individual author/contributor). *)
5151-module Person = struct
5252- type t = {
5353- name : Name.t;
5454- affiliation : string option;
5555- address : Cff_address.Address.t;
5656- contact : Cff_address.Contact.t;
5757- }
5858-5959- let make
6060- ?family_names ?given_names ?name_particle ?name_suffix ?alias
6161- ?affiliation
6262- ?(address = Cff_address.Address.empty)
6363- ?(contact = Cff_address.Contact.empty)
6464- () =
6565- let name = Name.make ?family_names ?given_names ?name_particle
6666- ?name_suffix ?alias () in
6767- { name; affiliation; address; contact }
6868-6969- let name t = t.name
7070- let affiliation t = t.affiliation
7171- let address t = t.address
7272- let contact t = t.contact
7373-7474- let family_names t = Name.family_names t.name
7575- let given_names t = Name.given_names t.name
7676- let name_particle t = Name.name_particle t.name
7777- let name_suffix t = Name.name_suffix t.name
7878- let alias t = Name.alias t.name
7979- let full_name t = Name.full_name t.name
6060+ ;;
80618162 let email t = Cff_address.Contact.email t.contact
8263 let orcid t = Cff_address.Contact.orcid t.contact
···8566 let pp ppf t =
8667 Format.fprintf ppf "%s" (full_name t);
8768 Option.iter (Format.fprintf ppf " (%s)") t.affiliation
6969+ ;;
88708971 let jsont =
9090- Jsont.Object.map ~kind:"Person"
9191- (fun family_names given_names name_particle name_suffix alias
9292- affiliation address city region post_code country
9393- email tel fax website orcid ->
9494- let name = Name.make ?family_names ?given_names ?name_particle
9595- ?name_suffix ?alias () in
9696- let address = Cff_address.Address.of_options
9797- ~address ~city ~region ~post_code ~country in
9898- let contact = Cff_address.Contact.of_options
9999- ~email ~tel ~fax ~website ~orcid in
100100- { name; affiliation; address; contact })
101101- |> Jsont.Object.opt_mem "family-names" Jsont.string
102102- ~enc:(fun p -> Name.family_names p.name)
103103- |> Jsont.Object.opt_mem "given-names" Jsont.string
104104- ~enc:(fun p -> Name.given_names p.name)
105105- |> Jsont.Object.opt_mem "name-particle" Jsont.string
106106- ~enc:(fun p -> Name.name_particle p.name)
107107- |> Jsont.Object.opt_mem "name-suffix" Jsont.string
108108- ~enc:(fun p -> Name.name_suffix p.name)
109109- |> Jsont.Object.opt_mem "alias" Jsont.string
110110- ~enc:(fun p -> Name.alias p.name)
111111- |> Jsont.Object.opt_mem "affiliation" Jsont.string
112112- ~enc:(fun p -> p.affiliation)
7272+ Jsont.Object.map
7373+ ~kind:"Person"
7474+ (fun
7575+ family_names
7676+ given_names
7777+ name_particle
7878+ name_suffix
7979+ alias
8080+ affiliation
8181+ address
8282+ city
8383+ region
8484+ post_code
8585+ country
8686+ email
8787+ tel
8888+ fax
8989+ website
9090+ orcid
9191+ ->
9292+ let address =
9393+ Cff_address.Address.of_options ~address ~city ~region ~post_code ~country
9494+ in
9595+ let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in
9696+ { family_names
9797+ ; given_names
9898+ ; name_particle
9999+ ; name_suffix
100100+ ; alias
101101+ ; affiliation
102102+ ; address
103103+ ; contact
104104+ })
105105+ |> Jsont.Object.opt_mem "family-names" Jsont.string ~enc:(fun p -> p.family_names)
106106+ |> Jsont.Object.opt_mem "given-names" Jsont.string ~enc:(fun p -> p.given_names)
107107+ |> Jsont.Object.opt_mem "name-particle" Jsont.string ~enc:(fun p -> p.name_particle)
108108+ |> Jsont.Object.opt_mem "name-suffix" Jsont.string ~enc:(fun p -> p.name_suffix)
109109+ |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun p -> p.alias)
110110+ |> Jsont.Object.opt_mem "affiliation" Jsont.string ~enc:(fun p -> p.affiliation)
113111 |> Cff_address.Address.jsont_fields ~get:(fun p -> p.address)
114112 |> Cff_address.Contact.jsont_fields ~get:(fun p -> p.contact)
115113 |> Jsont.Object.skip_unknown
116114 |> Jsont.Object.finish
117117-end
118118-119119-(** Event dates for entities (e.g., conferences). *)
120120-module Event_dates = struct
121121- type t = {
122122- date_start : Cff_date.t option;
123123- date_end : Cff_date.t option;
124124- }
125125-126126- let empty = { date_start = None; date_end = None }
127127-128128- let make ?date_start ?date_end () = { date_start; date_end }
129129-130130- let date_start t = t.date_start
131131- let date_end t = t.date_end
132132-133133- let is_empty t = t.date_start = None && t.date_end = None
134134-135135- let pp ppf t =
136136- match t.date_start, t.date_end with
137137- | Some s, Some e ->
138138- Format.fprintf ppf "%a - %a" Cff_date.pp s Cff_date.pp e
139139- | Some s, None ->
140140- Format.fprintf ppf "%a -" Cff_date.pp s
141141- | None, Some e ->
142142- Format.fprintf ppf "- %a" Cff_date.pp e
143143- | None, None -> ()
115115+ ;;
144116end
145117146118(** An entity (organization, team, conference, etc.). *)
147119module Entity = struct
148148- type t = {
149149- name : string;
150150- alias : string option;
151151- address : Cff_address.Address.t;
152152- contact : Cff_address.Contact.t;
153153- event_dates : Event_dates.t;
154154- location : string option;
155155- }
120120+ type t =
121121+ { name : string
122122+ ; alias : string option
123123+ ; address : Cff_address.Address.t
124124+ ; contact : Cff_address.Contact.t
125125+ ; date_start : Cff_date.t option
126126+ ; date_end : Cff_date.t option
127127+ ; location : string option
128128+ }
156129157130 let make
158158- ~name ?alias
159159- ?(address = Cff_address.Address.empty)
160160- ?(contact = Cff_address.Contact.empty)
161161- ?date_start ?date_end ?location
162162- () =
163163- let event_dates = Event_dates.make ?date_start ?date_end () in
164164- { name; alias; address; contact; event_dates; location }
131131+ ~name
132132+ ?alias
133133+ ?(address = Cff_address.Address.empty)
134134+ ?(contact = Cff_address.Contact.empty)
135135+ ?date_start
136136+ ?date_end
137137+ ?location
138138+ ()
139139+ =
140140+ { name; alias; address; contact; date_start; date_end; location }
141141+ ;;
165142166143 let name t = t.name
167144 let alias t = t.alias
168145 let address t = t.address
169146 let contact t = t.contact
170170- let event_dates t = t.event_dates
147147+ let date_start t = t.date_start
148148+ let date_end t = t.date_end
171149 let location t = t.location
172172-173150 let email t = Cff_address.Contact.email t.contact
174151 let orcid t = Cff_address.Contact.orcid t.contact
175152 let website t = Cff_address.Contact.website t.contact
···177154 let pp ppf t =
178155 Format.pp_print_string ppf t.name;
179156 Option.iter (Format.fprintf ppf " (%s)") t.alias
157157+ ;;
180158181159 let jsont =
182182- Jsont.Object.map ~kind:"Entity"
183183- (fun name alias address city region post_code country
184184- email tel fax website orcid date_start date_end location ->
185185- let address = Cff_address.Address.of_options
186186- ~address ~city ~region ~post_code ~country in
187187- let contact = Cff_address.Contact.of_options
188188- ~email ~tel ~fax ~website ~orcid in
189189- let event_dates = Event_dates.make ?date_start ?date_end () in
190190- { name; alias; address; contact; event_dates; location })
191191- |> Jsont.Object.mem "name" Jsont.string
192192- ~enc:(fun e -> e.name)
193193- |> Jsont.Object.opt_mem "alias" Jsont.string
194194- ~enc:(fun e -> e.alias)
160160+ Jsont.Object.map
161161+ ~kind:"Entity"
162162+ (fun
163163+ name
164164+ alias
165165+ address
166166+ city
167167+ region
168168+ post_code
169169+ country
170170+ email
171171+ tel
172172+ fax
173173+ website
174174+ orcid
175175+ date_start
176176+ date_end
177177+ location
178178+ ->
179179+ let address =
180180+ Cff_address.Address.of_options ~address ~city ~region ~post_code ~country
181181+ in
182182+ let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in
183183+ { name; alias; address; contact; date_start; date_end; location })
184184+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name)
185185+ |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun e -> e.alias)
195186 |> Cff_address.Address.jsont_fields ~get:(fun e -> e.address)
196187 |> Cff_address.Contact.jsont_fields ~get:(fun e -> e.contact)
197197- |> Jsont.Object.opt_mem "date-start" Cff_date.jsont
198198- ~enc:(fun e -> Event_dates.date_start e.event_dates)
199199- |> Jsont.Object.opt_mem "date-end" Cff_date.jsont
200200- ~enc:(fun e -> Event_dates.date_end e.event_dates)
201201- |> Jsont.Object.opt_mem "location" Jsont.string
202202- ~enc:(fun e -> e.location)
188188+ |> Jsont.Object.opt_mem "date-start" Cff_date.jsont ~enc:(fun e -> e.date_start)
189189+ |> Jsont.Object.opt_mem "date-end" Cff_date.jsont ~enc:(fun e -> e.date_end)
190190+ |> Jsont.Object.opt_mem "location" Jsont.string ~enc:(fun e -> e.location)
203191 |> Jsont.Object.skip_unknown
204192 |> Jsont.Object.finish
193193+ ;;
205194end
206195207196(** An author can be either a Person or an Entity. *)
208197type t =
209209- | Person of Person.t
210210- | Entity of Entity.t
198198+ [ `Person of Person.t
199199+ | `Entity of Entity.t
200200+ ]
211201212212-let person p = Person p
213213-let entity e = Entity e
202202+let person
203203+ ?family_names
204204+ ?given_names
205205+ ?name_particle
206206+ ?name_suffix
207207+ ?alias
208208+ ?affiliation
209209+ ?address
210210+ ?contact
211211+ ()
212212+ =
213213+ `Person
214214+ (Person.make
215215+ ?family_names
216216+ ?given_names
217217+ ?name_particle
218218+ ?name_suffix
219219+ ?alias
220220+ ?affiliation
221221+ ?address
222222+ ?contact
223223+ ())
224224+;;
225225+226226+let entity ~name ?alias ?address ?contact ?date_start ?date_end ?location () =
227227+ `Entity (Entity.make ~name ?alias ?address ?contact ?date_start ?date_end ?location ())
228228+;;
214229215230let name = function
216216- | Person p -> Person.full_name p
217217- | Entity e -> Entity.name e
231231+ | `Person p -> Person.full_name p
232232+ | `Entity e -> Entity.name e
233233+;;
218234219235let orcid = function
220220- | Person p -> Person.orcid p
221221- | Entity e -> Entity.orcid e
236236+ | `Person p -> Person.orcid p
237237+ | `Entity e -> Entity.orcid e
238238+;;
222239223240let email = function
224224- | Person p -> Person.email p
225225- | Entity e -> Entity.email e
241241+ | `Person p -> Person.email p
242242+ | `Entity e -> Entity.email e
243243+;;
226244227245let pp ppf = function
228228- | Person p -> Person.pp ppf p
229229- | Entity e -> Entity.pp ppf e
246246+ | `Person p -> Person.pp ppf p
247247+ | `Entity e -> Entity.pp ppf e
248248+;;
230249231250(* Jsont codec that discriminates based on "name" field presence.
232251 If "name" is present -> Entity, otherwise -> Person *)
···237256 | _ -> false
238257 in
239258 let dec_json j =
240240- if has_name_member j then
259259+ if has_name_member j
260260+ then (
241261 match Jsont.Json.decode' Entity.jsont j with
242242- | Ok e -> Entity e
243243- | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err)
244244- else
262262+ | Ok e -> `Entity e
263263+ | Error err ->
264264+ Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err))
265265+ else (
245266 match Jsont.Json.decode' Person.jsont j with
246246- | Ok p -> Person p
247247- | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err)
267267+ | Ok p -> `Person p
268268+ | Error err ->
269269+ Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err))
248270 in
249271 let enc_author = function
250250- | Person p ->
272272+ | `Person p ->
251273 (match Jsont.Json.encode' Person.jsont p with
252274 | Ok j -> j
253275 | Error _ -> assert false)
254254- | Entity e ->
276276+ | `Entity e ->
255277 (match Jsont.Json.encode' Entity.jsont e with
256278 | Ok j -> j
257279 | Error _ -> assert false)
258280 in
259281 Jsont.json |> Jsont.map ~dec:dec_json ~enc:enc_author
282282+;;
+145-242
lib/cff_author.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···1616 of a [name] field: if present, the entry is an entity; otherwise,
1717 it's a person.
18181919- {1 Name Components}
2020-2121- CFF follows academic citation conventions for person names:
2222-2323- - {b family-names}: Last name/surname (e.g., ["Smith"], ["van Rossum"])
2424- - {b given-names}: First name(s) (e.g., ["Jane"], ["Guido"])
2525- - {b name-particle}: Connector before family name (e.g., ["von"], ["van"], ["de"])
2626- - {b name-suffix}: Generational suffix (e.g., ["Jr."], ["III"])
2727- - {b alias}: Nickname or pseudonym
2828-2929- {1 Entity Types}
3030-3131- Entities can represent various organizations:
3232-3333- - Research institutions and universities
3434- - Companies and corporations
3535- - Government agencies
3636- - Open source projects and communities
3737- - Academic conferences (with date-start/date-end)
3838- - Standards bodies
3939-4040- {1 Example}
1919+ {1 Quick Example}
41204221 {[
4343- (* A person author with contact info *)
4444- let contact = Cff.Address.Contact.make
4545- ~orcid:"https://orcid.org/0000-0001-2345-6789" () in
4646- let jane = Cff.Author.Person (Cff.Author.Person.make
2222+ (* Create a person author *)
2323+ let jane = Cff.Author.person
4724 ~family_names:"Smith"
4848- ~given_names:"Jane A."
2525+ ~given_names:"Jane"
4926 ~affiliation:"MIT"
5050- ~contact
5151- ())
5252-5353- (* A person with name particle *)
5454- let guido = Cff.Author.Person (Cff.Author.Person.make
5555- ~family_names:"Rossum"
5656- ~given_names:"Guido"
5757- ~name_particle:"van"
5858- ())
2727+ ()
59286060- (* An organization entity *)
6161- let address = Cff.Address.Address.make
6262- ~city:"San Francisco" ~country:"US" () in
6363- let contact = Cff.Address.Contact.make
6464- ~website:"https://mozilla.org" () in
6565- let mozilla = Cff.Author.Entity (Cff.Author.Entity.make
2929+ (* Create an entity author *)
3030+ let mozilla = Cff.Author.entity
6631 ~name:"Mozilla Foundation"
6767- ~address ~contact
6868- ())
6969-7070- (* A conference entity with dates *)
7171- let conf = Cff.Author.Entity (Cff.Author.Entity.make
7272- ~name:"ICSE 2024"
7373- ~date_start:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:14)
7474- ~date_end:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:20)
7575- ~location:"Lisbon, Portugal"
7676- ())
7777- ]}
7878-7979- {1 Name Components} *)
8080-8181-(** Name components for persons.
8282-8383- CFF name handling follows scholarly citation conventions to properly
8484- represent names from various cultures and naming traditions. *)
8585-module Name : sig
8686- type t
8787-8888- val empty : t
8989- (** Empty name with all components as [None]. *)
9090-9191- val make :
9292- ?family_names:string ->
9393- ?given_names:string ->
9494- ?name_particle:string ->
9595- ?name_suffix:string ->
9696- ?alias:string ->
9797- unit -> t
9898- (** Create a name with optional components.
9999-100100- @param family_names Last name/surname
101101- @param given_names First name(s)
102102- @param name_particle Connector like ["von"], ["van"], ["de"]
103103- @param name_suffix Generational suffix like ["Jr."], ["III"]
104104- @param alias Nickname or pseudonym *)
105105-106106- val family_names : t -> string option
107107- (** The person's family name (surname, last name). *)
108108-109109- val given_names : t -> string option
110110- (** The person's given name(s) (first name, forenames). *)
111111-112112- val name_particle : t -> string option
113113- (** Name connector appearing before family name.
3232+ ()
11433115115- Examples: ["von"] in "Ludwig von Beethoven",
116116- ["van"] in "Vincent van Gogh". *)
3434+ (* Pattern match on authors *)
3535+ let show_author = function
3636+ | `Person p -> Cff.Author.Person.full_name p
3737+ | `Entity e -> Cff.Author.Entity.name e
3838+ ]} *)
11739118118- val name_suffix : t -> string option
119119- (** Generational or honorary suffix.
4040+(** {1 Person}
12041121121- Examples: ["Jr."], ["Sr."], ["III"], ["PhD"]. *)
122122-123123- val alias : t -> string option
124124- (** Nickname, pseudonym, or alternative name.
125125-126126- Example: ["Tim"] for "Timothy", ["DHH"] for "David Heinemeier Hansson". *)
127127-128128- val full_name : t -> string
129129- (** Format name as "Given Particle Family, Suffix".
130130-131131- Examples:
132132- - ["Jane Smith"]
133133- - ["Guido van Rossum"]
134134- - ["John Smith, Jr."] *)
135135-136136- val pp : Format.formatter -> t -> unit
137137- (** Pretty-print the full name. *)
138138-end
139139-140140-(** Individual person (author, contributor, editor, etc.).
4242+ Individual person (author, contributor, editor, etc.).
1414314244 A person represents a human contributor with:
143143- - Name components (required: at least family or given names)
4545+ - Name components (family names, given names, particle, suffix, alias)
14446 - Optional affiliation (institution, company)
14547 - Optional physical address
14648 - Optional contact information (email, ORCID, website) *)
14749module Person : sig
5050+ (** A person record. *)
14851 type t
14952150150- val make :
151151- ?family_names:string ->
152152- ?given_names:string ->
153153- ?name_particle:string ->
154154- ?name_suffix:string ->
155155- ?alias:string ->
156156- ?affiliation:string ->
157157- ?address:Cff_address.Address.t ->
158158- ?contact:Cff_address.Contact.t ->
159159- unit -> t
16053 (** Create a person with optional fields.
1615416255 At minimum, provide [family_names] or [given_names].
1635616457 @param family_names Last name/surname
16558 @param given_names First name(s)
166166- @param name_particle Connector before family name
167167- @param name_suffix Generational suffix
5959+ @param name_particle Connector before family name (e.g., "von", "van")
6060+ @param name_suffix Generational suffix (e.g., "Jr.", "III")
16861 @param alias Nickname or pseudonym
16962 @param affiliation Institution or organization name
17063 @param address Physical address
17164 @param contact Contact information (email, ORCID, website, etc.) *)
172172-173173- val name : t -> Name.t
174174- (** The person's name components. *)
175175-176176- val affiliation : t -> string option
177177- (** The person's institutional affiliation.
178178-179179- Example: ["Massachusetts Institute of Technology"]. *)
180180-181181- val address : t -> Cff_address.Address.t
182182- (** Physical address information. *)
183183-184184- val contact : t -> Cff_address.Contact.t
185185- (** Contact information (email, phone, web, ORCID). *)
6565+ val make
6666+ : ?family_names:string
6767+ -> ?given_names:string
6868+ -> ?name_particle:string
6969+ -> ?name_suffix:string
7070+ -> ?alias:string
7171+ -> ?affiliation:string
7272+ -> ?address:Cff_address.Address.t
7373+ -> ?contact:Cff_address.Contact.t
7474+ -> unit
7575+ -> t
18676187187- (** {2 Convenience Accessors for Name} *)
7777+ (** {2 Name Fields} *)
188787979+ (** The person's family name (surname, last name). *)
18980 val family_names : t -> string option
190190- (** Shortcut for [Name.family_names (name t)]. *)
191818282+ (** The person's given name(s) (first name, forenames). *)
19283 val given_names : t -> string option
193193- (** Shortcut for [Name.given_names (name t)]. *)
194848585+ (** Name connector appearing before family name.
8686+ Examples: ["von"] in "Ludwig von Beethoven". *)
19587 val name_particle : t -> string option
196196- (** Shortcut for [Name.name_particle (name t)]. *)
197888989+ (** Generational or honorary suffix.
9090+ Examples: ["Jr."], ["Sr."], ["III"]. *)
19891 val name_suffix : t -> string option
199199- (** Shortcut for [Name.name_suffix (name t)]. *)
200929393+ (** Nickname, pseudonym, or alternative name. *)
20194 val alias : t -> string option
202202- (** Shortcut for [Name.alias (name t)]. *)
203959696+ (** Format name as "Given Particle Family, Suffix".
9797+ Examples: ["Jane Smith"], ["Guido van Rossum"]. *)
20498 val full_name : t -> string
205205- (** Shortcut for [Name.full_name (name t)]. *)
9999+100100+ (** {2 Affiliation and Location} *)
101101+102102+ (** The person's institutional affiliation. *)
103103+ val affiliation : t -> string option
104104+105105+ (** Physical address information. *)
106106+ val address : t -> Cff_address.Address.t
107107+108108+ (** {2 Contact Information} *)
206109207207- (** {2 Convenience Accessors for Contact} *)
110110+ (** Full contact information record. *)
111111+ val contact : t -> Cff_address.Contact.t
208112113113+ (** The person's email address. *)
209114 val email : t -> string option
210210- (** The person's email address. *)
211115116116+ (** The person's ORCID identifier URL. *)
212117 val orcid : t -> string option
213213- (** The person's ORCID identifier URL.
214118215215- ORCID (Open Researcher and Contributor ID) provides persistent
216216- digital identifiers for researchers. Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]. *)
217217-119119+ (** The person's website URL. *)
218120 val website : t -> string option
219219- (** The person's website URL. *)
220121221221- val pp : Format.formatter -> t -> unit
122122+ (** {2 Formatting and Codec} *)
123123+222124 (** Pretty-print as "Full Name (affiliation)". *)
125125+ val pp : Format.formatter -> t -> unit
223126224224- val jsont : t Jsont.t
225127 (** JSON/YAML codec for person records. *)
128128+ val jsont : t Jsont.t
226129end
227130228228-(** Event date range for entities like conferences.
131131+(** {1 Entity}
229132230230- Some entities (particularly conferences) have associated dates
231231- when they take place. *)
232232-module Event_dates : sig
233233- type t
234234-235235- val empty : t
236236- (** Empty date range with both dates as [None]. *)
237237-238238- val make :
239239- ?date_start:Cff_date.t ->
240240- ?date_end:Cff_date.t ->
241241- unit -> t
242242- (** Create an event date range.
243243-244244- @param date_start When the event begins
245245- @param date_end When the event ends *)
246246-247247- val date_start : t -> Cff_date.t option
248248- (** The start date of the event. *)
249249-250250- val date_end : t -> Cff_date.t option
251251- (** The end date of the event. *)
252252-253253- val is_empty : t -> bool
254254- (** [true] if both dates are [None]. *)
255255-256256- val pp : Format.formatter -> t -> unit
257257- (** Pretty-print as "YYYY-MM-DD - YYYY-MM-DD". *)
258258-end
259259-260260-(** Organization, institution, project, or conference.
133133+ Organization, institution, project, or conference.
261134262135 An entity represents a non-person author or contributor, such as:
263136 - Research institutions (["MIT"], ["CERN"])
264137 - Companies (["Google"], ["Mozilla Foundation"])
265265- - Government agencies (["NASA"], ["NIH"])
266138 - Open source projects (["The Rust Project"])
267267- - Academic conferences (["ICSE 2024"])
268268- - Standards bodies (["IEEE"], ["W3C"])
139139+ - Academic conferences (["ICSE 2024"]) with dates
269140270141 Entities are distinguished from persons in YAML by the presence
271271- of a required [name] field (persons have [family-names]/[given-names]
272272- instead). *)
142142+ of a required [name] field. *)
273143module Entity : sig
144144+ (** An entity record. *)
274145 type t
275146276276- val make :
277277- name:string ->
278278- ?alias:string ->
279279- ?address:Cff_address.Address.t ->
280280- ?contact:Cff_address.Contact.t ->
281281- ?date_start:Cff_date.t ->
282282- ?date_end:Cff_date.t ->
283283- ?location:string ->
284284- unit -> t
285147 (** Create an entity.
286148287149 @param name The entity's official name (required)
···291153 @param date_start Event start date (for conferences)
292154 @param date_end Event end date (for conferences)
293155 @param location Event location description *)
156156+ val make
157157+ : name:string
158158+ -> ?alias:string
159159+ -> ?address:Cff_address.Address.t
160160+ -> ?contact:Cff_address.Contact.t
161161+ -> ?date_start:Cff_date.t
162162+ -> ?date_end:Cff_date.t
163163+ -> ?location:string
164164+ -> unit
165165+ -> t
294166167167+ (** {2 Core Fields} *)
168168+169169+ (** The entity's official name. *)
295170 val name : t -> string
296296- (** The entity's official name. This field distinguishes entities
297297- from persons in the YAML format. *)
298171172172+ (** Short name, acronym, or alternative name. *)
299173 val alias : t -> string option
300300- (** Short name, acronym, or alternative name.
301174302302- Example: ["MIT"] for "Massachusetts Institute of Technology". *)
175175+ (** {2 Location} *)
303176177177+ (** Physical address information. *)
304178 val address : t -> Cff_address.Address.t
305305- (** Physical address information. *)
179179+180180+ (** Event location description (for conferences). *)
181181+ val location : t -> string option
306182307307- val contact : t -> Cff_address.Contact.t
308308- (** Contact information. *)
183183+ (** {2 Event Dates} *)
309184310310- val event_dates : t -> Event_dates.t
311311- (** Event dates (for conferences). *)
185185+ (** The start date of the event (for conferences). *)
186186+ val date_start : t -> Cff_date.t option
312187313313- val location : t -> string option
314314- (** Event location description (for conferences).
188188+ (** The end date of the event (for conferences). *)
189189+ val date_end : t -> Cff_date.t option
315190316316- Example: ["Lisbon, Portugal"]. *)
191191+ (** {2 Contact Information} *)
317192318318- (** {2 Convenience Accessors for Contact} *)
193193+ (** Full contact information record. *)
194194+ val contact : t -> Cff_address.Contact.t
319195320320- val email : t -> string option
321196 (** The entity's contact email. *)
197197+ val email : t -> string option
322198199199+ (** The entity's ORCID (organizations can have ORCIDs). *)
323200 val orcid : t -> string option
324324- (** The entity's ORCID (organizations can have ORCIDs). *)
325201326326- val website : t -> string option
327202 (** The entity's official website URL. *)
203203+ val website : t -> string option
328204329329- val pp : Format.formatter -> t -> unit
205205+ (** {2 Formatting and Codec} *)
206206+330207 (** Pretty-print as "Name (alias)". *)
208208+ val pp : Format.formatter -> t -> unit
331209332332- val jsont : t Jsont.t
333210 (** JSON/YAML codec for entity records. *)
211211+ val jsont : t Jsont.t
334212end
335213336336-(** {1 Author Discriminated Union}
214214+(** {1 Author Type}
337215338338- The main author type is a sum type that can hold either a person
339339- or an entity. This matches the CFF specification where authors
340340- can be either individuals or organizations. *)
216216+ The main author type is a polymorphic variant that can hold either
217217+ a person or an entity. *)
341218342342-type t =
343343- | Person of Person.t (** An individual person *)
344344- | Entity of Entity.t (** An organization or entity *)
345219(** An author: either a person or an entity. *)
220220+type t =
221221+ [ `Person of Person.t
222222+ | `Entity of Entity.t
223223+ ]
346224347347-val person : Person.t -> t
348348-(** Wrap a person as an author. *)
225225+(** {2 Smart Constructors} *)
349226350350-val entity : Entity.t -> t
351351-(** Wrap an entity as an author. *)
227227+(** Create a person author directly.
352228353353-val name : t -> string
229229+ Equivalent to [`Person (Person.make ...)]. *)
230230+val person
231231+ : ?family_names:string
232232+ -> ?given_names:string
233233+ -> ?name_particle:string
234234+ -> ?name_suffix:string
235235+ -> ?alias:string
236236+ -> ?affiliation:string
237237+ -> ?address:Cff_address.Address.t
238238+ -> ?contact:Cff_address.Contact.t
239239+ -> unit
240240+ -> t
241241+242242+(** Create an entity author directly.
243243+244244+ Equivalent to [`Entity (Entity.make ...)]. *)
245245+val entity
246246+ : name:string
247247+ -> ?alias:string
248248+ -> ?address:Cff_address.Address.t
249249+ -> ?contact:Cff_address.Contact.t
250250+ -> ?date_start:Cff_date.t
251251+ -> ?date_end:Cff_date.t
252252+ -> ?location:string
253253+ -> unit
254254+ -> t
255255+256256+(** {2 Common Accessors} *)
257257+354258(** Get the display name.
355259356260 For persons, returns the full formatted name.
357261 For entities, returns the entity name. *)
262262+val name : t -> string
358263359359-val orcid : t -> string option
360264(** Get the ORCID if present. Works for both persons and entities. *)
265265+val orcid : t -> string option
361266267267+(** Get the email if present. Works for both persons and entities. *)
362268val email : t -> string option
363363-(** Get the email if present. Works for both persons and entities. *)
364269365365-val pp : Format.formatter -> t -> unit
270270+(** {2 Formatting and Codec} *)
271271+366272(** Pretty-print the author. *)
273273+val pp : Format.formatter -> t -> unit
367274368368-val jsont : t Jsont.t
369275(** JSON/YAML codec that discriminates based on [name] field presence.
370276371277 When decoding:
372278 - If the object has a [name] field -> Entity
373373- - Otherwise -> Person
374374-375375- This matches the CFF specification where entities are distinguished
376376- by having a [name] field while persons have [family-names] and
377377- [given-names] fields. *)
279279+ - Otherwise -> Person *)
280280+val jsont : t Jsont.t
+10-13
lib/cff_country.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···1313 try
1414 let _ = ISO3166.alpha2_of_string s in
1515 Ok s
1616- with Invalid_argument _ ->
1717- Error (`Invalid_country s)
1616+ with
1717+ | Invalid_argument _ -> Error (`Invalid_country s)
1818+;;
18191920let to_string t = t
20212122let to_iso3166 t =
2222- try
2323- Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t))
2424- with Invalid_argument _ ->
2525- None
2323+ try Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t)) with
2424+ | Invalid_argument _ -> None
2525+;;
26262727let name t = Option.map ISO3166.Country.name (to_iso3166 t)
2828-2928let equal = String.equal
3029let compare = String.compare
3131-3232-let pp ppf t =
3333- Format.pp_print_string ppf t
3030+let pp ppf t = Format.pp_print_string ppf t
34313532(* Jsont codec for country codes *)
3633let jsont =
···4138 Jsont.Error.msgf Jsont.Meta.none "Invalid ISO 3166-1 alpha-2 country code: %s" s
4239 in
4340 let enc t = to_string t in
4444- Jsont.string
4545- |> Jsont.map ~dec ~enc
4141+ Jsont.string |> Jsont.map ~dec ~enc
4242+;;
46434744(* Lenient codec that accepts any string *)
4845let jsont_lenient = Jsont.string
+11-11
lib/cff_country.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···3939 country: DE
4040 ]} *)
41414242-type t = string
4342(** An ISO 3166-1 alpha-2 country code (two uppercase letters). *)
4343+type t = string
44444545-val of_string : string -> (t, [> `Invalid_country of string]) result
4645(** Parse and validate a country code.
47464847 Case-insensitive: ["us"], ["US"], and ["Us"] all produce ["US"].
4948 Returns [Error (`Invalid_country s)] for unknown codes. *)
4949+val of_string : string -> (t, [> `Invalid_country of string ]) result
50505151+(** Return the uppercase country code. *)
5152val to_string : t -> string
5252-(** Return the uppercase country code. *)
53535454-val to_iso3166 : t -> ISO3166.Country.t option
5554(** Look up the full country record from {!ISO3166}.
56555756 Returns [None] if the code is not in the ISO 3166-1 list. *)
5757+val to_iso3166 : t -> ISO3166.Country.t option
58585959-val name : t -> string option
6059(** Get the country name if the code is valid.
61606261 Examples:
6362 - [name "US" = Some "United States of America"]
6463 - [name "GB" = Some "United Kingdom of Great Britain and Northern Ireland"]
6564 - [name "XX" = None] *)
6565+val name : t -> string option
66666767+(** Country code equality (case-sensitive after normalization). *)
6768val equal : t -> t -> bool
6868-(** Country code equality (case-sensitive after normalization). *)
69697070-val compare : t -> t -> int
7170(** Alphabetical comparison of country codes. *)
7171+val compare : t -> t -> int
72727373+(** Pretty-print the country code. *)
7374val pp : Format.formatter -> t -> unit
7474-(** Pretty-print the country code. *)
75757676-val jsont : t Jsont.t
7776(** JSON/YAML codec that validates country codes.
78777978 Returns an error for invalid ISO 3166-1 alpha-2 codes. *)
7979+val jsont : t Jsont.t
80808181-val jsont_lenient : t Jsont.t
8281(** JSON/YAML codec that accepts any string.
83828483 Use this when parsing CFF files that may contain non-standard
8584 country codes. *)
8585+val jsont_lenient : t Jsont.t
+10-17
lib/cff_date.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···1010let of_string s =
1111 (* CFF dates are YYYY-MM-DD format *)
1212 match String.split_on_char '-' s with
1313- | [y; m; d] ->
1313+ | [ y; m; d ] ->
1414 (match int_of_string_opt y, int_of_string_opt m, int_of_string_opt d with
1515 | Some year, Some month, Some day ->
1616 (* Validate the date components *)
1717- if year >= 0 && year <= 9999 &&
1818- month >= 1 && month <= 12 &&
1919- day >= 1 && day <= 31 then
2020- Ok (year, month, day)
2121- else
2222- Error (`Invalid_date s)
1717+ if year >= 0 && year <= 9999 && month >= 1 && month <= 12 && day >= 1 && day <= 31
1818+ then Ok (year, month, day)
1919+ else Error (`Invalid_date s)
2320 | _ -> Error (`Invalid_date s))
2421 | _ -> Error (`Invalid_date s)
2222+;;
25232626-let to_string (year, month, day) =
2727- Printf.sprintf "%04d-%02d-%02d" year month day
2828-2424+let to_string (year, month, day) = Printf.sprintf "%04d-%02d-%02d" year month day
2925let year (y, _, _) = y
3026let month (_, m, _) = m
3127let day (_, _, d) = d
3232-3328let equal a b = a = b
3429let compare = Stdlib.compare
3535-3636-let pp ppf date =
3737- Format.pp_print_string ppf (to_string date)
3030+let pp ppf date = Format.pp_print_string ppf (to_string date)
38313932(* Jsont codec for dates *)
4033let jsont =
···4538 Jsont.Error.msgf Jsont.Meta.none "Invalid date format: %s" s
4639 in
4740 let enc date = to_string date in
4848- Jsont.string
4949- |> Jsont.map ~dec ~enc
4141+ Jsont.string |> Jsont.map ~dec ~enc
4242+;;
+11-11
lib/cff_date.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···4545 For historical works or when only the year is known, use the [year]
4646 field (an integer) instead of a full date. *)
47474848-type t = Ptime.date
4948(** A date as [(year, month, day)] tuple.
50495150 The tuple contains:
5251 - [year]: Four-digit year (e.g., [2024])
5352 - [month]: Month number (1-12)
5453 - [day]: Day of month (1-31) *)
5454+type t = Ptime.date
55555656-val of_string : string -> (t, [> `Invalid_date of string]) result
5756(** Parse a date from [YYYY-MM-DD] format.
58575958 Returns [Error (`Invalid_date s)] if the string is not a valid date.
6059 Validates that the date is a real calendar date (e.g., rejects Feb 30). *)
6060+val of_string : string -> (t, [> `Invalid_date of string ]) result
61616262-val to_string : t -> string
6362(** Format a date as [YYYY-MM-DD]. *)
6363+val to_string : t -> string
64646565+(** Extract the year component. *)
6566val year : t -> int
6666-(** Extract the year component. *)
67676868-val month : t -> int
6968(** Extract the month component (1-12). *)
6969+val month : t -> int
70707171-val day : t -> int
7271(** Extract the day component (1-31). *)
7272+val day : t -> int
73737474+(** Date equality. *)
7475val equal : t -> t -> bool
7575-(** Date equality. *)
76767777-val compare : t -> t -> int
7877(** Date comparison (chronological order). *)
7878+val compare : t -> t -> int
79798080+(** Pretty-print a date in [YYYY-MM-DD] format. *)
8081val pp : Format.formatter -> t -> unit
8181-(** Pretty-print a date in [YYYY-MM-DD] format. *)
82828383-val jsont : t Jsont.t
8483(** JSON/YAML codec for dates.
85848685 Parses strings in [YYYY-MM-DD] format and serializes back to the
8786 same format. *)
8787+val jsont : t Jsont.t
+229-204
lib/cff_enums.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···88(** Functor to generate common enum operations. *)
99module type STRING_ENUM = sig
1010 type t
1111+1112 val of_string : string -> t option
1213 val to_string : t -> string
1314 val type_name : string
···15161617module Make_enum (E : STRING_ENUM) = struct
1718 include E
1919+1820 let equal (a : t) (b : t) = a = b
1921 let compare = Stdlib.compare
2022 let pp ppf t = Format.pp_print_string ppf (to_string t)
2323+2124 let jsont =
2222- Jsont.string |> Jsont.map
2323- ~dec:(fun s ->
2424- match of_string s with
2525- | Some t -> t
2626- | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s)
2727- ~enc:to_string
2525+ Jsont.string
2626+ |> Jsont.map
2727+ ~dec:(fun s ->
2828+ match of_string s with
2929+ | Some t -> t
3030+ | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s)
3131+ ~enc:to_string
3232+ ;;
2833end
29343035module Identifier_type = Make_enum (struct
3131- type t = [ `Doi | `Url | `Swh | `Other ]
3232- let type_name = "identifier type"
3636+ type t =
3737+ [ `Doi
3838+ | `Url
3939+ | `Swh
4040+ | `Other
4141+ ]
4242+4343+ let type_name = "identifier type"
33443434- let of_string = function
3535- | "doi" -> Some `Doi
3636- | "url" -> Some `Url
3737- | "swh" -> Some `Swh
3838- | "other" -> Some `Other
3939- | _ -> None
4545+ let of_string = function
4646+ | "doi" -> Some `Doi
4747+ | "url" -> Some `Url
4848+ | "swh" -> Some `Swh
4949+ | "other" -> Some `Other
5050+ | _ -> None
5151+ ;;
40524141- let to_string = function
4242- | `Doi -> "doi"
4343- | `Url -> "url"
4444- | `Swh -> "swh"
4545- | `Other -> "other"
4646-end)
5353+ let to_string = function
5454+ | `Doi -> "doi"
5555+ | `Url -> "url"
5656+ | `Swh -> "swh"
5757+ | `Other -> "other"
5858+ ;;
5959+ end)
47604861module Reference_type = Make_enum (struct
4949- type t = [
5050- | `Art
5151- | `Article
5252- | `Audiovisual
5353- | `Bill
5454- | `Blog
5555- | `Book
5656- | `Catalogue
5757- | `Conference
5858- | `Conference_paper
5959- | `Data
6060- | `Database
6161- | `Dictionary
6262- | `Edited_work
6363- | `Encyclopedia
6464- | `Film_broadcast
6565- | `Generic
6666- | `Government_document
6767- | `Grant
6868- | `Hearing
6969- | `Historical_work
7070- | `Legal_case
7171- | `Legal_rule
7272- | `Magazine_article
7373- | `Manual
7474- | `Map
7575- | `Multimedia
7676- | `Music
7777- | `Newspaper_article
7878- | `Pamphlet
7979- | `Patent
8080- | `Personal_communication
8181- | `Proceedings
8282- | `Report
8383- | `Serial
8484- | `Slides
8585- | `Software
8686- | `Software_code
8787- | `Software_container
8888- | `Software_executable
8989- | `Software_virtual_machine
9090- | `Sound_recording
9191- | `Standard
9292- | `Statute
9393- | `Thesis
9494- | `Unpublished
9595- | `Video
9696- | `Website
9797- ]
9898- let type_name = "reference type"
6262+ type t =
6363+ [ `Art
6464+ | `Article
6565+ | `Audiovisual
6666+ | `Bill
6767+ | `Blog
6868+ | `Book
6969+ | `Catalogue
7070+ | `Conference
7171+ | `Conference_paper
7272+ | `Data
7373+ | `Database
7474+ | `Dictionary
7575+ | `Edited_work
7676+ | `Encyclopedia
7777+ | `Film_broadcast
7878+ | `Generic
7979+ | `Government_document
8080+ | `Grant
8181+ | `Hearing
8282+ | `Historical_work
8383+ | `Legal_case
8484+ | `Legal_rule
8585+ | `Magazine_article
8686+ | `Manual
8787+ | `Map
8888+ | `Multimedia
8989+ | `Music
9090+ | `Newspaper_article
9191+ | `Pamphlet
9292+ | `Patent
9393+ | `Personal_communication
9494+ | `Proceedings
9595+ | `Report
9696+ | `Serial
9797+ | `Slides
9898+ | `Software
9999+ | `Software_code
100100+ | `Software_container
101101+ | `Software_executable
102102+ | `Software_virtual_machine
103103+ | `Sound_recording
104104+ | `Standard
105105+ | `Statute
106106+ | `Thesis
107107+ | `Unpublished
108108+ | `Video
109109+ | `Website
110110+ ]
111111+112112+ let type_name = "reference type"
99113100100- let of_string = function
101101- | "art" -> Some `Art
102102- | "article" -> Some `Article
103103- | "audiovisual" -> Some `Audiovisual
104104- | "bill" -> Some `Bill
105105- | "blog" -> Some `Blog
106106- | "book" -> Some `Book
107107- | "catalogue" -> Some `Catalogue
108108- | "conference" -> Some `Conference
109109- | "conference-paper" -> Some `Conference_paper
110110- | "data" -> Some `Data
111111- | "database" -> Some `Database
112112- | "dictionary" -> Some `Dictionary
113113- | "edited-work" -> Some `Edited_work
114114- | "encyclopedia" -> Some `Encyclopedia
115115- | "film-broadcast" -> Some `Film_broadcast
116116- | "generic" -> Some `Generic
117117- | "government-document" -> Some `Government_document
118118- | "grant" -> Some `Grant
119119- | "hearing" -> Some `Hearing
120120- | "historical-work" -> Some `Historical_work
121121- | "legal-case" -> Some `Legal_case
122122- | "legal-rule" -> Some `Legal_rule
123123- | "magazine-article" -> Some `Magazine_article
124124- | "manual" -> Some `Manual
125125- | "map" -> Some `Map
126126- | "multimedia" -> Some `Multimedia
127127- | "music" -> Some `Music
128128- | "newspaper-article" -> Some `Newspaper_article
129129- | "pamphlet" -> Some `Pamphlet
130130- | "patent" -> Some `Patent
131131- | "personal-communication" -> Some `Personal_communication
132132- | "proceedings" -> Some `Proceedings
133133- | "report" -> Some `Report
134134- | "serial" -> Some `Serial
135135- | "slides" -> Some `Slides
136136- | "software" -> Some `Software
137137- | "software-code" -> Some `Software_code
138138- | "software-container" -> Some `Software_container
139139- | "software-executable" -> Some `Software_executable
140140- | "software-virtual-machine" -> Some `Software_virtual_machine
141141- | "sound-recording" -> Some `Sound_recording
142142- | "standard" -> Some `Standard
143143- | "statute" -> Some `Statute
144144- | "thesis" -> Some `Thesis
145145- | "unpublished" -> Some `Unpublished
146146- | "video" -> Some `Video
147147- | "website" -> Some `Website
148148- | _ -> None
114114+ let of_string = function
115115+ | "art" -> Some `Art
116116+ | "article" -> Some `Article
117117+ | "audiovisual" -> Some `Audiovisual
118118+ | "bill" -> Some `Bill
119119+ | "blog" -> Some `Blog
120120+ | "book" -> Some `Book
121121+ | "catalogue" -> Some `Catalogue
122122+ | "conference" -> Some `Conference
123123+ | "conference-paper" -> Some `Conference_paper
124124+ | "data" -> Some `Data
125125+ | "database" -> Some `Database
126126+ | "dictionary" -> Some `Dictionary
127127+ | "edited-work" -> Some `Edited_work
128128+ | "encyclopedia" -> Some `Encyclopedia
129129+ | "film-broadcast" -> Some `Film_broadcast
130130+ | "generic" -> Some `Generic
131131+ | "government-document" -> Some `Government_document
132132+ | "grant" -> Some `Grant
133133+ | "hearing" -> Some `Hearing
134134+ | "historical-work" -> Some `Historical_work
135135+ | "legal-case" -> Some `Legal_case
136136+ | "legal-rule" -> Some `Legal_rule
137137+ | "magazine-article" -> Some `Magazine_article
138138+ | "manual" -> Some `Manual
139139+ | "map" -> Some `Map
140140+ | "multimedia" -> Some `Multimedia
141141+ | "music" -> Some `Music
142142+ | "newspaper-article" -> Some `Newspaper_article
143143+ | "pamphlet" -> Some `Pamphlet
144144+ | "patent" -> Some `Patent
145145+ | "personal-communication" -> Some `Personal_communication
146146+ | "proceedings" -> Some `Proceedings
147147+ | "report" -> Some `Report
148148+ | "serial" -> Some `Serial
149149+ | "slides" -> Some `Slides
150150+ | "software" -> Some `Software
151151+ | "software-code" -> Some `Software_code
152152+ | "software-container" -> Some `Software_container
153153+ | "software-executable" -> Some `Software_executable
154154+ | "software-virtual-machine" -> Some `Software_virtual_machine
155155+ | "sound-recording" -> Some `Sound_recording
156156+ | "standard" -> Some `Standard
157157+ | "statute" -> Some `Statute
158158+ | "thesis" -> Some `Thesis
159159+ | "unpublished" -> Some `Unpublished
160160+ | "video" -> Some `Video
161161+ | "website" -> Some `Website
162162+ | _ -> None
163163+ ;;
149164150150- let to_string = function
151151- | `Art -> "art"
152152- | `Article -> "article"
153153- | `Audiovisual -> "audiovisual"
154154- | `Bill -> "bill"
155155- | `Blog -> "blog"
156156- | `Book -> "book"
157157- | `Catalogue -> "catalogue"
158158- | `Conference -> "conference"
159159- | `Conference_paper -> "conference-paper"
160160- | `Data -> "data"
161161- | `Database -> "database"
162162- | `Dictionary -> "dictionary"
163163- | `Edited_work -> "edited-work"
164164- | `Encyclopedia -> "encyclopedia"
165165- | `Film_broadcast -> "film-broadcast"
166166- | `Generic -> "generic"
167167- | `Government_document -> "government-document"
168168- | `Grant -> "grant"
169169- | `Hearing -> "hearing"
170170- | `Historical_work -> "historical-work"
171171- | `Legal_case -> "legal-case"
172172- | `Legal_rule -> "legal-rule"
173173- | `Magazine_article -> "magazine-article"
174174- | `Manual -> "manual"
175175- | `Map -> "map"
176176- | `Multimedia -> "multimedia"
177177- | `Music -> "music"
178178- | `Newspaper_article -> "newspaper-article"
179179- | `Pamphlet -> "pamphlet"
180180- | `Patent -> "patent"
181181- | `Personal_communication -> "personal-communication"
182182- | `Proceedings -> "proceedings"
183183- | `Report -> "report"
184184- | `Serial -> "serial"
185185- | `Slides -> "slides"
186186- | `Software -> "software"
187187- | `Software_code -> "software-code"
188188- | `Software_container -> "software-container"
189189- | `Software_executable -> "software-executable"
190190- | `Software_virtual_machine -> "software-virtual-machine"
191191- | `Sound_recording -> "sound-recording"
192192- | `Standard -> "standard"
193193- | `Statute -> "statute"
194194- | `Thesis -> "thesis"
195195- | `Unpublished -> "unpublished"
196196- | `Video -> "video"
197197- | `Website -> "website"
198198-end)
165165+ let to_string = function
166166+ | `Art -> "art"
167167+ | `Article -> "article"
168168+ | `Audiovisual -> "audiovisual"
169169+ | `Bill -> "bill"
170170+ | `Blog -> "blog"
171171+ | `Book -> "book"
172172+ | `Catalogue -> "catalogue"
173173+ | `Conference -> "conference"
174174+ | `Conference_paper -> "conference-paper"
175175+ | `Data -> "data"
176176+ | `Database -> "database"
177177+ | `Dictionary -> "dictionary"
178178+ | `Edited_work -> "edited-work"
179179+ | `Encyclopedia -> "encyclopedia"
180180+ | `Film_broadcast -> "film-broadcast"
181181+ | `Generic -> "generic"
182182+ | `Government_document -> "government-document"
183183+ | `Grant -> "grant"
184184+ | `Hearing -> "hearing"
185185+ | `Historical_work -> "historical-work"
186186+ | `Legal_case -> "legal-case"
187187+ | `Legal_rule -> "legal-rule"
188188+ | `Magazine_article -> "magazine-article"
189189+ | `Manual -> "manual"
190190+ | `Map -> "map"
191191+ | `Multimedia -> "multimedia"
192192+ | `Music -> "music"
193193+ | `Newspaper_article -> "newspaper-article"
194194+ | `Pamphlet -> "pamphlet"
195195+ | `Patent -> "patent"
196196+ | `Personal_communication -> "personal-communication"
197197+ | `Proceedings -> "proceedings"
198198+ | `Report -> "report"
199199+ | `Serial -> "serial"
200200+ | `Slides -> "slides"
201201+ | `Software -> "software"
202202+ | `Software_code -> "software-code"
203203+ | `Software_container -> "software-container"
204204+ | `Software_executable -> "software-executable"
205205+ | `Software_virtual_machine -> "software-virtual-machine"
206206+ | `Sound_recording -> "sound-recording"
207207+ | `Standard -> "standard"
208208+ | `Statute -> "statute"
209209+ | `Thesis -> "thesis"
210210+ | `Unpublished -> "unpublished"
211211+ | `Video -> "video"
212212+ | `Website -> "website"
213213+ ;;
214214+ end)
199215200216module Status = Make_enum (struct
201201- type t = [
202202- | `Abstract
203203- | `Advance_online
204204- | `In_preparation
205205- | `In_press
206206- | `Preprint
207207- | `Submitted
208208- ]
209209- let type_name = "status"
217217+ type t =
218218+ [ `Abstract
219219+ | `Advance_online
220220+ | `In_preparation
221221+ | `In_press
222222+ | `Preprint
223223+ | `Submitted
224224+ ]
210225211211- let of_string = function
212212- | "abstract" -> Some `Abstract
213213- | "advance-online" -> Some `Advance_online
214214- | "in-preparation" -> Some `In_preparation
215215- | "in-press" -> Some `In_press
216216- | "preprint" -> Some `Preprint
217217- | "submitted" -> Some `Submitted
218218- | _ -> None
226226+ let type_name = "status"
219227220220- let to_string = function
221221- | `Abstract -> "abstract"
222222- | `Advance_online -> "advance-online"
223223- | `In_preparation -> "in-preparation"
224224- | `In_press -> "in-press"
225225- | `Preprint -> "preprint"
226226- | `Submitted -> "submitted"
227227-end)
228228+ let of_string = function
229229+ | "abstract" -> Some `Abstract
230230+ | "advance-online" -> Some `Advance_online
231231+ | "in-preparation" -> Some `In_preparation
232232+ | "in-press" -> Some `In_press
233233+ | "preprint" -> Some `Preprint
234234+ | "submitted" -> Some `Submitted
235235+ | _ -> None
236236+ ;;
237237+238238+ let to_string = function
239239+ | `Abstract -> "abstract"
240240+ | `Advance_online -> "advance-online"
241241+ | `In_preparation -> "in-preparation"
242242+ | `In_press -> "in-press"
243243+ | `Preprint -> "preprint"
244244+ | `Submitted -> "submitted"
245245+ ;;
246246+ end)
228247229248module Cff_type = Make_enum (struct
230230- type t = [ `Software | `Dataset ]
231231- let type_name = "CFF type"
249249+ type t =
250250+ [ `Software
251251+ | `Dataset
252252+ ]
232253233233- let of_string = function
234234- | "software" -> Some `Software
235235- | "dataset" -> Some `Dataset
236236- | _ -> None
254254+ let type_name = "CFF type"
255255+256256+ let of_string = function
257257+ | "software" -> Some `Software
258258+ | "dataset" -> Some `Dataset
259259+ | _ -> None
260260+ ;;
237261238238- let to_string = function
239239- | `Software -> "software"
240240- | `Dataset -> "dataset"
241241-end)
262262+ let to_string = function
263263+ | `Software -> "software"
264264+ | `Dataset -> "dataset"
265265+ ;;
266266+ end)
+31-23
lib/cff_enums.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···4848 description: Software Heritage archive
4949 ]} *)
5050module Identifier_type : sig
5151- type t = [ `Doi | `Url | `Swh | `Other ]
5251 (** Identifier types. *)
5252+ type t =
5353+ [ `Doi
5454+ | `Url
5555+ | `Swh
5656+ | `Other
5757+ ]
53585959+ (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *)
5460 val of_string : string -> t option
5555- (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *)
56615757- val to_string : t -> string
5862 (** Convert to YAML string representation. *)
6363+ val to_string : t -> string
59646065 val equal : t -> t -> bool
6166 val compare : t -> t -> int
6267 val pp : Format.formatter -> t -> unit
63686969+ (** JSON/YAML codec. *)
6470 val jsont : t Jsont.t
6565- (** JSON/YAML codec. *)
6671end
67726873(** Reference type for bibliographic entries.
···138143 - [`Standard] - Technical standard
139144 - [`Unpublished] - Unpublished work *)
140145module Reference_type : sig
141141- type t = [
142142- | `Art
146146+ (** All supported reference types. *)
147147+ type t =
148148+ [ `Art
143149 | `Article
144150 | `Audiovisual
145151 | `Bill
···186192 | `Unpublished
187193 | `Video
188194 | `Website
189189- ]
190190- (** All supported reference types. *)
195195+ ]
191196192192- val of_string : string -> t option
193197 (** Parse from YAML string. Hyphenated names like ["conference-paper"]
194198 map to underscored variants like [`Conference_paper]. *)
199199+ val of_string : string -> t option
195200196196- val to_string : t -> string
197201 (** Convert to YAML string representation.
198202 Underscored variants like [`Conference_paper] become ["conference-paper"]. *)
203203+ val to_string : t -> string
199204200205 val equal : t -> t -> bool
201206 val compare : t -> t -> int
202207 val pp : Format.formatter -> t -> unit
203208204204- val jsont : t Jsont.t
205209 (** JSON/YAML codec. *)
210210+ val jsont : t Jsont.t
206211end
207212208213(** Publication status for works in progress.
···230235 status: submitted
231236 ]} *)
232237module Status : sig
233233- type t = [
234234- | `Abstract
238238+ (** Publication status values. *)
239239+ type t =
240240+ [ `Abstract
235241 | `Advance_online
236242 | `In_preparation
237243 | `In_press
238244 | `Preprint
239245 | `Submitted
240240- ]
241241- (** Publication status values. *)
246246+ ]
242247243243- val of_string : string -> t option
244248 (** Parse from YAML string: ["abstract"], ["advance-online"], etc. *)
249249+ val of_string : string -> t option
245250251251+ (** Convert to YAML string representation. *)
246252 val to_string : t -> string
247247- (** Convert to YAML string representation. *)
248253249254 val equal : t -> t -> bool
250255 val compare : t -> t -> int
251256 val pp : Format.formatter -> t -> unit
252257253253- val jsont : t Jsont.t
254258 (** JSON/YAML codec. *)
259259+ val jsont : t Jsont.t
255260end
256261257262(** CFF file type: software or dataset.
···271276 # ...
272277 ]} *)
273278module Cff_type : sig
274274- type t = [ `Software | `Dataset ]
275279 (** CFF file types. *)
280280+ type t =
281281+ [ `Software
282282+ | `Dataset
283283+ ]
276284277277- val of_string : string -> t option
278285 (** Parse from YAML string: ["software"] or ["dataset"]. *)
286286+ val of_string : string -> t option
279287280280- val to_string : t -> string
281288 (** Convert to YAML string representation. *)
289289+ val to_string : t -> string
282290283291 val equal : t -> t -> bool
284292 val compare : t -> t -> int
285293 val pp : Format.formatter -> t -> unit
286294287287- val jsont : t Jsont.t
288295 (** JSON/YAML codec. *)
296296+ val jsont : t Jsont.t
289297end
+17-23
lib/cff_identifier.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566(** Identifier type for CFF. *)
7788-type t = {
99- type_ : Cff_enums.Identifier_type.t;
1010- value : string;
1111- description : string option;
1212-}
1313-1414-let make ~type_ ~value ?description () =
1515- { type_; value; description }
88+type t =
99+ { type_ : Cff_enums.Identifier_type.t
1010+ ; value : string
1111+ ; description : string option
1212+ }
16131414+let make ~type_ ~value ?description () = { type_; value; description }
1715let type_ t = t.type_
1816let value t = t.value
1917let description t = t.description
20182119let equal a b =
2222- Cff_enums.Identifier_type.equal a.type_ b.type_ &&
2323- String.equal a.value b.value
2020+ Cff_enums.Identifier_type.equal a.type_ b.type_ && String.equal a.value b.value
2121+;;
24222523let compare a b =
2624 match Cff_enums.Identifier_type.compare a.type_ b.type_ with
2725 | 0 -> String.compare a.value b.value
2826 | n -> n
2727+;;
29283030-let pp ppf t =
3131- Format.fprintf ppf "%a: %s"
3232- Cff_enums.Identifier_type.pp t.type_
3333- t.value
2929+let pp ppf t = Format.fprintf ppf "%a: %s" Cff_enums.Identifier_type.pp t.type_ t.value
34303531let jsont =
3636- Jsont.Object.map ~kind:"Identifier"
3737- (fun type_ value description -> { type_; value; description })
3838- |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont
3939- ~enc:(fun i -> i.type_)
4040- |> Jsont.Object.mem "value" Jsont.string
4141- ~enc:(fun i -> i.value)
4242- |> Jsont.Object.opt_mem "description" Jsont.string
4343- ~enc:(fun i -> i.description)
3232+ Jsont.Object.map ~kind:"Identifier" (fun type_ value description ->
3333+ { type_; value; description })
3434+ |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont ~enc:(fun i -> i.type_)
3535+ |> Jsont.Object.mem "value" Jsont.string ~enc:(fun i -> i.value)
3636+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun i -> i.description)
4437 |> Jsont.Object.skip_unknown
4538 |> Jsont.Object.finish
3939+;;
+15-14
lib/cff_identifier.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···6666 - [rel]: Release
6767 - [snp]: Snapshot *)
68686969-type t
7069(** An identifier with type, value, and optional description. *)
7070+type t
71717272-val make :
7373- type_:Cff_enums.Identifier_type.t ->
7474- value:string ->
7575- ?description:string ->
7676- unit -> t
7772(** Create an identifier.
78737974 @param type_ The identifier type ([`Doi], [`Url], [`Swh], or [`Other])
8075 @param value The identifier value (DOI, URL, SWH ID, etc.)
8176 @param description Optional human-readable description *)
7777+val make
7878+ : type_:Cff_enums.Identifier_type.t
7979+ -> value:string
8080+ -> ?description:string
8181+ -> unit
8282+ -> t
82838484+(** The identifier type. *)
8385val type_ : t -> Cff_enums.Identifier_type.t
8484-(** The identifier type. *)
85868686-val value : t -> string
8787(** The identifier value.
88888989 For DOIs, this is just the DOI (e.g., ["10.5281/zenodo.1234567"]),
9090 not the full URL. *)
9191+val value : t -> string
91929292-val description : t -> string option
9393(** Optional description explaining what this identifier refers to.
94949595 Examples:
9696 - ["The concept DOI for all versions"]
9797 - ["Version 1.0.0 archive"]
9898 - ["Release on GitHub"] *)
9999+val description : t -> string option
99100101101+(** Identifier equality (compares all fields). *)
100102val equal : t -> t -> bool
101101-(** Identifier equality (compares all fields). *)
102103103103-val compare : t -> t -> int
104104(** Identifier comparison. *)
105105+val compare : t -> t -> int
105106106106-val pp : Format.formatter -> t -> unit
107107(** Pretty-print as "[type]: value (description)". *)
108108+val pp : Format.formatter -> t -> unit
108109110110+(** JSON/YAML codec for identifiers. *)
109111val jsont : t Jsont.t
110110-(** JSON/YAML codec for identifiers. *)
+63-35
lib/cff_license.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566(** SPDX license handling for CFF. *)
7788-type t = [ `Expr of Spdx_licenses.t | `Raw of string list ]
88+type t =
99+ [ `Spdx of Spdx_licenses.t
1010+ | `Other of string list * string option
1111+ ]
9121010-let of_spdx spdx = `Expr spdx
1313+let of_spdx spdx = `Spdx spdx
11141215let of_string s =
1316 match Spdx_licenses.parse s with
1414- | Ok spdx -> `Expr spdx
1515- | Error _ -> `Raw [s]
1717+ | Ok spdx -> `Spdx spdx
1818+ | Error _ -> `Other ([ s ], None)
1919+;;
16201721let of_strings ss =
1818- (* Try to parse as OR combination, fall back to Raw *)
2222+ (* Try to parse as OR combination, fall back to Other *)
1923 let try_parse_all () =
2024 let rec build = function
2125 | [] -> None
2222- | [s] ->
2626+ | [ s ] ->
2327 (match Spdx_licenses.parse s with
2428 | Ok spdx -> Some spdx
2529 | Error _ -> None)
···3135 build ss
3236 in
3337 match try_parse_all () with
3434- | Some spdx -> `Expr spdx
3535- | None -> `Raw ss
3838+ | Some spdx -> `Spdx spdx
3939+ | None -> `Other (ss, None)
4040+;;
4141+4242+let with_url url = function
4343+ | `Spdx _ as t -> t (* SPDX licenses have well-known URLs, ignore provided URL *)
4444+ | `Other (ids, _) -> `Other (ids, Some url)
4545+;;
4646+4747+let with_url_opt url_opt t =
4848+ match url_opt with
4949+ | None -> t
5050+ | Some url -> with_url url t
5151+;;
36523753let to_spdx = function
3838- | `Expr spdx -> Some spdx
3939- | `Raw _ -> None
5454+ | `Spdx spdx -> Some spdx
5555+ | `Other _ -> None
5656+;;
40574158let to_strings = function
4242- | `Expr spdx -> [Spdx_licenses.to_string spdx]
4343- | `Raw ss -> ss
5959+ | `Spdx spdx -> [ Spdx_licenses.to_string spdx ]
6060+ | `Other (ss, _) -> ss
6161+;;
6262+6363+let url = function
6464+ | `Spdx _ -> None
6565+ | `Other (_, url) -> url
6666+;;
44674568let pp ppf = function
4646- | `Expr spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx)
4747- | `Raw ss ->
4848- match ss with
4949- | [s] -> Format.pp_print_string ppf s
5050- | _ ->
5151- Format.fprintf ppf "[%a]"
5252- (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
5353- Format.pp_print_string) ss
6969+ | `Spdx spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx)
7070+ | `Other (ss, url_opt) ->
7171+ (match ss with
7272+ | [ s ] -> Format.pp_print_string ppf s
7373+ | _ ->
7474+ Format.fprintf
7575+ ppf
7676+ "[%a]"
7777+ (Format.pp_print_list
7878+ ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
7979+ Format.pp_print_string)
8080+ ss);
8181+ Option.iter (Format.fprintf ppf " <%s>") url_opt
8282+;;
54835555-(* Jsont codec - lenient, accepts any string/array *)
5684let jsont =
5785 let string_codec =
5858- Jsont.string |> Jsont.map
5959- ~dec:(fun s -> of_string s)
6060- ~enc:(function
6161- | `Expr spdx -> Spdx_licenses.to_string spdx
6262- | `Raw [s] -> s
6363- | `Raw _ -> assert false)
8686+ Jsont.string
8787+ |> Jsont.map ~dec:of_string ~enc:(function
8888+ | `Spdx spdx -> Spdx_licenses.to_string spdx
8989+ | `Other ([ s ], _) -> s
9090+ | `Other _ -> assert false)
6491 in
6592 let array_codec =
6666- Jsont.(array string) |> Jsont.map
6767- ~dec:(fun ss -> of_strings (Array.to_list ss))
6868- ~enc:(fun t -> Array.of_list (to_strings t))
9393+ Jsont.(array string)
9494+ |> Jsont.map
9595+ ~dec:(fun ss -> of_strings (Array.to_list ss))
9696+ ~enc:(fun t -> Array.of_list (to_strings t))
6997 in
7098 Jsont.any
7199 ~dec_string:string_codec
72100 ~dec_array:array_codec
7373- ~enc:(fun t ->
7474- match t with
7575- | `Expr (Spdx_licenses.Simple _) -> string_codec
7676- | `Raw [_] -> string_codec
101101+ ~enc:(function
102102+ | `Spdx (Spdx_licenses.Simple _) -> string_codec
103103+ | `Other ([ _ ], _) -> string_codec
77104 | _ -> array_codec)
78105 ()
106106+;;
+58-30
lib/cff_license.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566(** SPDX license expressions for CFF.
7788 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers}
99- for the [license] field. This module wraps {!Spdx_licenses.t} with
1010- support for invalid/unknown licenses to enable round-tripping.
99+ for the [license] field. This module combines license identification
1010+ with optional URLs for non-standard licenses.
11111212 {1 License Representation}
13131414 Licenses are represented as either:
1515- - [`Expr spdx] - A valid, parsed SPDX license expression
1616- - [`Raw strings] - Unparsed strings for invalid/unknown licenses
1515+ - [`Spdx expr] - A valid SPDX expression (URL is implicit/well-known)
1616+ - [`Other (ids, url_opt)] - Unknown license ID(s) with optional URL
17171818 The parser is lenient: it tries to parse as SPDX but preserves
1919- invalid strings for round-tripping.
1919+ invalid/unknown identifiers for round-tripping.
2020+2121+ {1 Why Combined?}
2222+2323+ The CFF spec has separate [license] and [license-url] fields, but they
2424+ have a hidden relationship:
2525+ - SPDX licenses have well-known URLs (e.g., MIT → https://spdx.org/licenses/MIT.html)
2626+ - [license-url] is only meaningful for non-SPDX licenses
2727+2828+ This type makes that relationship explicit: [`Spdx] licenses don't need
2929+ a URL, while [`Other] licenses can optionally include one.
20302131 {1 Examples}
22322323- {2 Single License}
3333+ {2 Standard SPDX License}
2434 {[
2535 license: MIT
2636 ]}
3737+ Parsed as [`Spdx (Simple (LicenseID "MIT"))].
27382839 {2 SPDX Expression}
2940 {[
3030- license: GPL-3.0-or-later WITH Classpath-exception-2.0
4141+ license: Apache-2.0 OR MIT
3142 ]}
4343+ Parsed as [`Spdx (OR ...)].
32443333- {2 Multiple Licenses (OR)}
4545+ {2 Custom License with URL}
3446 {[
3535- license:
3636- - Apache-2.0
3737- - MIT
4747+ license: ACME-Proprietary-1.0
4848+ license-url: https://acme.com/license
3849 ]}
3939- This is parsed as [Apache-2.0 OR MIT]. *)
5050+ Parsed as [`Other (["ACME-Proprietary-1.0"], Some "https://acme.com/license")]. *)
40514141-type t = [ `Expr of Spdx_licenses.t | `Raw of string list ]
4242-(** The license type: either a valid SPDX expression or raw strings. *)
5252+(** License type: SPDX expression or unknown ID(s) with optional URL. *)
5353+type t =
5454+ [ `Spdx of Spdx_licenses.t
5555+ | `Other of string list * string option
5656+ ]
43574458(** {1 Construction} *)
45596060+(** [of_spdx expr] wraps a valid SPDX expression. *)
4661val of_spdx : Spdx_licenses.t -> t
4747-(** [of_spdx spdx] wraps a valid SPDX expression. *)
48624949-val of_string : string -> t
5063(** [of_string s] parses [s] as an SPDX expression.
5151- Returns [`Expr] on success, [`Raw [s]] on parse failure. *)
6464+ Returns [`Spdx] on success, [`Other ([s], None)] on parse failure. *)
6565+val of_string : string -> t
52665353-val of_strings : string list -> t
5467(** [of_strings ss] parses a list of license strings.
5555- If all strings are valid license IDs, returns an [`Expr] with OR combination.
5656- Otherwise returns [`Raw ss] to preserve the original strings. *)
6868+ If all are valid SPDX IDs, returns [`Spdx] with OR combination.
6969+ Otherwise returns [`Other (ss, None)]. *)
7070+val of_strings : string list -> t
7171+7272+(** [with_url url t] adds a URL to the license.
7373+ - For [`Spdx], returns unchanged (SPDX URLs are well-known)
7474+ - For [`Other (ids, _)], returns [`Other (ids, Some url)] *)
7575+val with_url : string -> t -> t
7676+7777+(** [with_url_opt url_opt t] optionally adds a URL.
7878+ Convenience for combining during jsont decoding. *)
7979+val with_url_opt : string option -> t -> t
57805881(** {1 Access} *)
59828383+(** [to_spdx t] returns [Some expr] if valid SPDX, [None] otherwise. *)
6084val to_spdx : t -> Spdx_licenses.t option
6161-(** [to_spdx t] returns [Some spdx] if [t] is a valid expression,
6262- [None] if it contains unparsed raw strings. *)
63858686+(** [to_strings t] returns the license identifier(s) as strings. *)
6487val to_strings : t -> string list
6565-(** [to_strings t] returns the license as a list of strings.
6666- For [`Expr], returns the normalized SPDX string.
6767- For [`Raw], returns the original strings. *)
8888+8989+(** [url t] returns the license URL.
9090+ - For [`Spdx], always [None] (use SPDX's well-known URLs)
9191+ - For [`Other], returns the URL if provided *)
9292+val url : t -> string option
68936994(** {1 Formatting} *)
70957171-val pp : Format.formatter -> t -> unit
7296(** Pretty-print the license. *)
9797+val pp : Format.formatter -> t -> unit
73987499(** {1 Codec} *)
751007676-val jsont : t Jsont.t
7777-(** JSON/YAML codec for licenses.
101101+(** JSON/YAML codec for the license field only.
102102+103103+ This handles just the [license] field. The [license-url] field
104104+ is handled separately by the parent codec which combines them
105105+ using {!with_url}.
781067979- Handles both single string and array of strings.
80107 Lenient: accepts any string without validation for round-tripping. *)
108108+val jsont : t Jsont.t
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···7676module Core : sig
7777 type t
78787979- val make :
8080- type_:Cff_enums.Reference_type.t ->
8181- title:string ->
8282- authors:Cff_author.t list ->
8383- ?abstract:string ->
8484- ?abbreviation:string ->
8585- unit -> t
8679 (** Create a core record.
87808881 @param type_ The reference type (article, book, software, etc.)
8982 @param title The title of the work
9083 @param authors List of persons and/or entities *)
8484+ val make
8585+ : type_:Cff_enums.Reference_type.t
8686+ -> title:string
8787+ -> authors:Cff_author.t list
8888+ -> ?abstract:string
8989+ -> ?abbreviation:string
9090+ -> unit
9191+ -> t
91929292- val type_ : t -> Cff_enums.Reference_type.t
9393 (** The reference type. Determines which other fields are applicable. *)
9494+ val type_ : t -> Cff_enums.Reference_type.t
94959595- val title : t -> string
9696 (** The title of the referenced work. *)
9797+ val title : t -> string
97989999+ (** The authors/creators of the work. *)
98100 val authors : t -> Cff_author.t list
9999- (** The authors/creators of the work. *)
100101101101- val abstract : t -> string option
102102 (** A description or abstract of the work. *)
103103+ val abstract : t -> string option
103104104104- val abbreviation : t -> string option
105105 (** Abbreviated form of the title (e.g., for journal names). *)
106106+ val abbreviation : t -> string option
106107107108 val pp : Format.formatter -> t -> unit
108109end
···115116module Publication : sig
116117 type t
117118118118- val empty : t
119119 (** Empty publication record with all fields as [None]. *)
120120+ val empty : t
120121121121- val make :
122122- ?journal:string ->
123123- ?volume:string ->
124124- ?issue:string ->
125125- ?pages:string ->
126126- ?start:string ->
127127- ?end_:string ->
128128- ?edition:string ->
129129- ?section:string ->
130130- ?status:Cff_enums.Status.t ->
131131- unit -> t
122122+ val make
123123+ : ?journal:string
124124+ -> ?volume:string
125125+ -> ?issue:string
126126+ -> ?pages:string
127127+ -> ?start:string
128128+ -> ?end_:string
129129+ -> ?edition:string
130130+ -> ?section:string
131131+ -> ?status:Cff_enums.Status.t
132132+ -> unit
133133+ -> t
132134133133- val journal : t -> string option
134135 (** The name of the journal or magazine. *)
136136+ val journal : t -> string option
135137136136- val volume : t -> string option
137138 (** The volume number of the journal. *)
139139+ val volume : t -> string option
138140141141+ (** The issue number within the volume. *)
139142 val issue : t -> string option
140140- (** The issue number within the volume. *)
141143144144+ (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *)
142145 val pages : t -> string option
143143- (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *)
144146145145- val start : t -> string option
146147 (** Starting page number. *)
148148+ val start : t -> string option
147149150150+ (** Ending page number. *)
148151 val end_ : t -> string option
149149- (** Ending page number. *)
150152153153+ (** The edition of the work (e.g., ["2nd edition"]). *)
151154 val edition : t -> string option
152152- (** The edition of the work (e.g., ["2nd edition"]). *)
153155154154- val section : t -> string option
155156 (** The section of a work (e.g., newspaper section). *)
157157+ val section : t -> string option
156158157157- val status : t -> Cff_enums.Status.t option
158159 (** Publication status: preprint, in-press, submitted, etc. *)
160160+ val status : t -> Cff_enums.Status.t option
159161162162+ (** [true] if all fields are [None]. *)
160163 val is_empty : t -> bool
161161- (** [true] if all fields are [None]. *)
162164end
163165164166(** Collection metadata for works in edited volumes.
···170172171173 val empty : t
172174173173- val make :
174174- ?collection_title:string ->
175175- ?collection_type:string ->
176176- ?collection_doi:string ->
177177- ?volume_title:string ->
178178- ?number_volumes:string ->
179179- unit -> t
175175+ val make
176176+ : ?collection_title:string
177177+ -> ?collection_type:string
178178+ -> ?collection_doi:string
179179+ -> ?volume_title:string
180180+ -> ?number_volumes:string
181181+ -> unit
182182+ -> t
180183181181- val collection_title : t -> string option
182184 (** Title of the collection (proceedings, book series, etc.). *)
185185+ val collection_title : t -> string option
183186187187+ (** Type of collection (e.g., ["proceedings"], ["book series"]). *)
184188 val collection_type : t -> string option
185185- (** Type of collection (e.g., ["proceedings"], ["book series"]). *)
186189187187- val collection_doi : t -> string option
188190 (** DOI of the collection itself (not the individual work). *)
191191+ val collection_doi : t -> string option
189192193193+ (** Title of the specific volume within a multi-volume collection. *)
190194 val volume_title : t -> string option
191191- (** Title of the specific volume within a multi-volume collection. *)
192195193193- val number_volumes : t -> string option
194196 (** Total number of volumes in the collection. *)
197197+ val number_volumes : t -> string option
195198196199 val is_empty : t -> bool
197200end
···211214212215 val empty : t
213216214214- val make :
215215- ?date_accessed:Cff_date.t ->
216216- ?date_downloaded:Cff_date.t ->
217217- ?date_published:Cff_date.t ->
218218- ?date_released:Cff_date.t ->
219219- ?year:int ->
220220- ?year_original:int ->
221221- ?month:int ->
222222- ?issue_date:string ->
223223- unit -> t
217217+ val make
218218+ : ?date_accessed:Cff_date.t
219219+ -> ?date_downloaded:Cff_date.t
220220+ -> ?date_published:Cff_date.t
221221+ -> ?date_released:Cff_date.t
222222+ -> ?year:int
223223+ -> ?year_original:int
224224+ -> ?month:int
225225+ -> ?issue_date:string
226226+ -> unit
227227+ -> t
224228225225- val date_accessed : t -> Cff_date.t option
226229 (** Date when an online resource was accessed for citation. *)
230230+ val date_accessed : t -> Cff_date.t option
227231228228- val date_downloaded : t -> Cff_date.t option
229232 (** Date when a resource was downloaded. *)
233233+ val date_downloaded : t -> Cff_date.t option
230234235235+ (** Formal publication date. *)
231236 val date_published : t -> Cff_date.t option
232232- (** Formal publication date. *)
233237234234- val date_released : t -> Cff_date.t option
235238 (** Release date (typically for software). *)
239239+ val date_released : t -> Cff_date.t option
236240241241+ (** Publication year when full date is unknown. *)
237242 val year : t -> int option
238238- (** Publication year when full date is unknown. *)
239243240240- val year_original : t -> int option
241244 (** Year of original publication (for reprints, translations). *)
245245+ val year_original : t -> int option
242246247247+ (** Publication month (1-12) when only month/year is known. *)
243248 val month : t -> int option
244244- (** Publication month (1-12) when only month/year is known. *)
245249246246- val issue_date : t -> string option
247250 (** Issue date as a string (for periodicals with specific dates). *)
251251+ val issue_date : t -> string option
248252249253 val is_empty : t -> bool
250254end
···263267264268 val empty : t
265269266266- val make :
267267- ?doi:string ->
268268- ?url:string ->
269269- ?repository:string ->
270270- ?repository_code:string ->
271271- ?repository_artifact:string ->
272272- ?isbn:string ->
273273- ?issn:string ->
274274- ?pmcid:string ->
275275- ?nihmsid:string ->
276276- ?identifiers:Cff_identifier.t list ->
277277- unit -> t
270270+ val make
271271+ : ?doi:string
272272+ -> ?url:string
273273+ -> ?repository:string
274274+ -> ?repository_code:string
275275+ -> ?repository_artifact:string
276276+ -> ?isbn:string
277277+ -> ?issn:string
278278+ -> ?pmcid:string
279279+ -> ?nihmsid:string
280280+ -> ?identifiers:Cff_identifier.t list
281281+ -> unit
282282+ -> t
278283284284+ (** Digital Object Identifier (e.g., ["10.1234/example"]). *)
279285 val doi : t -> string option
280280- (** Digital Object Identifier (e.g., ["10.1234/example"]). *)
281286282282- val url : t -> string option
283287 (** URL where the work can be accessed. *)
288288+ val url : t -> string option
284289285285- val repository : t -> string option
286290 (** General repository URL. *)
291291+ val repository : t -> string option
287292293293+ (** Source code repository (GitHub, GitLab, etc.). *)
288294 val repository_code : t -> string option
289289- (** Source code repository (GitHub, GitLab, etc.). *)
290295291291- val repository_artifact : t -> string option
292296 (** Built artifact repository (npm, PyPI, Docker Hub, etc.). *)
297297+ val repository_artifact : t -> string option
293298294294- val isbn : t -> string option
295299 (** International Standard Book Number. *)
300300+ val isbn : t -> string option
296301302302+ (** International Standard Serial Number (for journals). *)
297303 val issn : t -> string option
298298- (** International Standard Serial Number (for journals). *)
299304300300- val pmcid : t -> string option
301305 (** PubMed Central identifier. *)
306306+ val pmcid : t -> string option
302307303303- val nihmsid : t -> string option
304308 (** NIH Manuscript Submission System identifier. *)
309309+ val nihmsid : t -> string option
305310311311+ (** Additional typed identifiers (DOI, URL, SWH, other). *)
306312 val identifiers : t -> Cff_identifier.t list option
307307- (** Additional typed identifiers (DOI, URL, SWH, other). *)
308313309314 val is_empty : t -> bool
310315end
···321326322327 val empty : t
323328324324- val make :
325325- ?editors:Cff_author.t list ->
326326- ?editors_series:Cff_author.t list ->
327327- ?translators:Cff_author.t list ->
328328- ?recipients:Cff_author.t list ->
329329- ?senders:Cff_author.t list ->
330330- ?contact:Cff_author.t list ->
331331- ?publisher:Cff_author.Entity.t ->
332332- ?institution:Cff_author.Entity.t ->
333333- ?conference:Cff_author.Entity.t ->
334334- ?database_provider:Cff_author.Entity.t ->
335335- ?location:Cff_author.Entity.t ->
336336- unit -> t
329329+ val make
330330+ : ?editors:Cff_author.t list
331331+ -> ?editors_series:Cff_author.t list
332332+ -> ?translators:Cff_author.t list
333333+ -> ?recipients:Cff_author.t list
334334+ -> ?senders:Cff_author.t list
335335+ -> ?contact:Cff_author.t list
336336+ -> ?publisher:Cff_author.Entity.t
337337+ -> ?institution:Cff_author.Entity.t
338338+ -> ?conference:Cff_author.Entity.t
339339+ -> ?database_provider:Cff_author.Entity.t
340340+ -> ?location:Cff_author.Entity.t
341341+ -> unit
342342+ -> t
337343338338- val editors : t -> Cff_author.t list option
339344 (** Editors of the work (for edited volumes). *)
345345+ val editors : t -> Cff_author.t list option
340346347347+ (** Series editors (for book series). *)
341348 val editors_series : t -> Cff_author.t list option
342342- (** Series editors (for book series). *)
343349344344- val translators : t -> Cff_author.t list option
345350 (** Translators of the work. *)
351351+ val translators : t -> Cff_author.t list option
346352347347- val recipients : t -> Cff_author.t list option
348353 (** Recipients (for personal communications). *)
354354+ val recipients : t -> Cff_author.t list option
349355356356+ (** Senders (for personal communications). *)
350357 val senders : t -> Cff_author.t list option
351351- (** Senders (for personal communications). *)
352358353353- val contact : t -> Cff_author.t list option
354359 (** Contact persons for the work. *)
360360+ val contact : t -> Cff_author.t list option
355361356356- val publisher : t -> Cff_author.Entity.t option
357362 (** Publishing organization. *)
363363+ val publisher : t -> Cff_author.Entity.t option
358364365365+ (** Academic/research institution (for theses, reports). *)
359366 val institution : t -> Cff_author.Entity.t option
360360- (** Academic/research institution (for theses, reports). *)
361367362362- val conference : t -> Cff_author.Entity.t option
363368 (** Conference where the work was presented. *)
369369+ val conference : t -> Cff_author.Entity.t option
364370365365- val database_provider : t -> Cff_author.Entity.t option
366371 (** Provider of a database (for data references). *)
372372+ val database_provider : t -> Cff_author.Entity.t option
367373374374+ (** Location entity (city, venue for conferences). *)
368375 val location : t -> Cff_author.Entity.t option
369369- (** Location entity (city, venue for conferences). *)
370376371377 val is_empty : t -> bool
372378end
···379385380386 val empty : t
381387382382- val make :
383383- ?keywords:string list ->
384384- ?languages:string list ->
385385- ?license:Cff_license.t ->
386386- ?license_url:string ->
387387- ?copyright:string ->
388388- ?scope:string ->
389389- ?notes:string ->
390390- unit -> t
388388+ val make
389389+ : ?keywords:string list
390390+ -> ?languages:string list
391391+ -> ?license:Cff_license.t
392392+ -> ?copyright:string
393393+ -> ?scope:string
394394+ -> ?notes:string
395395+ -> unit
396396+ -> t
391397398398+ (** Descriptive keywords for the work. *)
392399 val keywords : t -> string list option
393393- (** Descriptive keywords for the work. *)
394400395395- val languages : t -> string list option
396401 (** Languages the work is available in (ISO 639 codes). *)
402402+ val languages : t -> string list option
397403404404+ (** SPDX license identifier(s), or unknown license with optional URL. *)
398405 val license : t -> Cff_license.t option
399399- (** SPDX license identifier(s). *)
400406401401- val license_url : t -> string option
402402- (** URL to license text (for non-SPDX licenses). *)
403403-407407+ (** Copyright statement. *)
404408 val copyright : t -> string option
405405- (** Copyright statement. *)
406409407407- val scope : t -> string option
408410 (** Scope of the reference (what aspect it covers). *)
411411+ val scope : t -> string option
409412413413+ (** Additional notes or comments. *)
410414 val notes : t -> string option
411411- (** Additional notes or comments. *)
412415413416 val is_empty : t -> bool
414417end
···426429427430 val empty : t
428431429429- val make :
430430- ?commit:string ->
431431- ?version:string ->
432432- ?filename:string ->
433433- ?format:string ->
434434- ?medium:string ->
435435- ?data_type:string ->
436436- ?database:string ->
437437- ?number:string ->
438438- ?patent_states:string list ->
439439- ?thesis_type:string ->
440440- ?term:string ->
441441- ?entry:string ->
442442- ?department:string ->
443443- ?loc_start:string ->
444444- ?loc_end:string ->
445445- unit -> t
432432+ val make
433433+ : ?commit:string
434434+ -> ?version:string
435435+ -> ?filename:string
436436+ -> ?format:string
437437+ -> ?medium:string
438438+ -> ?data_type:string
439439+ -> ?database:string
440440+ -> ?number:string
441441+ -> ?patent_states:string list
442442+ -> ?thesis_type:string
443443+ -> ?term:string
444444+ -> ?entry:string
445445+ -> ?department:string
446446+ -> ?loc_start:string
447447+ -> ?loc_end:string
448448+ -> unit
449449+ -> t
446450447447- val commit : t -> string option
448451 (** Git commit hash or VCS revision. *)
452452+ val commit : t -> string option
449453450450- val version : t -> string option
451454 (** Version string of the software/data. *)
455455+ val version : t -> string option
452456453453- val filename : t -> string option
454457 (** Name of the file being referenced. *)
458458+ val filename : t -> string option
455459460460+ (** Format of the work (e.g., ["PDF"], ["HTML"]). *)
456461 val format : t -> string option
457457- (** Format of the work (e.g., ["PDF"], ["HTML"]). *)
458462463463+ (** Physical medium (e.g., ["CD-ROM"], ["print"]). *)
459464 val medium : t -> string option
460460- (** Physical medium (e.g., ["CD-ROM"], ["print"]). *)
461465462462- val data_type : t -> string option
463466 (** Type of data (for datasets). *)
467467+ val data_type : t -> string option
464468469469+ (** Name of the database. *)
465470 val database : t -> string option
466466- (** Name of the database. *)
467471472472+ (** Report/patent/standard number. *)
468473 val number : t -> string option
469469- (** Report/patent/standard number. *)
470474471471- val patent_states : t -> string list option
472475 (** Countries where a patent is held. *)
476476+ val patent_states : t -> string list option
473477474474- val thesis_type : t -> string option
475478 (** Type of thesis (["PhD"], ["Master's"], etc.). *)
479479+ val thesis_type : t -> string option
476480481481+ (** Dictionary/encyclopedia term being referenced. *)
477482 val term : t -> string option
478478- (** Dictionary/encyclopedia term being referenced. *)
479483480480- val entry : t -> string option
481484 (** Encyclopedia entry name. *)
485485+ val entry : t -> string option
482486487487+ (** Academic department (for theses). *)
483488 val department : t -> string option
484484- (** Academic department (for theses). *)
485489490490+ (** Starting line/location in source code. *)
486491 val loc_start : t -> string option
487487- (** Starting line/location in source code. *)
488492489489- val loc_end : t -> string option
490493 (** Ending line/location in source code. *)
494494+ val loc_end : t -> string option
491495492496 val is_empty : t -> bool
493497end
···497501(** The complete reference type combining all sub-records. *)
498502type t
499503500500-val make :
501501- core:Core.t ->
502502- ?publication:Publication.t ->
503503- ?collection:Collection.t ->
504504- ?dates:Dates.t ->
505505- ?identifiers:Identifiers.t ->
506506- ?entities:Entities.t ->
507507- ?metadata:Metadata.t ->
508508- ?technical:Technical.t ->
509509- unit -> t
510504(** Construct a reference from sub-records.
511505512506 Only [core] is required; other sub-records default to empty. *)
507507+val make
508508+ : core:Core.t
509509+ -> ?publication:Publication.t
510510+ -> ?collection:Collection.t
511511+ -> ?dates:Dates.t
512512+ -> ?identifiers:Identifiers.t
513513+ -> ?entities:Entities.t
514514+ -> ?metadata:Metadata.t
515515+ -> ?technical:Technical.t
516516+ -> unit
517517+ -> t
513518514514-val make_simple :
515515- type_:Cff_enums.Reference_type.t ->
516516- title:string ->
517517- authors:Cff_author.t list ->
518518- ?doi:string ->
519519- ?year:int ->
520520- ?journal:string ->
521521- unit -> t
522519(** Convenience constructor for simple references.
523520524521 Creates a reference with just the most common fields. Suitable
525522 for quick article or software references. *)
523523+val make_simple
524524+ : type_:Cff_enums.Reference_type.t
525525+ -> title:string
526526+ -> authors:Cff_author.t list
527527+ -> ?doi:string
528528+ -> ?year:int
529529+ -> ?journal:string
530530+ -> unit
531531+ -> t
526532527533(** {2 Sub-record Accessors} *)
528534529529-val core : t -> Core.t
530535(** The core identity fields. *)
536536+val core : t -> Core.t
531537532532-val publication : t -> Publication.t
533538(** Publication metadata (journal, volume, pages). *)
539539+val publication : t -> Publication.t
534540541541+(** Collection metadata (proceedings, book series). *)
535542val collection : t -> Collection.t
536536-(** Collection metadata (proceedings, book series). *)
537543538538-val dates : t -> Dates.t
539544(** Date-related fields. *)
545545+val dates : t -> Dates.t
540546541541-val identifiers : t -> Identifiers.t
542547(** Identifiers and links. *)
548548+val identifiers : t -> Identifiers.t
543549550550+(** Related entities (editors, publisher). *)
544551val entities : t -> Entities.t
545545-(** Related entities (editors, publisher). *)
546552547547-val metadata : t -> Metadata.t
548553(** Descriptive metadata (keywords, license). *)
554554+val metadata : t -> Metadata.t
549555550550-val technical : t -> Technical.t
551556(** Technical fields (commit, version, format). *)
557557+val technical : t -> Technical.t
552558553559(** {2 Direct Accessors for Common Fields}
554560555561 Convenience accessors that delegate to sub-records. *)
556562557557-val type_ : t -> Cff_enums.Reference_type.t
558563(** Shortcut for [Core.type_ (core t)]. *)
564564+val type_ : t -> Cff_enums.Reference_type.t
559565560560-val title : t -> string
561566(** Shortcut for [Core.title (core t)]. *)
567567+val title : t -> string
562568563563-val authors : t -> Cff_author.t list
564569(** Shortcut for [Core.authors (core t)]. *)
570570+val authors : t -> Cff_author.t list
565571572572+(** Shortcut for [Identifiers.doi (identifiers t)]. *)
566573val doi : t -> string option
567567-(** Shortcut for [Identifiers.doi (identifiers t)]. *)
568574569569-val year : t -> int option
570575(** Shortcut for [Dates.year (dates t)]. *)
576576+val year : t -> int option
571577572578(** {1 Formatting and Codec} *)
573579574574-val pp : Format.formatter -> t -> unit
575580(** Pretty-print a reference in a human-readable format. *)
581581+val pp : Format.formatter -> t -> unit
576582583583+(** JSON/YAML codec for serialization. *)
577584val jsont : t Jsont.t
578578-(** JSON/YAML codec for serialization. *)
+15-5
lib_eio/cff_eio.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···88(* Custom error type for CFF parsing errors *)
99type Eio.Exn.err += E of string
10101111-let () = Eio.Exn.register_pp (fun f -> function
1212- | E msg -> Format.fprintf f "Cff %s" msg; true
1111+let () =
1212+ Eio.Exn.register_pp (fun f -> function
1313+ | E msg ->
1414+ Format.fprintf f "Cff %s" msg;
1515+ true
1316 | _ -> false)
1717+;;
14181519let err msg = Eio.Exn.create (E msg)
1620···1923 match Yamlt.decode ~layout:true Cff.jsont reader with
2024 | Ok cff -> cff
2125 | Error msg -> raise (err msg)
2626+;;
22272328let to_yaml_string t =
2429 let buf = Buffer.create 1024 in
···2631 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
2732 | Ok () -> Buffer.contents buf
2833 | Error msg -> raise (err msg)
3434+;;
29353036let of_yaml_flow flow =
3137 let reader = Bytesrw_eio.bytes_reader_of_flow flow in
3238 match Yamlt.decode ~layout:true Cff.jsont reader with
3339 | Ok cff -> cff
3440 | Error msg -> raise (err msg)
4141+;;
35423643let to_yaml_flow flow t =
3744 let writer = Bytesrw_eio.bytes_writer_of_flow flow in
3845 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
3946 | Ok () -> ()
4047 | Error msg -> raise (err msg)
4848+;;
41494250let of_file ~fs path =
4351 let data = Eio.Path.load Eio.Path.(fs / path) in
4444- try of_yaml_string data
4545- with Eio.Exn.Io _ as ex ->
5252+ try of_yaml_string data with
5353+ | Eio.Exn.Io _ as ex ->
4654 let bt = Printexc.get_raw_backtrace () in
4755 Eio.Exn.reraise_with_context ex bt "parsing CFF file %S" path
5656+;;
48574958let to_file ~fs path t =
5059 let data = to_yaml_string t in
5160 Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) data
6161+;;
+10-9
lib_eio/cff_eio.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···2828 Parsing and encoding errors are raised as {!Eio.Exn.Io} exceptions
2929 with the error type {!E}. *)
30303131-type Eio.Exn.err += E of string
3232-(** CFF parsing or encoding error. The string contains the error message. *)
3131+type Eio.Exn.err +=
3232+ | E of string
3333+ (** CFF parsing or encoding error. The string contains the error message. *)
33343435(** {1 String Functions} *)
35363636-val of_yaml_string : string -> Cff.t
3737(** [of_yaml_string s] parses a CFF from YAML string [s].
38383939 @raise Eio.Exn.Io on parse error. *)
4040+val of_yaml_string : string -> Cff.t
40414141-val to_yaml_string : Cff.t -> string
4242(** [to_yaml_string cff] serializes [cff] to a YAML string.
43434444 The output uses YAML block style for readability.
45454646 @raise Eio.Exn.Io on encoding error. *)
4747+val to_yaml_string : Cff.t -> string
47484849(** {1 Flow Functions} *)
49505050-val of_yaml_flow : _ Eio.Flow.source -> Cff.t
5151(** [of_yaml_flow flow] parses a CFF from an Eio source flow.
52525353 Reads directly from the flow using bytesrw-eio.
54545555 @raise Eio.Exn.Io on parse error. *)
5656+val of_yaml_flow : _ Eio.Flow.source -> Cff.t
56575757-val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit
5858(** [to_yaml_flow flow cff] serializes [cff] to an Eio sink flow.
59596060 Writes directly to the flow using bytesrw-eio.
61616262 @raise Eio.Exn.Io on encoding error. *)
6363+val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit
63646465(** {1 File Functions} *)
65666666-val of_file : fs:_ Eio.Path.t -> string -> Cff.t
6767(** [of_file ~fs path] reads and parses a [CITATION.cff] file.
68686969 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
7070 @param path Path to the CFF file
7171 @raise Eio.Exn.Io if the file cannot be read or contains invalid CFF data.
7272 The exception context includes the file path. *)
7373+val of_file : fs:_ Eio.Path.t -> string -> Cff.t
73747474-val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit
7575(** [to_file ~fs path cff] writes [cff] to a file at [path].
76767777 Creates or overwrites the file.
···7979 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env])
8080 @param path Path to write the CFF file
8181 @raise Eio.Exn.Io on I/O or encoding failure. *)
8282+val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit
+8-4
lib_unix/cff_unix.ml
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···88let of_yaml_string s =
99 let reader = Bytesrw.Bytes.Reader.of_string s in
1010 Yamlt.decode ~layout:true Cff.jsont reader
1111+;;
11121213let to_yaml_string t =
1314 let buf = Buffer.create 1024 in
···1516 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with
1617 | Ok () -> Ok (Buffer.contents buf)
1718 | Error e -> Error e
1919+;;
18201921let of_file path =
2022 match In_channel.with_open_text path In_channel.input_all with
2123 | s -> of_yaml_string s
2224 | exception Sys_error e -> Error e
2525+;;
23262427let to_file path t =
2528 match to_yaml_string t with
2629 | Error e -> Error e
2730 | Ok s ->
2828- match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with
2929- | () -> Ok ()
3030- | exception Sys_error e -> Error e
3131+ (match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with
3232+ | () -> Ok ()
3333+ | exception Sys_error e -> Error e)
3434+;;
+5-5
lib_unix/cff_unix.mli
···11(*---------------------------------------------------------------------------
22- Copyright (c) 2025 The ocaml-cff programmers. All rights reserved.
22+ Copyright (c) 2026 The ocaml-cff programmers. All rights reserved.
33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
55···22222323 {1 Functions} *)
24242525-val of_yaml_string : string -> (Cff.t, string) result
2625(** [of_yaml_string s] parses a CFF from YAML string [s].
27262827 Returns [Ok cff] on success or [Error msg] with a descriptive error
2928 message on failure. *)
2929+val of_yaml_string : string -> (Cff.t, string) result
30303131-val to_yaml_string : Cff.t -> (string, string) result
3231(** [to_yaml_string cff] serializes [cff] to a YAML string.
33323433 The output uses YAML block style for readability. *)
3434+val to_yaml_string : Cff.t -> (string, string) result
35353636-val of_file : string -> (Cff.t, string) result
3736(** [of_file path] reads and parses a [CITATION.cff] file.
38373938 Returns [Ok cff] on success or [Error msg] if the file cannot be
4039 read or contains invalid CFF data. *)
4040+val of_file : string -> (Cff.t, string) result
41414242-val to_file : string -> Cff.t -> (unit, string) result
4342(** [to_file path cff] writes [cff] to a file at [path].
44434544 Creates or overwrites the file. Returns [Error msg] on I/O failure. *)
4545+val to_file : string -> Cff.t -> (unit, string) result