OCaml codecs for the Citation File Format (CFF)

import

+2459 -1847
+17 -1
.gitignore
··· 1 - _build 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+2
.ocamlformat
··· 1 + version=0.28.1 2 + profile=janestreet
+53
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + 31 + steps: 32 + - name: opam 33 + command: | 34 + opam init --disable-sandboxing -a -y 35 + - name: repo 36 + command: | 37 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 38 + - name: switch 39 + command: | 40 + opam install . --confirm-level=unsafe-yes --deps-only 41 + - name: build 42 + command: | 43 + opam exec -- dune build -p cff 44 + - name: switch-test 45 + command: | 46 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 47 + - name: test 48 + command: | 49 + opam exec -- dune runtest --verbose 50 + - name: doc 51 + command: | 52 + opam install -y odoc 53 + opam exec -- dune build @doc
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+82
README.md
··· 1 + # ocaml-cff 2 + 3 + Citation File Format (CFF) codec for OCaml. 4 + 5 + A library for parsing and generating `CITATION.cff` files following the 6 + [CFF 1.2.0 specification](https://github.com/citation-file-format/citation-file-format). 7 + CFF is a human- and machine-readable format for software and dataset citation metadata. 8 + 9 + ## Features 10 + 11 + - Full CFF 1.2.0 specification support 12 + - SPDX license expression parsing via `spdx_licenses` 13 + - Lenient parsing (accepts unknown fields, preserves non-SPDX licenses for round-tripping) 14 + - Multiple I/O backends: 15 + - `cff.unix` - Unix file I/O using `In_channel`/`Out_channel` 16 + - `cff.eio` - Eio-based async I/O 17 + 18 + ## Installation 19 + 20 + ``` 21 + opam install cff 22 + ``` 23 + 24 + ## Usage 25 + 26 + ### Reading a CITATION.cff file 27 + 28 + ```ocaml 29 + (* Using cff.unix *) 30 + match Cff_unix.of_file "CITATION.cff" with 31 + | Ok cff -> 32 + Printf.printf "Title: %s\n" (Cff.title cff); 33 + Printf.printf "Authors: %d\n" (List.length (Cff.authors cff)) 34 + | Error msg -> 35 + Printf.eprintf "Error: %s\n" msg 36 + ``` 37 + 38 + ### Creating a CFF record programmatically 39 + 40 + ```ocaml 41 + let author = Cff.Author.person 42 + ~family_names:"Smith" 43 + ~given_names:"Jane" 44 + ~orcid:"https://orcid.org/0000-0001-2345-6789" 45 + () in 46 + let cff = Cff.make 47 + ~title:"My Research Software" 48 + ~authors:[author] 49 + ~version:"1.0.0" 50 + ~doi:"10.5281/zenodo.1234567" 51 + ~license:(`Spdx (Spdx_licenses.parse_exn "MIT")) 52 + () 53 + ``` 54 + 55 + ### Writing a CITATION.cff file 56 + 57 + ```ocaml 58 + match Cff_unix.to_file "CITATION.cff" cff with 59 + | Ok () -> print_endline "Written successfully" 60 + | Error msg -> Printf.eprintf "Error: %s\n" msg 61 + ``` 62 + 63 + ## API Overview 64 + 65 + ### Core Types 66 + 67 + - `Cff.t` - A complete CFF document 68 + - `Cff.Author.t` - Person or entity (polymorphic variant) 69 + - `Cff.Reference.t` - Bibliographic reference 70 + - `Cff.License.t` - SPDX expression or custom license with optional URL 71 + - `Cff.Identifier.t` - DOI, URL, SWH, or custom identifier 72 + 73 + ### Submodules 74 + 75 + - `Cff.Person` - Individual author with name fields 76 + - `Cff.Entity` - Organization, team, or conference 77 + - `Cff.Date` - ISO 8601 date as `(year, month, day)` tuple 78 + - `Cff.Reference` - Full bibliographic reference with 60+ fields 79 + 80 + ## License 81 + 82 + ISC License. See [LICENSE.md](LICENSE.md).
+7 -6
cff.opam
··· 3 3 synopsis: "Citation File Format (CFF) codec for OCaml" 4 4 description: 5 5 "A library for parsing and generating CITATION.cff files following the CFF 1.2.0 specification. Provides findlib subpackages: cff.unix for Unix file I/O and cff.eio for Eio-based I/O." 6 - maintainer: ["anil@recoil.org"] 7 - authors: ["The ocaml-cff programmers"] 6 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 + authors: ["Anil Madhavapeddy"] 8 8 license: "ISC" 9 - homepage: "https://github.com/avsm/ocaml-cff" 10 - bug-reports: "https://github.com/avsm/ocaml-cff/issues" 9 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-cff" 10 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-cff/issues" 11 11 depends: [ 12 12 "dune" {>= "3.20"} 13 - "ocaml" {>= "4.14.0"} 13 + "ocaml" {>= "5.1.0"} 14 14 "ptime" 15 15 "ISO3166" 16 16 "spdx_licenses" ··· 20 20 "eio" 21 21 "bytesrw-eio" 22 22 "odoc" {with-doc} 23 + "alcotest" {with-test & >= "1.7.0"} 24 + "eio_main" {with-test} 23 25 ] 24 26 build: [ 25 27 ["dune" "subst"] {dev} ··· 35 37 "@doc" {with-doc} 36 38 ] 37 39 ] 38 - dev-repo: "git+https://github.com/avsm/ocaml-cff.git" 39 40 x-maintenance-intent: ["(latest)"]
+16 -6
dune-project
··· 1 1 (lang dune 3.20) 2 + 2 3 (name cff) 4 + 3 5 (generate_opam_files true) 6 + 4 7 (license ISC) 5 - (authors "The ocaml-cff programmers") 6 - (maintainers "anil@recoil.org") 7 - (source (github avsm/ocaml-cff)) 8 + (authors "Anil Madhavapeddy") 9 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-cff") 10 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 11 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-cff/issues") 12 + (maintenance_intent "(latest)") 8 13 9 14 (package 10 15 (name cff) 11 16 (synopsis "Citation File Format (CFF) codec for OCaml") 12 17 (description 13 - "A library for parsing and generating CITATION.cff files following the CFF 1.2.0 specification. Provides findlib subpackages: cff.unix for Unix file I/O and cff.eio for Eio-based I/O.") 18 + "A library for parsing and generating CITATION.cff files following the \ 19 + CFF 1.2.0 specification. Provides findlib subpackages: cff.unix for Unix \ 20 + file I/O and cff.eio for Eio-based I/O.") 14 21 (depends 15 - (ocaml (>= 4.14.0)) 22 + (ocaml (>= 5.1.0)) 16 23 ptime 17 24 ISO3166 18 25 spdx_licenses ··· 20 27 yamlt 21 28 bytesrw 22 29 eio 23 - bytesrw-eio)) 30 + bytesrw-eio 31 + (odoc :with-doc) 32 + (alcotest (and :with-test (>= 1.7.0))) 33 + (eio_main :with-test)))
+153 -91
lib/cff.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 9 9 module Date = Cff_date 10 10 module Country = Cff_country 11 11 module License = Cff_license 12 - 13 12 module Identifier_type = Cff_enums.Identifier_type 14 13 module Reference_type = Cff_enums.Reference_type 15 14 module Status = Cff_enums.Status 16 15 module Cff_type = Cff_enums.Cff_type 17 - 18 16 module Address = Cff_address.Address 19 17 module Contact = Cff_address.Contact 20 - 21 18 module Author = Cff_author 22 - module Name = Cff_author.Name 23 19 module Person = Cff_author.Person 24 20 module Entity = Cff_author.Entity 25 - 26 21 module Identifier = Cff_identifier 27 22 module Reference = Cff_reference 28 23 29 24 (* Root CFF type - integrated directly *) 30 25 31 - type t = { 32 - cff_version : string; 33 - message : string; 34 - title : string; 35 - authors : Cff_author.t list; 36 - abstract : string option; 37 - commit : string option; 38 - contact : Cff_author.t list option; 39 - date_released : Cff_date.t option; 40 - doi : string option; 41 - identifiers : Cff_identifier.t list option; 42 - keywords : string list option; 43 - license : Cff_license.t option; 44 - license_url : string option; 45 - preferred_citation : Cff_reference.t option; 46 - references : Cff_reference.t list option; 47 - repository : string option; 48 - repository_artifact : string option; 49 - repository_code : string option; 50 - type_ : Cff_enums.Cff_type.t option; 51 - url : string option; 52 - version : string option; 53 - } 26 + type t = 27 + { cff_version : string 28 + ; message : string 29 + ; title : string 30 + ; authors : Cff_author.t list 31 + ; abstract : string option 32 + ; commit : string option 33 + ; contact : Cff_author.t list option 34 + ; date_released : Cff_date.t option 35 + ; doi : string option 36 + ; identifiers : Cff_identifier.t list option 37 + ; keywords : string list option 38 + ; license : Cff_license.t option 39 + ; preferred_citation : Cff_reference.t option 40 + ; references : Cff_reference.t list option 41 + ; repository : string option 42 + ; repository_artifact : string option 43 + ; repository_code : string option 44 + ; type_ : Cff_enums.Cff_type.t option 45 + ; url : string option 46 + ; version : string option 47 + } 54 48 55 49 let default_cff_version = "1.2.0" 56 - let default_message = "If you use this software, please cite it using the metadata from this file." 50 + 51 + let default_message = 52 + "If you use this software, please cite it using the metadata from this file." 53 + ;; 57 54 58 55 let make 59 - ?(cff_version = default_cff_version) 60 - ?(message = default_message) 61 - ~title 62 - ~authors 63 - ?abstract 64 - ?commit 65 - ?contact 66 - ?date_released 67 - ?doi 68 - ?identifiers 69 - ?keywords 70 - ?license 71 - ?license_url 72 - ?preferred_citation 73 - ?references 74 - ?repository 75 - ?repository_artifact 76 - ?repository_code 77 - ?type_ 78 - ?url 79 - ?version 80 - () = 81 - { cff_version; message; title; authors; 82 - abstract; commit; contact; date_released; doi; 83 - identifiers; keywords; license; license_url; 84 - preferred_citation; references; repository; 85 - repository_artifact; repository_code; type_; url; version } 56 + ?(cff_version = default_cff_version) 57 + ?(message = default_message) 58 + ~title 59 + ~authors 60 + ?abstract 61 + ?commit 62 + ?contact 63 + ?date_released 64 + ?doi 65 + ?identifiers 66 + ?keywords 67 + ?license 68 + ?preferred_citation 69 + ?references 70 + ?repository 71 + ?repository_artifact 72 + ?repository_code 73 + ?type_ 74 + ?url 75 + ?version 76 + () 77 + = 78 + { cff_version 79 + ; message 80 + ; title 81 + ; authors 82 + ; abstract 83 + ; commit 84 + ; contact 85 + ; date_released 86 + ; doi 87 + ; identifiers 88 + ; keywords 89 + ; license 90 + ; preferred_citation 91 + ; references 92 + ; repository 93 + ; repository_artifact 94 + ; repository_code 95 + ; type_ 96 + ; url 97 + ; version 98 + } 99 + ;; 86 100 87 101 (* Required field accessors *) 88 102 let cff_version t = t.cff_version ··· 99 113 let identifiers t = t.identifiers 100 114 let keywords t = t.keywords 101 115 let license t = t.license 102 - let license_url t = t.license_url 103 116 let preferred_citation t = t.preferred_citation 104 117 let references t = t.references 105 118 let repository t = t.repository ··· 118 131 List.iter (fun a -> Format.fprintf ppf " - %a@," Cff_author.pp a) t.authors; 119 132 Option.iter (fun v -> Format.fprintf ppf "version: %s@," v) t.version; 120 133 Option.iter (fun v -> Format.fprintf ppf "doi: %s@," v) t.doi; 121 - Option.iter (fun v -> Format.fprintf ppf "date-released: %a@," Cff_date.pp v) t.date_released; 134 + Option.iter 135 + (fun v -> Format.fprintf ppf "date-released: %a@," Cff_date.pp v) 136 + t.date_released; 122 137 Option.iter (fun v -> Format.fprintf ppf "license: %a@," Cff_license.pp v) t.license; 123 138 Option.iter (fun v -> Format.fprintf ppf "url: %s@," v) t.url; 124 139 Option.iter (fun v -> Format.fprintf ppf "repository: %s@," v) t.repository; ··· 126 141 Option.iter (fun v -> Format.fprintf ppf "abstract: %s@," v) t.abstract; 127 142 Option.iter (fun v -> Format.fprintf ppf "commit: %s@," v) t.commit; 128 143 Option.iter (fun v -> Format.fprintf ppf "type: %a@," Cff_enums.Cff_type.pp v) t.type_; 129 - Option.iter (fun kws -> 130 - Format.fprintf ppf "keywords:@,"; 131 - List.iter (fun k -> Format.fprintf ppf " - %s@," k) kws 132 - ) t.keywords; 133 - Option.iter (fun ids -> 134 - Format.fprintf ppf "identifiers:@,"; 135 - List.iter (fun id -> Format.fprintf ppf " - %a@," Cff_identifier.pp id) ids 136 - ) t.identifiers; 137 - Option.iter (fun contacts -> 138 - Format.fprintf ppf "contact:@,"; 139 - List.iter (fun c -> Format.fprintf ppf " - %a@," Cff_author.pp c) contacts 140 - ) t.contact; 141 - Option.iter (fun refs -> 142 - Format.fprintf ppf "references:@,"; 143 - List.iter (fun r -> Format.fprintf ppf " - %a@," Cff_reference.pp r) refs 144 - ) t.references; 145 - Option.iter (fun pc -> 146 - Format.fprintf ppf "preferred-citation:@, %a@," Cff_reference.pp pc 147 - ) t.preferred_citation; 144 + Option.iter 145 + (fun kws -> 146 + Format.fprintf ppf "keywords:@,"; 147 + List.iter (fun k -> Format.fprintf ppf " - %s@," k) kws) 148 + t.keywords; 149 + Option.iter 150 + (fun ids -> 151 + Format.fprintf ppf "identifiers:@,"; 152 + List.iter (fun id -> Format.fprintf ppf " - %a@," Cff_identifier.pp id) ids) 153 + t.identifiers; 154 + Option.iter 155 + (fun contacts -> 156 + Format.fprintf ppf "contact:@,"; 157 + List.iter (fun c -> Format.fprintf ppf " - %a@," Cff_author.pp c) contacts) 158 + t.contact; 159 + Option.iter 160 + (fun refs -> 161 + Format.fprintf ppf "references:@,"; 162 + List.iter (fun r -> Format.fprintf ppf " - %a@," Cff_reference.pp r) refs) 163 + t.references; 164 + Option.iter 165 + (fun pc -> Format.fprintf ppf "preferred-citation:@, %a@," Cff_reference.pp pc) 166 + t.preferred_citation; 148 167 Format.fprintf ppf "@]" 168 + ;; 149 169 150 - let list_jsont elt = Jsont.(array elt |> map ~dec:Stdlib.Array.to_list ~enc:Stdlib.Array.of_list) 170 + let list_jsont elt = 171 + Jsont.(array elt |> map ~dec:Stdlib.Array.to_list ~enc:Stdlib.Array.of_list) 172 + ;; 151 173 152 174 let jsont = 153 175 let open Jsont in ··· 155 177 let identifiers_jsont = list_jsont Cff_identifier.jsont in 156 178 let references_jsont = list_jsont Cff_reference.jsont in 157 179 let keywords_jsont = list_jsont string in 158 - Object.map ~kind:"CFF" 159 - (fun cff_version message title authors abstract commit contact 160 - date_released doi identifiers keywords license license_url 161 - preferred_citation references repository repository_artifact 162 - repository_code type_ url version -> 163 - { cff_version; message; title; authors; 164 - abstract; commit; contact; date_released; doi; 165 - identifiers; keywords; license; license_url; 166 - preferred_citation; references; repository; 167 - repository_artifact; repository_code; type_; url; version }) 180 + Object.map 181 + ~kind:"CFF" 182 + (fun 183 + cff_version 184 + message 185 + title 186 + authors 187 + abstract 188 + commit 189 + contact 190 + date_released 191 + doi 192 + identifiers 193 + keywords 194 + license 195 + license_url 196 + preferred_citation 197 + references 198 + repository 199 + repository_artifact 200 + repository_code 201 + type_ 202 + url 203 + version 204 + -> 205 + let license = Option.map (Cff_license.with_url_opt license_url) license in 206 + { cff_version 207 + ; message 208 + ; title 209 + ; authors 210 + ; abstract 211 + ; commit 212 + ; contact 213 + ; date_released 214 + ; doi 215 + ; identifiers 216 + ; keywords 217 + ; license 218 + ; preferred_citation 219 + ; references 220 + ; repository 221 + ; repository_artifact 222 + ; repository_code 223 + ; type_ 224 + ; url 225 + ; version 226 + }) 168 227 |> Object.mem "cff-version" string ~enc:(fun t -> t.cff_version) 169 228 |> Object.mem "message" string ~enc:(fun t -> t.message) 170 229 |> Object.mem "title" string ~enc:(fun t -> t.title) ··· 177 236 |> Object.opt_mem "identifiers" identifiers_jsont ~enc:(fun t -> t.identifiers) 178 237 |> Object.opt_mem "keywords" keywords_jsont ~enc:(fun t -> t.keywords) 179 238 |> Object.opt_mem "license" Cff_license.jsont ~enc:(fun t -> t.license) 180 - |> Object.opt_mem "license-url" string ~enc:(fun t -> t.license_url) 181 - |> Object.opt_mem "preferred-citation" Cff_reference.jsont ~enc:(fun t -> t.preferred_citation) 239 + |> Object.opt_mem "license-url" string ~enc:(fun t -> 240 + Option.bind t.license Cff_license.url) 241 + |> Object.opt_mem "preferred-citation" Cff_reference.jsont ~enc:(fun t -> 242 + t.preferred_citation) 182 243 |> Object.opt_mem "references" references_jsont ~enc:(fun t -> t.references) 183 244 |> Object.opt_mem "repository" string ~enc:(fun t -> t.repository) 184 245 |> Object.opt_mem "repository-artifact" string ~enc:(fun t -> t.repository_artifact) ··· 187 248 |> Object.opt_mem "url" string ~enc:(fun t -> t.url) 188 249 |> Object.opt_mem "version" string ~enc:(fun t -> t.version) 189 250 |> Object.finish 251 + ;;
+65 -61
lib/cff.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (** Citation File Format (CFF) codec for OCaml. 7 7 8 - This library provides types and codecs for the 8 + This library provides codecs for the 9 9 {{:https://citation-file-format.github.io/}Citation File Format (CFF)} 10 10 version 1.2.0, a human- and machine-readable format for software and 11 11 dataset citation metadata. ··· 22 22 - [title]: The name of the software or dataset 23 23 - [authors]: A list of persons and/or entities 24 24 25 - {1 Quick Start} 26 - 27 25 {2 Creating a CFF record} 28 26 29 27 {[ 30 - let author = Cff.Author.Person 31 - (Cff.Person.make ~family_names:"Smith" ~given_names:"Jane" ()) in 28 + let author = Cff.Author.person 29 + ~family_names:"Smith" ~given_names:"Jane" () in 32 30 let cff = Cff.make 33 31 ~title:"My Research Software" 34 32 ~authors:[author] ··· 37 35 () 38 36 ]} 39 37 40 - {2 File I/O} 38 + {2 I/O} 41 39 42 40 For file operations, use the backend-specific subpackages: 43 41 - [cff.unix] - Unix file I/O using {!In_channel}/{!Out_channel} ··· 54 52 55 53 This implementation follows the 56 54 {{:https://github.com/citation-file-format/citation-file-format}CFF 1.2.0 specification}. 57 - Key concepts include: 55 + Useful modules include: 58 56 59 57 - {!module:Author}: Can be persons (with family/given names) or entities 60 58 (organizations, identified by a [name] field) ··· 100 98 Used for author and entity addresses. *) 101 99 module Country = Cff_country 102 100 101 + (** Physical address information. 102 + 103 + Address fields used for persons and entities: street address, city, 104 + region (state/province), postal code, and country code. *) 105 + module Address = Cff_address.Address 106 + 107 + (** Contact information. 108 + 109 + Contact fields used for persons and entities: email, telephone, fax, 110 + website URL, and ORCID identifier. *) 111 + module Contact = Cff_address.Contact 112 + 103 113 (** SPDX license identifiers. 104 114 105 115 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers} for ··· 160 170 161 171 (** {1 Construction} *) 162 172 163 - val default_cff_version : string 164 173 (** The default CFF version used when not specified: ["1.2.0"]. *) 174 + val default_cff_version : string 165 175 166 - val default_message : string 167 176 (** The default citation message: 168 177 ["If you use this software, please cite it using the metadata from this file."] *) 178 + val default_message : string 169 179 170 - val make : 171 - ?cff_version:string -> 172 - ?message:string -> 173 - title:string -> 174 - authors:Author.t list -> 175 - ?abstract:string -> 176 - ?commit:string -> 177 - ?contact:Author.t list -> 178 - ?date_released:Date.t -> 179 - ?doi:string -> 180 - ?identifiers:Identifier.t list -> 181 - ?keywords:string list -> 182 - ?license:License.t -> 183 - ?license_url:string -> 184 - ?preferred_citation:Reference.t -> 185 - ?references:Reference.t list -> 186 - ?repository:string -> 187 - ?repository_artifact:string -> 188 - ?repository_code:string -> 189 - ?type_:Cff_type.t -> 190 - ?url:string -> 191 - ?version:string -> 192 - unit -> t 193 180 (** [make ~title ~authors ...] constructs a CFF value. 194 181 195 182 @param cff_version The CFF schema version (default: {!default_cff_version}) 196 183 @param message Instructions for users on how to cite (default: {!default_message}) 197 184 @param title The name of the software or dataset 198 185 @param authors List of persons and/or entities who created the work *) 186 + val make 187 + : ?cff_version:string 188 + -> ?message:string 189 + -> title:string 190 + -> authors:Author.t list 191 + -> ?abstract:string 192 + -> ?commit:string 193 + -> ?contact:Author.t list 194 + -> ?date_released:Date.t 195 + -> ?doi:string 196 + -> ?identifiers:Identifier.t list 197 + -> ?keywords:string list 198 + -> ?license:License.t 199 + -> ?preferred_citation:Reference.t 200 + -> ?references:Reference.t list 201 + -> ?repository:string 202 + -> ?repository_artifact:string 203 + -> ?repository_code:string 204 + -> ?type_:Cff_type.t 205 + -> ?url:string 206 + -> ?version:string 207 + -> unit 208 + -> t 199 209 200 210 (** {2 Required Fields} *) 201 211 202 - val cff_version : t -> string 203 212 (** The CFF schema version that this file adheres to. 204 213 205 214 For CFF 1.2.0 files, this should be ["1.2.0"]. The version determines 206 215 which keys are valid and how they should be interpreted. *) 216 + val cff_version : t -> string 207 217 208 - val message : t -> string 209 218 (** A message to readers explaining how to cite the work. 210 219 211 220 Common examples: ··· 213 222 - ["Please cite this software using the metadata from 'preferred-citation'."] 214 223 215 224 The message should guide users toward the preferred citation method. *) 225 + val message : t -> string 216 226 217 - val title : t -> string 218 227 (** The name of the software or dataset. 219 228 220 229 This is the title that should appear in citations. For software, it's 221 230 typically the project name; for datasets, the dataset title. *) 231 + val title : t -> string 222 232 223 - val authors : t -> Author.t list 224 233 (** The creators of the software or dataset. 225 234 226 235 Authors can be persons (individuals) or entities (organizations). 227 236 At least one author is required for a valid CFF file. The order 228 237 typically reflects contribution significance. *) 238 + val authors : t -> Author.t list 229 239 230 240 (** {2 Optional Fields} *) 231 241 232 - val abstract : t -> string option 233 242 (** A description of the software or dataset. 234 243 235 244 Provides context about what the work does, its purpose, and scope. *) 245 + val abstract : t -> string option 236 246 237 - val commit : t -> string option 238 247 (** The commit hash or revision number of the software version. 239 248 240 249 Useful for precise version identification beyond semantic versioning. 241 250 Example: ["1ff847d81f29c45a3a1a5ce73d38e45c2f319bba"] *) 251 + val commit : t -> string option 242 252 243 - val contact : t -> Author.t list option 244 253 (** Contact persons or entities for the software or dataset. 245 254 246 255 May differ from authors; useful when the primary contact is a 247 256 project maintainer rather than the original author. *) 257 + val contact : t -> Author.t list option 248 258 249 - val date_released : t -> Date.t option 250 259 (** The date when the software or dataset was released. 251 260 252 261 Format is [(year, month, day)], corresponding to ISO 8601 [YYYY-MM-DD]. *) 262 + val date_released : t -> Date.t option 253 263 254 - val doi : t -> string option 255 264 (** The Digital Object Identifier for the software or dataset. 256 265 257 266 DOIs provide persistent, citable identifiers. This is a shorthand 258 267 for a single DOI; use {!identifiers} for multiple DOIs or other 259 268 identifier types. Example: ["10.5281/zenodo.1234567"] *) 269 + val doi : t -> string option 260 270 261 - val identifiers : t -> Identifier.t list option 262 271 (** Additional identifiers beyond the primary DOI. 263 272 264 273 Each identifier has a type (DOI, URL, SWH, other), value, and 265 274 optional description. Useful for versioned DOIs, Software Heritage 266 275 identifiers, or repository URLs. *) 276 + val identifiers : t -> Identifier.t list option 267 277 268 - val keywords : t -> string list option 269 278 (** Descriptive keywords for the work. 270 279 271 280 Help with discoverability and categorization. Example: 272 281 [["machine learning"; "image processing"; "python"]] *) 282 + val keywords : t -> string list option 273 283 274 - val license : t -> License.t option 275 284 (** The SPDX license identifier(s) for the work. 276 285 277 286 Uses {{:https://spdx.org/licenses/}SPDX identifiers}. Multiple 278 287 licenses imply an OR relationship (user may choose any). 279 288 Example: ["MIT"], ["Apache-2.0"], or [["GPL-3.0-only"; "MIT"]]. *) 280 - 281 - val license_url : t -> string option 282 - (** URL to the license text for non-standard licenses. 283 - 284 - Only needed for licenses not in the SPDX list. Standard SPDX 285 - licenses have well-known URLs. *) 289 + val license : t -> License.t option 286 290 287 - val preferred_citation : t -> Reference.t option 288 291 (** A reference to cite instead of the software itself. 289 292 290 293 Used for "credit redirection" when authors prefer citation of 291 294 a related publication (e.g., a methods paper) over the software. 292 295 Note: Software citation principles recommend citing software 293 296 directly; use this field judiciously. *) 297 + val preferred_citation : t -> Reference.t option 294 298 295 - val references : t -> Reference.t list option 296 299 (** Works that this software cites or depends upon. 297 300 298 301 Functions like a bibliography, listing dependencies, foundational 299 302 works, or related publications. Each reference includes full 300 303 bibliographic metadata. *) 304 + val references : t -> Reference.t list option 301 305 302 - val repository : t -> string option 303 306 (** URL to the repository where the software is developed. 304 307 305 308 Typically a version control system URL. For source code repositories, 306 309 prefer {!repository_code}. *) 310 + val repository : t -> string option 307 311 308 - val repository_artifact : t -> string option 309 312 (** URL to the built/compiled artifact repository. 310 313 311 314 For binary distributions, package registries (npm, PyPI, CRAN), 312 315 or container registries. *) 316 + val repository_artifact : t -> string option 313 317 314 - val repository_code : t -> string option 315 318 (** URL to the source code repository. 316 319 317 320 Typically a GitHub, GitLab, or similar URL where the source 318 321 code is publicly accessible. *) 322 + val repository_code : t -> string option 319 323 320 - val type_ : t -> Cff_type.t option 321 324 (** The type of work: [`Software] (default) or [`Dataset]. 322 325 323 326 Most CFF files describe software; use [`Dataset] for data packages. *) 327 + val type_ : t -> Cff_type.t option 324 328 325 - val url : t -> string option 326 329 (** The URL of the software or dataset homepage. 327 330 328 331 A general landing page, documentation site, or project website. *) 332 + val url : t -> string option 329 333 330 - val version : t -> string option 331 334 (** The version string of the software or dataset. 332 335 333 336 Can be any version format: semantic versioning (["1.2.3"]), 334 337 date-based (["2024.01"]), or other schemes. *) 338 + val version : t -> string option 335 339 336 340 (** {1 Formatting and Codec} *) 337 341 342 + (** Pretty-print a CFF value in a human-readable YAML-like format. *) 338 343 val pp : Format.formatter -> t -> unit 339 - (** Pretty-print a CFF value in a human-readable YAML-like format. *) 340 344 341 - val jsont : t Jsont.t 342 345 (** JSON/YAML codec for serialization and deserialization. 343 346 344 347 Used internally by the YAML codec functions. *) 348 + val jsont : t Jsont.t
+43 -55
lib/cff_address.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 7 7 8 8 (** Physical address information. *) 9 9 module Address = struct 10 - type t = { 11 - address : string option; 12 - city : string option; 13 - region : string option; 14 - post_code : string option; 15 - country : string option; (* ISO 3166-1 alpha-2 *) 16 - } 10 + type t = 11 + { address : string option 12 + ; city : string option 13 + ; region : string option 14 + ; post_code : string option 15 + ; country : string option (* ISO 3166-1 alpha-2 *) 16 + } 17 17 18 - let empty = { 19 - address = None; 20 - city = None; 21 - region = None; 22 - post_code = None; 23 - country = None; 24 - } 18 + let empty = 19 + { address = None; city = None; region = None; post_code = None; country = None } 20 + ;; 25 21 26 22 let make ?address ?city ?region ?post_code ?country () = 27 23 { address; city; region; post_code; country } 24 + ;; 28 25 29 26 let of_options ~address ~city ~region ~post_code ~country = 30 27 { address; city; region; post_code; country } 28 + ;; 31 29 32 30 let address t = t.address 33 31 let city t = t.city ··· 36 34 let country t = t.country 37 35 38 36 let is_empty t = 39 - t.address = None && t.city = None && t.region = None && 40 - t.post_code = None && t.country = None 37 + t.address = None 38 + && t.city = None 39 + && t.region = None 40 + && t.post_code = None 41 + && t.country = None 42 + ;; 41 43 42 44 let pp ppf t = 43 - let parts = List.filter_map Fun.id [ 44 - t.address; 45 - t.city; 46 - t.region; 47 - t.post_code; 48 - t.country; 49 - ] in 45 + let parts = 46 + List.filter_map Fun.id [ t.address; t.city; t.region; t.post_code; t.country ] 47 + in 50 48 Format.pp_print_string ppf (String.concat ", " parts) 49 + ;; 51 50 52 51 let jsont_fields ~get obj = 53 52 obj ··· 56 55 |> Jsont.Object.opt_mem "region" Jsont.string ~enc:(fun x -> (get x).region) 57 56 |> Jsont.Object.opt_mem "post-code" Jsont.string ~enc:(fun x -> (get x).post_code) 58 57 |> Jsont.Object.opt_mem "country" Jsont.string ~enc:(fun x -> (get x).country) 58 + ;; 59 59 end 60 60 61 61 (** Contact information. *) 62 62 module Contact = struct 63 - type t = { 64 - email : string option; 65 - tel : string option; 66 - fax : string option; 67 - website : string option; 68 - orcid : string option; 69 - } 63 + type t = 64 + { email : string option 65 + ; tel : string option 66 + ; fax : string option 67 + ; website : string option 68 + ; orcid : string option 69 + } 70 70 71 - let empty = { 72 - email = None; 73 - tel = None; 74 - fax = None; 75 - website = None; 76 - orcid = None; 77 - } 78 - 79 - let make ?email ?tel ?fax ?website ?orcid () = 80 - { email; tel; fax; website; orcid } 81 - 82 - let of_options ~email ~tel ~fax ~website ~orcid = 83 - { email; tel; fax; website; orcid } 84 - 71 + let empty = { email = None; tel = None; fax = None; website = None; orcid = None } 72 + let make ?email ?tel ?fax ?website ?orcid () = { email; tel; fax; website; orcid } 73 + let of_options ~email ~tel ~fax ~website ~orcid = { email; tel; fax; website; orcid } 85 74 let email t = t.email 86 75 let tel t = t.tel 87 76 let fax t = t.fax ··· 89 78 let orcid t = t.orcid 90 79 91 80 let is_empty t = 92 - t.email = None && t.tel = None && t.fax = None && 93 - t.website = None && t.orcid = None 81 + t.email = None && t.tel = None && t.fax = None && t.website = None && t.orcid = None 82 + ;; 94 83 95 84 let pp ppf t = 96 - let parts = List.filter_map (fun (k, v) -> 97 - Option.map (fun v -> k ^ ": " ^ v) v 98 - ) [ 99 - ("email", t.email); 100 - ("tel", t.tel); 101 - ("website", t.website); 102 - ("orcid", t.orcid); 103 - ] in 85 + let parts = 86 + List.filter_map 87 + (fun (k, v) -> Option.map (fun v -> k ^ ": " ^ v) v) 88 + [ "email", t.email; "tel", t.tel; "website", t.website; "orcid", t.orcid ] 89 + in 104 90 Format.pp_print_string ppf (String.concat ", " parts) 91 + ;; 105 92 106 93 let jsont_fields ~get obj = 107 94 obj ··· 110 97 |> Jsont.Object.opt_mem "fax" Jsont.string ~enc:(fun x -> (get x).fax) 111 98 |> Jsont.Object.opt_mem "website" Jsont.string ~enc:(fun x -> (get x).website) 112 99 |> Jsont.Object.opt_mem "orcid" Jsont.string ~enc:(fun x -> (get x).orcid) 100 + ;; 113 101 end
+71 -57
lib/cff_address.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 48 48 49 49 All fields are optional; an empty address is valid. *) 50 50 module Address : sig 51 - type t 52 51 (** Physical address record. *) 52 + type t 53 53 54 - val empty : t 55 54 (** Empty address with all fields [None]. *) 55 + val empty : t 56 56 57 - val make : 58 - ?address:string -> 59 - ?city:string -> 60 - ?region:string -> 61 - ?post_code:string -> 62 - ?country:string -> 63 - unit -> t 64 57 (** Create an address with optional fields. 65 58 66 59 @param address Street address ··· 68 61 @param region State, province, or administrative region 69 62 @param post_code Postal code, ZIP code, or postcode 70 63 @param country ISO 3166-1 alpha-2 country code *) 64 + val make 65 + : ?address:string 66 + -> ?city:string 67 + -> ?region:string 68 + -> ?post_code:string 69 + -> ?country:string 70 + -> unit 71 + -> t 71 72 72 - val of_options : 73 - address:string option -> 74 - city:string option -> 75 - region:string option -> 76 - post_code:string option -> 77 - country:string option -> 78 - t 79 73 (** Create an address from option values directly. 80 74 81 75 Used internally by jsont decoders where fields are decoded as options. *) 76 + val of_options 77 + : address:string option 78 + -> city:string option 79 + -> region:string option 80 + -> post_code:string option 81 + -> country:string option 82 + -> t 82 83 83 - val address : t -> string option 84 84 (** Street address (e.g., ["77 Massachusetts Avenue"]). *) 85 + val address : t -> string option 85 86 86 - val city : t -> string option 87 87 (** City name (e.g., ["Cambridge"], ["London"]). *) 88 + val city : t -> string option 88 89 89 - val region : t -> string option 90 90 (** State, province, or region (e.g., ["Massachusetts"], ["Bavaria"]). *) 91 + val region : t -> string option 91 92 92 - val post_code : t -> string option 93 93 (** Postal or ZIP code (e.g., ["02139"], ["W1A 1AA"]). *) 94 + val post_code : t -> string option 94 95 95 - val country : t -> string option 96 96 (** ISO 3166-1 alpha-2 country code (e.g., ["US"], ["DE"], ["GB"]). *) 97 + val country : t -> string option 97 98 98 - val is_empty : t -> bool 99 99 (** [true] if all fields are [None]. *) 100 + val is_empty : t -> bool 100 101 101 - val pp : Format.formatter -> t -> unit 102 102 (** Pretty-print the address. *) 103 + val pp : Format.formatter -> t -> unit 103 104 104 - val jsont_fields : 105 - get:('a -> t) -> 106 - ('a, string option -> string option -> string option -> 107 - string option -> string option -> 'b) Jsont.Object.map -> 108 - ('a, 'b) Jsont.Object.map 109 105 (** Add address fields to a jsont object builder. 110 106 111 107 This adds the five address fields (address, city, region, post-code, ··· 113 109 [string option] arguments in that order. 114 110 115 111 @param get Extracts the address from the parent type for encoding *) 112 + val jsont_fields 113 + : get:('a -> t) 114 + -> ( 'a 115 + , string option 116 + -> string option 117 + -> string option 118 + -> string option 119 + -> string option 120 + -> 'b ) 121 + Jsont.Object.map 122 + -> ('a, 'b) Jsont.Object.map 116 123 end 117 124 118 125 (** Contact information. ··· 120 127 Electronic contact details for persons and entities. All fields 121 128 are optional. *) 122 129 module Contact : sig 123 - type t 124 130 (** Contact information record. *) 131 + type t 125 132 126 - val empty : t 127 133 (** Empty contact with all fields [None]. *) 134 + val empty : t 128 135 129 - val make : 130 - ?email:string -> 131 - ?tel:string -> 132 - ?fax:string -> 133 - ?website:string -> 134 - ?orcid:string -> 135 - unit -> t 136 136 (** Create contact information with optional fields. 137 137 138 138 @param email Email address ··· 140 140 @param fax Fax number (any format) 141 141 @param website Website URL 142 142 @param orcid ORCID identifier URL *) 143 + val make 144 + : ?email:string 145 + -> ?tel:string 146 + -> ?fax:string 147 + -> ?website:string 148 + -> ?orcid:string 149 + -> unit 150 + -> t 143 151 144 - val of_options : 145 - email:string option -> 146 - tel:string option -> 147 - fax:string option -> 148 - website:string option -> 149 - orcid:string option -> 150 - t 151 152 (** Create contact info from option values directly. 152 153 153 154 Used internally by jsont decoders where fields are decoded as options. *) 155 + val of_options 156 + : email:string option 157 + -> tel:string option 158 + -> fax:string option 159 + -> website:string option 160 + -> orcid:string option 161 + -> t 154 162 155 - val email : t -> string option 156 163 (** Email address (e.g., ["jane.smith\@example.org"]). *) 164 + val email : t -> string option 157 165 158 - val tel : t -> string option 159 166 (** Telephone number. No specific format is required. *) 167 + val tel : t -> string option 160 168 161 - val fax : t -> string option 162 169 (** Fax number. No specific format is required. *) 170 + val fax : t -> string option 163 171 164 - val website : t -> string option 165 172 (** Website URL (e.g., ["https://example.org/~jsmith"]). *) 173 + val website : t -> string option 166 174 167 - val orcid : t -> string option 168 175 (** ORCID identifier as a URL. 169 176 170 177 ORCID (Open Researcher and Contributor ID) provides persistent ··· 173 180 Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"] 174 181 175 182 Example: ["https://orcid.org/0000-0001-2345-6789"] *) 183 + val orcid : t -> string option 176 184 177 - val is_empty : t -> bool 178 185 (** [true] if all fields are [None]. *) 186 + val is_empty : t -> bool 179 187 180 - val pp : Format.formatter -> t -> unit 181 188 (** Pretty-print the contact information. *) 189 + val pp : Format.formatter -> t -> unit 182 190 183 - val jsont_fields : 184 - get:('a -> t) -> 185 - ('a, string option -> string option -> string option -> 186 - string option -> string option -> 'b) Jsont.Object.map -> 187 - ('a, 'b) Jsont.Object.map 188 191 (** Add contact fields to a jsont object builder. 189 192 190 193 This adds the five contact fields (email, tel, fax, website, orcid) ··· 192 195 [string option] arguments in that order. 193 196 194 197 @param get Extracts the contact from the parent type for encoding *) 198 + val jsont_fields 199 + : get:('a -> t) 200 + -> ( 'a 201 + , string option 202 + -> string option 203 + -> string option 204 + -> string option 205 + -> string option 206 + -> 'b ) 207 + Jsont.Object.map 208 + -> ('a, 'b) Jsont.Object.map 195 209 end
+189 -166
lib/cff_author.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (** Person, Entity, and Author types for CFF. *) 7 7 8 - (** Person name components. *) 9 - module Name = struct 10 - type t = { 11 - family_names : string option; 12 - given_names : string option; 13 - name_particle : string option; (* e.g., "von" *) 14 - name_suffix : string option; (* e.g., "Jr." *) 15 - alias : string option; 16 - } 8 + (** A person (individual author/contributor). *) 9 + module Person = struct 10 + type t = 11 + { family_names : string option 12 + ; given_names : string option 13 + ; name_particle : string option 14 + ; name_suffix : string option 15 + ; alias : string option 16 + ; affiliation : string option 17 + ; address : Cff_address.Address.t 18 + ; contact : Cff_address.Contact.t 19 + } 17 20 18 - let empty = { 19 - family_names = None; 20 - given_names = None; 21 - name_particle = None; 22 - name_suffix = None; 23 - alias = None; 24 - } 25 - 26 - let make ?family_names ?given_names ?name_particle ?name_suffix ?alias () = 27 - { family_names; given_names; name_particle; name_suffix; alias } 21 + let make 22 + ?family_names 23 + ?given_names 24 + ?name_particle 25 + ?name_suffix 26 + ?alias 27 + ?affiliation 28 + ?(address = Cff_address.Address.empty) 29 + ?(contact = Cff_address.Contact.empty) 30 + () 31 + = 32 + { family_names 33 + ; given_names 34 + ; name_particle 35 + ; name_suffix 36 + ; alias 37 + ; affiliation 38 + ; address 39 + ; contact 40 + } 41 + ;; 28 42 29 43 let family_names t = t.family_names 30 44 let given_names t = t.given_names 31 45 let name_particle t = t.name_particle 32 46 let name_suffix t = t.name_suffix 33 47 let alias t = t.alias 48 + let affiliation t = t.affiliation 49 + let address t = t.address 50 + let contact t = t.contact 34 51 35 52 let full_name t = 36 - let parts = List.filter_map Fun.id [ 37 - t.given_names; 38 - t.name_particle; 39 - t.family_names; 40 - ] in 53 + let parts = 54 + List.filter_map Fun.id [ t.given_names; t.name_particle; t.family_names ] 55 + in 41 56 let base = String.concat " " parts in 42 57 match t.name_suffix with 43 58 | Some suffix -> base ^ ", " ^ suffix 44 59 | None -> base 45 - 46 - let pp ppf t = 47 - Format.pp_print_string ppf (full_name t) 48 - end 49 - 50 - (** A person (individual author/contributor). *) 51 - module Person = struct 52 - type t = { 53 - name : Name.t; 54 - affiliation : string option; 55 - address : Cff_address.Address.t; 56 - contact : Cff_address.Contact.t; 57 - } 58 - 59 - let make 60 - ?family_names ?given_names ?name_particle ?name_suffix ?alias 61 - ?affiliation 62 - ?(address = Cff_address.Address.empty) 63 - ?(contact = Cff_address.Contact.empty) 64 - () = 65 - let name = Name.make ?family_names ?given_names ?name_particle 66 - ?name_suffix ?alias () in 67 - { name; affiliation; address; contact } 68 - 69 - let name t = t.name 70 - let affiliation t = t.affiliation 71 - let address t = t.address 72 - let contact t = t.contact 73 - 74 - let family_names t = Name.family_names t.name 75 - let given_names t = Name.given_names t.name 76 - let name_particle t = Name.name_particle t.name 77 - let name_suffix t = Name.name_suffix t.name 78 - let alias t = Name.alias t.name 79 - let full_name t = Name.full_name t.name 60 + ;; 80 61 81 62 let email t = Cff_address.Contact.email t.contact 82 63 let orcid t = Cff_address.Contact.orcid t.contact ··· 85 66 let pp ppf t = 86 67 Format.fprintf ppf "%s" (full_name t); 87 68 Option.iter (Format.fprintf ppf " (%s)") t.affiliation 69 + ;; 88 70 89 71 let jsont = 90 - Jsont.Object.map ~kind:"Person" 91 - (fun family_names given_names name_particle name_suffix alias 92 - affiliation address city region post_code country 93 - email tel fax website orcid -> 94 - let name = Name.make ?family_names ?given_names ?name_particle 95 - ?name_suffix ?alias () in 96 - let address = Cff_address.Address.of_options 97 - ~address ~city ~region ~post_code ~country in 98 - let contact = Cff_address.Contact.of_options 99 - ~email ~tel ~fax ~website ~orcid in 100 - { name; affiliation; address; contact }) 101 - |> Jsont.Object.opt_mem "family-names" Jsont.string 102 - ~enc:(fun p -> Name.family_names p.name) 103 - |> Jsont.Object.opt_mem "given-names" Jsont.string 104 - ~enc:(fun p -> Name.given_names p.name) 105 - |> Jsont.Object.opt_mem "name-particle" Jsont.string 106 - ~enc:(fun p -> Name.name_particle p.name) 107 - |> Jsont.Object.opt_mem "name-suffix" Jsont.string 108 - ~enc:(fun p -> Name.name_suffix p.name) 109 - |> Jsont.Object.opt_mem "alias" Jsont.string 110 - ~enc:(fun p -> Name.alias p.name) 111 - |> Jsont.Object.opt_mem "affiliation" Jsont.string 112 - ~enc:(fun p -> p.affiliation) 72 + Jsont.Object.map 73 + ~kind:"Person" 74 + (fun 75 + family_names 76 + given_names 77 + name_particle 78 + name_suffix 79 + alias 80 + affiliation 81 + address 82 + city 83 + region 84 + post_code 85 + country 86 + email 87 + tel 88 + fax 89 + website 90 + orcid 91 + -> 92 + let address = 93 + Cff_address.Address.of_options ~address ~city ~region ~post_code ~country 94 + in 95 + let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in 96 + { family_names 97 + ; given_names 98 + ; name_particle 99 + ; name_suffix 100 + ; alias 101 + ; affiliation 102 + ; address 103 + ; contact 104 + }) 105 + |> Jsont.Object.opt_mem "family-names" Jsont.string ~enc:(fun p -> p.family_names) 106 + |> Jsont.Object.opt_mem "given-names" Jsont.string ~enc:(fun p -> p.given_names) 107 + |> Jsont.Object.opt_mem "name-particle" Jsont.string ~enc:(fun p -> p.name_particle) 108 + |> Jsont.Object.opt_mem "name-suffix" Jsont.string ~enc:(fun p -> p.name_suffix) 109 + |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun p -> p.alias) 110 + |> Jsont.Object.opt_mem "affiliation" Jsont.string ~enc:(fun p -> p.affiliation) 113 111 |> Cff_address.Address.jsont_fields ~get:(fun p -> p.address) 114 112 |> Cff_address.Contact.jsont_fields ~get:(fun p -> p.contact) 115 113 |> Jsont.Object.skip_unknown 116 114 |> Jsont.Object.finish 117 - end 118 - 119 - (** Event dates for entities (e.g., conferences). *) 120 - module Event_dates = struct 121 - type t = { 122 - date_start : Cff_date.t option; 123 - date_end : Cff_date.t option; 124 - } 125 - 126 - let empty = { date_start = None; date_end = None } 127 - 128 - let make ?date_start ?date_end () = { date_start; date_end } 129 - 130 - let date_start t = t.date_start 131 - let date_end t = t.date_end 132 - 133 - let is_empty t = t.date_start = None && t.date_end = None 134 - 135 - let pp ppf t = 136 - match t.date_start, t.date_end with 137 - | Some s, Some e -> 138 - Format.fprintf ppf "%a - %a" Cff_date.pp s Cff_date.pp e 139 - | Some s, None -> 140 - Format.fprintf ppf "%a -" Cff_date.pp s 141 - | None, Some e -> 142 - Format.fprintf ppf "- %a" Cff_date.pp e 143 - | None, None -> () 115 + ;; 144 116 end 145 117 146 118 (** An entity (organization, team, conference, etc.). *) 147 119 module Entity = struct 148 - type t = { 149 - name : string; 150 - alias : string option; 151 - address : Cff_address.Address.t; 152 - contact : Cff_address.Contact.t; 153 - event_dates : Event_dates.t; 154 - location : string option; 155 - } 120 + type t = 121 + { name : string 122 + ; alias : string option 123 + ; address : Cff_address.Address.t 124 + ; contact : Cff_address.Contact.t 125 + ; date_start : Cff_date.t option 126 + ; date_end : Cff_date.t option 127 + ; location : string option 128 + } 156 129 157 130 let make 158 - ~name ?alias 159 - ?(address = Cff_address.Address.empty) 160 - ?(contact = Cff_address.Contact.empty) 161 - ?date_start ?date_end ?location 162 - () = 163 - let event_dates = Event_dates.make ?date_start ?date_end () in 164 - { name; alias; address; contact; event_dates; location } 131 + ~name 132 + ?alias 133 + ?(address = Cff_address.Address.empty) 134 + ?(contact = Cff_address.Contact.empty) 135 + ?date_start 136 + ?date_end 137 + ?location 138 + () 139 + = 140 + { name; alias; address; contact; date_start; date_end; location } 141 + ;; 165 142 166 143 let name t = t.name 167 144 let alias t = t.alias 168 145 let address t = t.address 169 146 let contact t = t.contact 170 - let event_dates t = t.event_dates 147 + let date_start t = t.date_start 148 + let date_end t = t.date_end 171 149 let location t = t.location 172 - 173 150 let email t = Cff_address.Contact.email t.contact 174 151 let orcid t = Cff_address.Contact.orcid t.contact 175 152 let website t = Cff_address.Contact.website t.contact ··· 177 154 let pp ppf t = 178 155 Format.pp_print_string ppf t.name; 179 156 Option.iter (Format.fprintf ppf " (%s)") t.alias 157 + ;; 180 158 181 159 let jsont = 182 - Jsont.Object.map ~kind:"Entity" 183 - (fun name alias address city region post_code country 184 - email tel fax website orcid date_start date_end location -> 185 - let address = Cff_address.Address.of_options 186 - ~address ~city ~region ~post_code ~country in 187 - let contact = Cff_address.Contact.of_options 188 - ~email ~tel ~fax ~website ~orcid in 189 - let event_dates = Event_dates.make ?date_start ?date_end () in 190 - { name; alias; address; contact; event_dates; location }) 191 - |> Jsont.Object.mem "name" Jsont.string 192 - ~enc:(fun e -> e.name) 193 - |> Jsont.Object.opt_mem "alias" Jsont.string 194 - ~enc:(fun e -> e.alias) 160 + Jsont.Object.map 161 + ~kind:"Entity" 162 + (fun 163 + name 164 + alias 165 + address 166 + city 167 + region 168 + post_code 169 + country 170 + email 171 + tel 172 + fax 173 + website 174 + orcid 175 + date_start 176 + date_end 177 + location 178 + -> 179 + let address = 180 + Cff_address.Address.of_options ~address ~city ~region ~post_code ~country 181 + in 182 + let contact = Cff_address.Contact.of_options ~email ~tel ~fax ~website ~orcid in 183 + { name; alias; address; contact; date_start; date_end; location }) 184 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name) 185 + |> Jsont.Object.opt_mem "alias" Jsont.string ~enc:(fun e -> e.alias) 195 186 |> Cff_address.Address.jsont_fields ~get:(fun e -> e.address) 196 187 |> Cff_address.Contact.jsont_fields ~get:(fun e -> e.contact) 197 - |> Jsont.Object.opt_mem "date-start" Cff_date.jsont 198 - ~enc:(fun e -> Event_dates.date_start e.event_dates) 199 - |> Jsont.Object.opt_mem "date-end" Cff_date.jsont 200 - ~enc:(fun e -> Event_dates.date_end e.event_dates) 201 - |> Jsont.Object.opt_mem "location" Jsont.string 202 - ~enc:(fun e -> e.location) 188 + |> Jsont.Object.opt_mem "date-start" Cff_date.jsont ~enc:(fun e -> e.date_start) 189 + |> Jsont.Object.opt_mem "date-end" Cff_date.jsont ~enc:(fun e -> e.date_end) 190 + |> Jsont.Object.opt_mem "location" Jsont.string ~enc:(fun e -> e.location) 203 191 |> Jsont.Object.skip_unknown 204 192 |> Jsont.Object.finish 193 + ;; 205 194 end 206 195 207 196 (** An author can be either a Person or an Entity. *) 208 197 type t = 209 - | Person of Person.t 210 - | Entity of Entity.t 198 + [ `Person of Person.t 199 + | `Entity of Entity.t 200 + ] 211 201 212 - let person p = Person p 213 - let entity e = Entity e 202 + let person 203 + ?family_names 204 + ?given_names 205 + ?name_particle 206 + ?name_suffix 207 + ?alias 208 + ?affiliation 209 + ?address 210 + ?contact 211 + () 212 + = 213 + `Person 214 + (Person.make 215 + ?family_names 216 + ?given_names 217 + ?name_particle 218 + ?name_suffix 219 + ?alias 220 + ?affiliation 221 + ?address 222 + ?contact 223 + ()) 224 + ;; 225 + 226 + let entity ~name ?alias ?address ?contact ?date_start ?date_end ?location () = 227 + `Entity (Entity.make ~name ?alias ?address ?contact ?date_start ?date_end ?location ()) 228 + ;; 214 229 215 230 let name = function 216 - | Person p -> Person.full_name p 217 - | Entity e -> Entity.name e 231 + | `Person p -> Person.full_name p 232 + | `Entity e -> Entity.name e 233 + ;; 218 234 219 235 let orcid = function 220 - | Person p -> Person.orcid p 221 - | Entity e -> Entity.orcid e 236 + | `Person p -> Person.orcid p 237 + | `Entity e -> Entity.orcid e 238 + ;; 222 239 223 240 let email = function 224 - | Person p -> Person.email p 225 - | Entity e -> Entity.email e 241 + | `Person p -> Person.email p 242 + | `Entity e -> Entity.email e 243 + ;; 226 244 227 245 let pp ppf = function 228 - | Person p -> Person.pp ppf p 229 - | Entity e -> Entity.pp ppf e 246 + | `Person p -> Person.pp ppf p 247 + | `Entity e -> Entity.pp ppf e 248 + ;; 230 249 231 250 (* Jsont codec that discriminates based on "name" field presence. 232 251 If "name" is present -> Entity, otherwise -> Person *) ··· 237 256 | _ -> false 238 257 in 239 258 let dec_json j = 240 - if has_name_member j then 259 + if has_name_member j 260 + then ( 241 261 match Jsont.Json.decode' Entity.jsont j with 242 - | Ok e -> Entity e 243 - | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err) 244 - else 262 + | Ok e -> `Entity e 263 + | Error err -> 264 + Jsont.Error.msgf Jsont.Meta.none "Invalid entity: %s" (Jsont.Error.to_string err)) 265 + else ( 245 266 match Jsont.Json.decode' Person.jsont j with 246 - | Ok p -> Person p 247 - | Error err -> Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err) 267 + | Ok p -> `Person p 268 + | Error err -> 269 + Jsont.Error.msgf Jsont.Meta.none "Invalid person: %s" (Jsont.Error.to_string err)) 248 270 in 249 271 let enc_author = function 250 - | Person p -> 272 + | `Person p -> 251 273 (match Jsont.Json.encode' Person.jsont p with 252 274 | Ok j -> j 253 275 | Error _ -> assert false) 254 - | Entity e -> 276 + | `Entity e -> 255 277 (match Jsont.Json.encode' Entity.jsont e with 256 278 | Ok j -> j 257 279 | Error _ -> assert false) 258 280 in 259 281 Jsont.json |> Jsont.map ~dec:dec_json ~enc:enc_author 282 + ;;
+145 -242
lib/cff_author.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 16 16 of a [name] field: if present, the entry is an entity; otherwise, 17 17 it's a person. 18 18 19 - {1 Name Components} 20 - 21 - CFF follows academic citation conventions for person names: 22 - 23 - - {b family-names}: Last name/surname (e.g., ["Smith"], ["van Rossum"]) 24 - - {b given-names}: First name(s) (e.g., ["Jane"], ["Guido"]) 25 - - {b name-particle}: Connector before family name (e.g., ["von"], ["van"], ["de"]) 26 - - {b name-suffix}: Generational suffix (e.g., ["Jr."], ["III"]) 27 - - {b alias}: Nickname or pseudonym 28 - 29 - {1 Entity Types} 30 - 31 - Entities can represent various organizations: 32 - 33 - - Research institutions and universities 34 - - Companies and corporations 35 - - Government agencies 36 - - Open source projects and communities 37 - - Academic conferences (with date-start/date-end) 38 - - Standards bodies 39 - 40 - {1 Example} 19 + {1 Quick Example} 41 20 42 21 {[ 43 - (* A person author with contact info *) 44 - let contact = Cff.Address.Contact.make 45 - ~orcid:"https://orcid.org/0000-0001-2345-6789" () in 46 - let jane = Cff.Author.Person (Cff.Author.Person.make 22 + (* Create a person author *) 23 + let jane = Cff.Author.person 47 24 ~family_names:"Smith" 48 - ~given_names:"Jane A." 25 + ~given_names:"Jane" 49 26 ~affiliation:"MIT" 50 - ~contact 51 - ()) 52 - 53 - (* A person with name particle *) 54 - let guido = Cff.Author.Person (Cff.Author.Person.make 55 - ~family_names:"Rossum" 56 - ~given_names:"Guido" 57 - ~name_particle:"van" 58 - ()) 27 + () 59 28 60 - (* An organization entity *) 61 - let address = Cff.Address.Address.make 62 - ~city:"San Francisco" ~country:"US" () in 63 - let contact = Cff.Address.Contact.make 64 - ~website:"https://mozilla.org" () in 65 - let mozilla = Cff.Author.Entity (Cff.Author.Entity.make 29 + (* Create an entity author *) 30 + let mozilla = Cff.Author.entity 66 31 ~name:"Mozilla Foundation" 67 - ~address ~contact 68 - ()) 69 - 70 - (* A conference entity with dates *) 71 - let conf = Cff.Author.Entity (Cff.Author.Entity.make 72 - ~name:"ICSE 2024" 73 - ~date_start:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:14) 74 - ~date_end:(Cff.Date.of_ymd ~year:2024 ~month:4 ~day:20) 75 - ~location:"Lisbon, Portugal" 76 - ()) 77 - ]} 78 - 79 - {1 Name Components} *) 80 - 81 - (** Name components for persons. 82 - 83 - CFF name handling follows scholarly citation conventions to properly 84 - represent names from various cultures and naming traditions. *) 85 - module Name : sig 86 - type t 87 - 88 - val empty : t 89 - (** Empty name with all components as [None]. *) 90 - 91 - val make : 92 - ?family_names:string -> 93 - ?given_names:string -> 94 - ?name_particle:string -> 95 - ?name_suffix:string -> 96 - ?alias:string -> 97 - unit -> t 98 - (** Create a name with optional components. 99 - 100 - @param family_names Last name/surname 101 - @param given_names First name(s) 102 - @param name_particle Connector like ["von"], ["van"], ["de"] 103 - @param name_suffix Generational suffix like ["Jr."], ["III"] 104 - @param alias Nickname or pseudonym *) 105 - 106 - val family_names : t -> string option 107 - (** The person's family name (surname, last name). *) 108 - 109 - val given_names : t -> string option 110 - (** The person's given name(s) (first name, forenames). *) 111 - 112 - val name_particle : t -> string option 113 - (** Name connector appearing before family name. 32 + () 114 33 115 - Examples: ["von"] in "Ludwig von Beethoven", 116 - ["van"] in "Vincent van Gogh". *) 34 + (* Pattern match on authors *) 35 + let show_author = function 36 + | `Person p -> Cff.Author.Person.full_name p 37 + | `Entity e -> Cff.Author.Entity.name e 38 + ]} *) 117 39 118 - val name_suffix : t -> string option 119 - (** Generational or honorary suffix. 40 + (** {1 Person} 120 41 121 - Examples: ["Jr."], ["Sr."], ["III"], ["PhD"]. *) 122 - 123 - val alias : t -> string option 124 - (** Nickname, pseudonym, or alternative name. 125 - 126 - Example: ["Tim"] for "Timothy", ["DHH"] for "David Heinemeier Hansson". *) 127 - 128 - val full_name : t -> string 129 - (** Format name as "Given Particle Family, Suffix". 130 - 131 - Examples: 132 - - ["Jane Smith"] 133 - - ["Guido van Rossum"] 134 - - ["John Smith, Jr."] *) 135 - 136 - val pp : Format.formatter -> t -> unit 137 - (** Pretty-print the full name. *) 138 - end 139 - 140 - (** Individual person (author, contributor, editor, etc.). 42 + Individual person (author, contributor, editor, etc.). 141 43 142 44 A person represents a human contributor with: 143 - - Name components (required: at least family or given names) 45 + - Name components (family names, given names, particle, suffix, alias) 144 46 - Optional affiliation (institution, company) 145 47 - Optional physical address 146 48 - Optional contact information (email, ORCID, website) *) 147 49 module Person : sig 50 + (** A person record. *) 148 51 type t 149 52 150 - val make : 151 - ?family_names:string -> 152 - ?given_names:string -> 153 - ?name_particle:string -> 154 - ?name_suffix:string -> 155 - ?alias:string -> 156 - ?affiliation:string -> 157 - ?address:Cff_address.Address.t -> 158 - ?contact:Cff_address.Contact.t -> 159 - unit -> t 160 53 (** Create a person with optional fields. 161 54 162 55 At minimum, provide [family_names] or [given_names]. 163 56 164 57 @param family_names Last name/surname 165 58 @param given_names First name(s) 166 - @param name_particle Connector before family name 167 - @param name_suffix Generational suffix 59 + @param name_particle Connector before family name (e.g., "von", "van") 60 + @param name_suffix Generational suffix (e.g., "Jr.", "III") 168 61 @param alias Nickname or pseudonym 169 62 @param affiliation Institution or organization name 170 63 @param address Physical address 171 64 @param contact Contact information (email, ORCID, website, etc.) *) 172 - 173 - val name : t -> Name.t 174 - (** The person's name components. *) 175 - 176 - val affiliation : t -> string option 177 - (** The person's institutional affiliation. 178 - 179 - Example: ["Massachusetts Institute of Technology"]. *) 180 - 181 - val address : t -> Cff_address.Address.t 182 - (** Physical address information. *) 183 - 184 - val contact : t -> Cff_address.Contact.t 185 - (** Contact information (email, phone, web, ORCID). *) 65 + val make 66 + : ?family_names:string 67 + -> ?given_names:string 68 + -> ?name_particle:string 69 + -> ?name_suffix:string 70 + -> ?alias:string 71 + -> ?affiliation:string 72 + -> ?address:Cff_address.Address.t 73 + -> ?contact:Cff_address.Contact.t 74 + -> unit 75 + -> t 186 76 187 - (** {2 Convenience Accessors for Name} *) 77 + (** {2 Name Fields} *) 188 78 79 + (** The person's family name (surname, last name). *) 189 80 val family_names : t -> string option 190 - (** Shortcut for [Name.family_names (name t)]. *) 191 81 82 + (** The person's given name(s) (first name, forenames). *) 192 83 val given_names : t -> string option 193 - (** Shortcut for [Name.given_names (name t)]. *) 194 84 85 + (** Name connector appearing before family name. 86 + Examples: ["von"] in "Ludwig von Beethoven". *) 195 87 val name_particle : t -> string option 196 - (** Shortcut for [Name.name_particle (name t)]. *) 197 88 89 + (** Generational or honorary suffix. 90 + Examples: ["Jr."], ["Sr."], ["III"]. *) 198 91 val name_suffix : t -> string option 199 - (** Shortcut for [Name.name_suffix (name t)]. *) 200 92 93 + (** Nickname, pseudonym, or alternative name. *) 201 94 val alias : t -> string option 202 - (** Shortcut for [Name.alias (name t)]. *) 203 95 96 + (** Format name as "Given Particle Family, Suffix". 97 + Examples: ["Jane Smith"], ["Guido van Rossum"]. *) 204 98 val full_name : t -> string 205 - (** Shortcut for [Name.full_name (name t)]. *) 99 + 100 + (** {2 Affiliation and Location} *) 101 + 102 + (** The person's institutional affiliation. *) 103 + val affiliation : t -> string option 104 + 105 + (** Physical address information. *) 106 + val address : t -> Cff_address.Address.t 107 + 108 + (** {2 Contact Information} *) 206 109 207 - (** {2 Convenience Accessors for Contact} *) 110 + (** Full contact information record. *) 111 + val contact : t -> Cff_address.Contact.t 208 112 113 + (** The person's email address. *) 209 114 val email : t -> string option 210 - (** The person's email address. *) 211 115 116 + (** The person's ORCID identifier URL. *) 212 117 val orcid : t -> string option 213 - (** The person's ORCID identifier URL. 214 118 215 - ORCID (Open Researcher and Contributor ID) provides persistent 216 - digital identifiers for researchers. Format: ["https://orcid.org/XXXX-XXXX-XXXX-XXXX"]. *) 217 - 119 + (** The person's website URL. *) 218 120 val website : t -> string option 219 - (** The person's website URL. *) 220 121 221 - val pp : Format.formatter -> t -> unit 122 + (** {2 Formatting and Codec} *) 123 + 222 124 (** Pretty-print as "Full Name (affiliation)". *) 125 + val pp : Format.formatter -> t -> unit 223 126 224 - val jsont : t Jsont.t 225 127 (** JSON/YAML codec for person records. *) 128 + val jsont : t Jsont.t 226 129 end 227 130 228 - (** Event date range for entities like conferences. 131 + (** {1 Entity} 229 132 230 - Some entities (particularly conferences) have associated dates 231 - when they take place. *) 232 - module Event_dates : sig 233 - type t 234 - 235 - val empty : t 236 - (** Empty date range with both dates as [None]. *) 237 - 238 - val make : 239 - ?date_start:Cff_date.t -> 240 - ?date_end:Cff_date.t -> 241 - unit -> t 242 - (** Create an event date range. 243 - 244 - @param date_start When the event begins 245 - @param date_end When the event ends *) 246 - 247 - val date_start : t -> Cff_date.t option 248 - (** The start date of the event. *) 249 - 250 - val date_end : t -> Cff_date.t option 251 - (** The end date of the event. *) 252 - 253 - val is_empty : t -> bool 254 - (** [true] if both dates are [None]. *) 255 - 256 - val pp : Format.formatter -> t -> unit 257 - (** Pretty-print as "YYYY-MM-DD - YYYY-MM-DD". *) 258 - end 259 - 260 - (** Organization, institution, project, or conference. 133 + Organization, institution, project, or conference. 261 134 262 135 An entity represents a non-person author or contributor, such as: 263 136 - Research institutions (["MIT"], ["CERN"]) 264 137 - Companies (["Google"], ["Mozilla Foundation"]) 265 - - Government agencies (["NASA"], ["NIH"]) 266 138 - Open source projects (["The Rust Project"]) 267 - - Academic conferences (["ICSE 2024"]) 268 - - Standards bodies (["IEEE"], ["W3C"]) 139 + - Academic conferences (["ICSE 2024"]) with dates 269 140 270 141 Entities are distinguished from persons in YAML by the presence 271 - of a required [name] field (persons have [family-names]/[given-names] 272 - instead). *) 142 + of a required [name] field. *) 273 143 module Entity : sig 144 + (** An entity record. *) 274 145 type t 275 146 276 - val make : 277 - name:string -> 278 - ?alias:string -> 279 - ?address:Cff_address.Address.t -> 280 - ?contact:Cff_address.Contact.t -> 281 - ?date_start:Cff_date.t -> 282 - ?date_end:Cff_date.t -> 283 - ?location:string -> 284 - unit -> t 285 147 (** Create an entity. 286 148 287 149 @param name The entity's official name (required) ··· 291 153 @param date_start Event start date (for conferences) 292 154 @param date_end Event end date (for conferences) 293 155 @param location Event location description *) 156 + val make 157 + : name:string 158 + -> ?alias:string 159 + -> ?address:Cff_address.Address.t 160 + -> ?contact:Cff_address.Contact.t 161 + -> ?date_start:Cff_date.t 162 + -> ?date_end:Cff_date.t 163 + -> ?location:string 164 + -> unit 165 + -> t 294 166 167 + (** {2 Core Fields} *) 168 + 169 + (** The entity's official name. *) 295 170 val name : t -> string 296 - (** The entity's official name. This field distinguishes entities 297 - from persons in the YAML format. *) 298 171 172 + (** Short name, acronym, or alternative name. *) 299 173 val alias : t -> string option 300 - (** Short name, acronym, or alternative name. 301 174 302 - Example: ["MIT"] for "Massachusetts Institute of Technology". *) 175 + (** {2 Location} *) 303 176 177 + (** Physical address information. *) 304 178 val address : t -> Cff_address.Address.t 305 - (** Physical address information. *) 179 + 180 + (** Event location description (for conferences). *) 181 + val location : t -> string option 306 182 307 - val contact : t -> Cff_address.Contact.t 308 - (** Contact information. *) 183 + (** {2 Event Dates} *) 309 184 310 - val event_dates : t -> Event_dates.t 311 - (** Event dates (for conferences). *) 185 + (** The start date of the event (for conferences). *) 186 + val date_start : t -> Cff_date.t option 312 187 313 - val location : t -> string option 314 - (** Event location description (for conferences). 188 + (** The end date of the event (for conferences). *) 189 + val date_end : t -> Cff_date.t option 315 190 316 - Example: ["Lisbon, Portugal"]. *) 191 + (** {2 Contact Information} *) 317 192 318 - (** {2 Convenience Accessors for Contact} *) 193 + (** Full contact information record. *) 194 + val contact : t -> Cff_address.Contact.t 319 195 320 - val email : t -> string option 321 196 (** The entity's contact email. *) 197 + val email : t -> string option 322 198 199 + (** The entity's ORCID (organizations can have ORCIDs). *) 323 200 val orcid : t -> string option 324 - (** The entity's ORCID (organizations can have ORCIDs). *) 325 201 326 - val website : t -> string option 327 202 (** The entity's official website URL. *) 203 + val website : t -> string option 328 204 329 - val pp : Format.formatter -> t -> unit 205 + (** {2 Formatting and Codec} *) 206 + 330 207 (** Pretty-print as "Name (alias)". *) 208 + val pp : Format.formatter -> t -> unit 331 209 332 - val jsont : t Jsont.t 333 210 (** JSON/YAML codec for entity records. *) 211 + val jsont : t Jsont.t 334 212 end 335 213 336 - (** {1 Author Discriminated Union} 214 + (** {1 Author Type} 337 215 338 - The main author type is a sum type that can hold either a person 339 - or an entity. This matches the CFF specification where authors 340 - can be either individuals or organizations. *) 216 + The main author type is a polymorphic variant that can hold either 217 + a person or an entity. *) 341 218 342 - type t = 343 - | Person of Person.t (** An individual person *) 344 - | Entity of Entity.t (** An organization or entity *) 345 219 (** An author: either a person or an entity. *) 220 + type t = 221 + [ `Person of Person.t 222 + | `Entity of Entity.t 223 + ] 346 224 347 - val person : Person.t -> t 348 - (** Wrap a person as an author. *) 225 + (** {2 Smart Constructors} *) 349 226 350 - val entity : Entity.t -> t 351 - (** Wrap an entity as an author. *) 227 + (** Create a person author directly. 352 228 353 - val name : t -> string 229 + Equivalent to [`Person (Person.make ...)]. *) 230 + val person 231 + : ?family_names:string 232 + -> ?given_names:string 233 + -> ?name_particle:string 234 + -> ?name_suffix:string 235 + -> ?alias:string 236 + -> ?affiliation:string 237 + -> ?address:Cff_address.Address.t 238 + -> ?contact:Cff_address.Contact.t 239 + -> unit 240 + -> t 241 + 242 + (** Create an entity author directly. 243 + 244 + Equivalent to [`Entity (Entity.make ...)]. *) 245 + val entity 246 + : name:string 247 + -> ?alias:string 248 + -> ?address:Cff_address.Address.t 249 + -> ?contact:Cff_address.Contact.t 250 + -> ?date_start:Cff_date.t 251 + -> ?date_end:Cff_date.t 252 + -> ?location:string 253 + -> unit 254 + -> t 255 + 256 + (** {2 Common Accessors} *) 257 + 354 258 (** Get the display name. 355 259 356 260 For persons, returns the full formatted name. 357 261 For entities, returns the entity name. *) 262 + val name : t -> string 358 263 359 - val orcid : t -> string option 360 264 (** Get the ORCID if present. Works for both persons and entities. *) 265 + val orcid : t -> string option 361 266 267 + (** Get the email if present. Works for both persons and entities. *) 362 268 val email : t -> string option 363 - (** Get the email if present. Works for both persons and entities. *) 364 269 365 - val pp : Format.formatter -> t -> unit 270 + (** {2 Formatting and Codec} *) 271 + 366 272 (** Pretty-print the author. *) 273 + val pp : Format.formatter -> t -> unit 367 274 368 - val jsont : t Jsont.t 369 275 (** JSON/YAML codec that discriminates based on [name] field presence. 370 276 371 277 When decoding: 372 278 - If the object has a [name] field -> Entity 373 - - Otherwise -> Person 374 - 375 - This matches the CFF specification where entities are distinguished 376 - by having a [name] field while persons have [family-names] and 377 - [given-names] fields. *) 279 + - Otherwise -> Person *) 280 + val jsont : t Jsont.t
+10 -13
lib/cff_country.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 13 13 try 14 14 let _ = ISO3166.alpha2_of_string s in 15 15 Ok s 16 - with Invalid_argument _ -> 17 - Error (`Invalid_country s) 16 + with 17 + | Invalid_argument _ -> Error (`Invalid_country s) 18 + ;; 18 19 19 20 let to_string t = t 20 21 21 22 let to_iso3166 t = 22 - try 23 - Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t)) 24 - with Invalid_argument _ -> 25 - None 23 + try Some (ISO3166.alpha2_to_country (ISO3166.alpha2_of_string t)) with 24 + | Invalid_argument _ -> None 25 + ;; 26 26 27 27 let name t = Option.map ISO3166.Country.name (to_iso3166 t) 28 - 29 28 let equal = String.equal 30 29 let compare = String.compare 31 - 32 - let pp ppf t = 33 - Format.pp_print_string ppf t 30 + let pp ppf t = Format.pp_print_string ppf t 34 31 35 32 (* Jsont codec for country codes *) 36 33 let jsont = ··· 41 38 Jsont.Error.msgf Jsont.Meta.none "Invalid ISO 3166-1 alpha-2 country code: %s" s 42 39 in 43 40 let enc t = to_string t in 44 - Jsont.string 45 - |> Jsont.map ~dec ~enc 41 + Jsont.string |> Jsont.map ~dec ~enc 42 + ;; 46 43 47 44 (* Lenient codec that accepts any string *) 48 45 let jsont_lenient = Jsont.string
+11 -11
lib/cff_country.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 39 39 country: DE 40 40 ]} *) 41 41 42 - type t = string 43 42 (** An ISO 3166-1 alpha-2 country code (two uppercase letters). *) 43 + type t = string 44 44 45 - val of_string : string -> (t, [> `Invalid_country of string]) result 46 45 (** Parse and validate a country code. 47 46 48 47 Case-insensitive: ["us"], ["US"], and ["Us"] all produce ["US"]. 49 48 Returns [Error (`Invalid_country s)] for unknown codes. *) 49 + val of_string : string -> (t, [> `Invalid_country of string ]) result 50 50 51 + (** Return the uppercase country code. *) 51 52 val to_string : t -> string 52 - (** Return the uppercase country code. *) 53 53 54 - val to_iso3166 : t -> ISO3166.Country.t option 55 54 (** Look up the full country record from {!ISO3166}. 56 55 57 56 Returns [None] if the code is not in the ISO 3166-1 list. *) 57 + val to_iso3166 : t -> ISO3166.Country.t option 58 58 59 - val name : t -> string option 60 59 (** Get the country name if the code is valid. 61 60 62 61 Examples: 63 62 - [name "US" = Some "United States of America"] 64 63 - [name "GB" = Some "United Kingdom of Great Britain and Northern Ireland"] 65 64 - [name "XX" = None] *) 65 + val name : t -> string option 66 66 67 + (** Country code equality (case-sensitive after normalization). *) 67 68 val equal : t -> t -> bool 68 - (** Country code equality (case-sensitive after normalization). *) 69 69 70 - val compare : t -> t -> int 71 70 (** Alphabetical comparison of country codes. *) 71 + val compare : t -> t -> int 72 72 73 + (** Pretty-print the country code. *) 73 74 val pp : Format.formatter -> t -> unit 74 - (** Pretty-print the country code. *) 75 75 76 - val jsont : t Jsont.t 77 76 (** JSON/YAML codec that validates country codes. 78 77 79 78 Returns an error for invalid ISO 3166-1 alpha-2 codes. *) 79 + val jsont : t Jsont.t 80 80 81 - val jsont_lenient : t Jsont.t 82 81 (** JSON/YAML codec that accepts any string. 83 82 84 83 Use this when parsing CFF files that may contain non-standard 85 84 country codes. *) 85 + val jsont_lenient : t Jsont.t
+10 -17
lib/cff_date.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 10 10 let of_string s = 11 11 (* CFF dates are YYYY-MM-DD format *) 12 12 match String.split_on_char '-' s with 13 - | [y; m; d] -> 13 + | [ y; m; d ] -> 14 14 (match int_of_string_opt y, int_of_string_opt m, int_of_string_opt d with 15 15 | Some year, Some month, Some day -> 16 16 (* Validate the date components *) 17 - if year >= 0 && year <= 9999 && 18 - month >= 1 && month <= 12 && 19 - day >= 1 && day <= 31 then 20 - Ok (year, month, day) 21 - else 22 - Error (`Invalid_date s) 17 + if year >= 0 && year <= 9999 && month >= 1 && month <= 12 && day >= 1 && day <= 31 18 + then Ok (year, month, day) 19 + else Error (`Invalid_date s) 23 20 | _ -> Error (`Invalid_date s)) 24 21 | _ -> Error (`Invalid_date s) 22 + ;; 25 23 26 - let to_string (year, month, day) = 27 - Printf.sprintf "%04d-%02d-%02d" year month day 28 - 24 + let to_string (year, month, day) = Printf.sprintf "%04d-%02d-%02d" year month day 29 25 let year (y, _, _) = y 30 26 let month (_, m, _) = m 31 27 let day (_, _, d) = d 32 - 33 28 let equal a b = a = b 34 29 let compare = Stdlib.compare 35 - 36 - let pp ppf date = 37 - Format.pp_print_string ppf (to_string date) 30 + let pp ppf date = Format.pp_print_string ppf (to_string date) 38 31 39 32 (* Jsont codec for dates *) 40 33 let jsont = ··· 45 38 Jsont.Error.msgf Jsont.Meta.none "Invalid date format: %s" s 46 39 in 47 40 let enc date = to_string date in 48 - Jsont.string 49 - |> Jsont.map ~dec ~enc 41 + Jsont.string |> Jsont.map ~dec ~enc 42 + ;;
+11 -11
lib/cff_date.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 45 45 For historical works or when only the year is known, use the [year] 46 46 field (an integer) instead of a full date. *) 47 47 48 - type t = Ptime.date 49 48 (** A date as [(year, month, day)] tuple. 50 49 51 50 The tuple contains: 52 51 - [year]: Four-digit year (e.g., [2024]) 53 52 - [month]: Month number (1-12) 54 53 - [day]: Day of month (1-31) *) 54 + type t = Ptime.date 55 55 56 - val of_string : string -> (t, [> `Invalid_date of string]) result 57 56 (** Parse a date from [YYYY-MM-DD] format. 58 57 59 58 Returns [Error (`Invalid_date s)] if the string is not a valid date. 60 59 Validates that the date is a real calendar date (e.g., rejects Feb 30). *) 60 + val of_string : string -> (t, [> `Invalid_date of string ]) result 61 61 62 - val to_string : t -> string 63 62 (** Format a date as [YYYY-MM-DD]. *) 63 + val to_string : t -> string 64 64 65 + (** Extract the year component. *) 65 66 val year : t -> int 66 - (** Extract the year component. *) 67 67 68 - val month : t -> int 69 68 (** Extract the month component (1-12). *) 69 + val month : t -> int 70 70 71 - val day : t -> int 72 71 (** Extract the day component (1-31). *) 72 + val day : t -> int 73 73 74 + (** Date equality. *) 74 75 val equal : t -> t -> bool 75 - (** Date equality. *) 76 76 77 - val compare : t -> t -> int 78 77 (** Date comparison (chronological order). *) 78 + val compare : t -> t -> int 79 79 80 + (** Pretty-print a date in [YYYY-MM-DD] format. *) 80 81 val pp : Format.formatter -> t -> unit 81 - (** Pretty-print a date in [YYYY-MM-DD] format. *) 82 82 83 - val jsont : t Jsont.t 84 83 (** JSON/YAML codec for dates. 85 84 86 85 Parses strings in [YYYY-MM-DD] format and serializes back to the 87 86 same format. *) 87 + val jsont : t Jsont.t
+229 -204
lib/cff_enums.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 8 8 (** Functor to generate common enum operations. *) 9 9 module type STRING_ENUM = sig 10 10 type t 11 + 11 12 val of_string : string -> t option 12 13 val to_string : t -> string 13 14 val type_name : string ··· 15 16 16 17 module Make_enum (E : STRING_ENUM) = struct 17 18 include E 19 + 18 20 let equal (a : t) (b : t) = a = b 19 21 let compare = Stdlib.compare 20 22 let pp ppf t = Format.pp_print_string ppf (to_string t) 23 + 21 24 let jsont = 22 - Jsont.string |> Jsont.map 23 - ~dec:(fun s -> 24 - match of_string s with 25 - | Some t -> t 26 - | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s) 27 - ~enc:to_string 25 + Jsont.string 26 + |> Jsont.map 27 + ~dec:(fun s -> 28 + match of_string s with 29 + | Some t -> t 30 + | None -> Jsont.Error.msgf Jsont.Meta.none "Invalid %s: %s" type_name s) 31 + ~enc:to_string 32 + ;; 28 33 end 29 34 30 35 module Identifier_type = Make_enum (struct 31 - type t = [ `Doi | `Url | `Swh | `Other ] 32 - let type_name = "identifier type" 36 + type t = 37 + [ `Doi 38 + | `Url 39 + | `Swh 40 + | `Other 41 + ] 42 + 43 + let type_name = "identifier type" 33 44 34 - let of_string = function 35 - | "doi" -> Some `Doi 36 - | "url" -> Some `Url 37 - | "swh" -> Some `Swh 38 - | "other" -> Some `Other 39 - | _ -> None 45 + let of_string = function 46 + | "doi" -> Some `Doi 47 + | "url" -> Some `Url 48 + | "swh" -> Some `Swh 49 + | "other" -> Some `Other 50 + | _ -> None 51 + ;; 40 52 41 - let to_string = function 42 - | `Doi -> "doi" 43 - | `Url -> "url" 44 - | `Swh -> "swh" 45 - | `Other -> "other" 46 - end) 53 + let to_string = function 54 + | `Doi -> "doi" 55 + | `Url -> "url" 56 + | `Swh -> "swh" 57 + | `Other -> "other" 58 + ;; 59 + end) 47 60 48 61 module Reference_type = Make_enum (struct 49 - type t = [ 50 - | `Art 51 - | `Article 52 - | `Audiovisual 53 - | `Bill 54 - | `Blog 55 - | `Book 56 - | `Catalogue 57 - | `Conference 58 - | `Conference_paper 59 - | `Data 60 - | `Database 61 - | `Dictionary 62 - | `Edited_work 63 - | `Encyclopedia 64 - | `Film_broadcast 65 - | `Generic 66 - | `Government_document 67 - | `Grant 68 - | `Hearing 69 - | `Historical_work 70 - | `Legal_case 71 - | `Legal_rule 72 - | `Magazine_article 73 - | `Manual 74 - | `Map 75 - | `Multimedia 76 - | `Music 77 - | `Newspaper_article 78 - | `Pamphlet 79 - | `Patent 80 - | `Personal_communication 81 - | `Proceedings 82 - | `Report 83 - | `Serial 84 - | `Slides 85 - | `Software 86 - | `Software_code 87 - | `Software_container 88 - | `Software_executable 89 - | `Software_virtual_machine 90 - | `Sound_recording 91 - | `Standard 92 - | `Statute 93 - | `Thesis 94 - | `Unpublished 95 - | `Video 96 - | `Website 97 - ] 98 - let type_name = "reference type" 62 + type t = 63 + [ `Art 64 + | `Article 65 + | `Audiovisual 66 + | `Bill 67 + | `Blog 68 + | `Book 69 + | `Catalogue 70 + | `Conference 71 + | `Conference_paper 72 + | `Data 73 + | `Database 74 + | `Dictionary 75 + | `Edited_work 76 + | `Encyclopedia 77 + | `Film_broadcast 78 + | `Generic 79 + | `Government_document 80 + | `Grant 81 + | `Hearing 82 + | `Historical_work 83 + | `Legal_case 84 + | `Legal_rule 85 + | `Magazine_article 86 + | `Manual 87 + | `Map 88 + | `Multimedia 89 + | `Music 90 + | `Newspaper_article 91 + | `Pamphlet 92 + | `Patent 93 + | `Personal_communication 94 + | `Proceedings 95 + | `Report 96 + | `Serial 97 + | `Slides 98 + | `Software 99 + | `Software_code 100 + | `Software_container 101 + | `Software_executable 102 + | `Software_virtual_machine 103 + | `Sound_recording 104 + | `Standard 105 + | `Statute 106 + | `Thesis 107 + | `Unpublished 108 + | `Video 109 + | `Website 110 + ] 111 + 112 + let type_name = "reference type" 99 113 100 - let of_string = function 101 - | "art" -> Some `Art 102 - | "article" -> Some `Article 103 - | "audiovisual" -> Some `Audiovisual 104 - | "bill" -> Some `Bill 105 - | "blog" -> Some `Blog 106 - | "book" -> Some `Book 107 - | "catalogue" -> Some `Catalogue 108 - | "conference" -> Some `Conference 109 - | "conference-paper" -> Some `Conference_paper 110 - | "data" -> Some `Data 111 - | "database" -> Some `Database 112 - | "dictionary" -> Some `Dictionary 113 - | "edited-work" -> Some `Edited_work 114 - | "encyclopedia" -> Some `Encyclopedia 115 - | "film-broadcast" -> Some `Film_broadcast 116 - | "generic" -> Some `Generic 117 - | "government-document" -> Some `Government_document 118 - | "grant" -> Some `Grant 119 - | "hearing" -> Some `Hearing 120 - | "historical-work" -> Some `Historical_work 121 - | "legal-case" -> Some `Legal_case 122 - | "legal-rule" -> Some `Legal_rule 123 - | "magazine-article" -> Some `Magazine_article 124 - | "manual" -> Some `Manual 125 - | "map" -> Some `Map 126 - | "multimedia" -> Some `Multimedia 127 - | "music" -> Some `Music 128 - | "newspaper-article" -> Some `Newspaper_article 129 - | "pamphlet" -> Some `Pamphlet 130 - | "patent" -> Some `Patent 131 - | "personal-communication" -> Some `Personal_communication 132 - | "proceedings" -> Some `Proceedings 133 - | "report" -> Some `Report 134 - | "serial" -> Some `Serial 135 - | "slides" -> Some `Slides 136 - | "software" -> Some `Software 137 - | "software-code" -> Some `Software_code 138 - | "software-container" -> Some `Software_container 139 - | "software-executable" -> Some `Software_executable 140 - | "software-virtual-machine" -> Some `Software_virtual_machine 141 - | "sound-recording" -> Some `Sound_recording 142 - | "standard" -> Some `Standard 143 - | "statute" -> Some `Statute 144 - | "thesis" -> Some `Thesis 145 - | "unpublished" -> Some `Unpublished 146 - | "video" -> Some `Video 147 - | "website" -> Some `Website 148 - | _ -> None 114 + let of_string = function 115 + | "art" -> Some `Art 116 + | "article" -> Some `Article 117 + | "audiovisual" -> Some `Audiovisual 118 + | "bill" -> Some `Bill 119 + | "blog" -> Some `Blog 120 + | "book" -> Some `Book 121 + | "catalogue" -> Some `Catalogue 122 + | "conference" -> Some `Conference 123 + | "conference-paper" -> Some `Conference_paper 124 + | "data" -> Some `Data 125 + | "database" -> Some `Database 126 + | "dictionary" -> Some `Dictionary 127 + | "edited-work" -> Some `Edited_work 128 + | "encyclopedia" -> Some `Encyclopedia 129 + | "film-broadcast" -> Some `Film_broadcast 130 + | "generic" -> Some `Generic 131 + | "government-document" -> Some `Government_document 132 + | "grant" -> Some `Grant 133 + | "hearing" -> Some `Hearing 134 + | "historical-work" -> Some `Historical_work 135 + | "legal-case" -> Some `Legal_case 136 + | "legal-rule" -> Some `Legal_rule 137 + | "magazine-article" -> Some `Magazine_article 138 + | "manual" -> Some `Manual 139 + | "map" -> Some `Map 140 + | "multimedia" -> Some `Multimedia 141 + | "music" -> Some `Music 142 + | "newspaper-article" -> Some `Newspaper_article 143 + | "pamphlet" -> Some `Pamphlet 144 + | "patent" -> Some `Patent 145 + | "personal-communication" -> Some `Personal_communication 146 + | "proceedings" -> Some `Proceedings 147 + | "report" -> Some `Report 148 + | "serial" -> Some `Serial 149 + | "slides" -> Some `Slides 150 + | "software" -> Some `Software 151 + | "software-code" -> Some `Software_code 152 + | "software-container" -> Some `Software_container 153 + | "software-executable" -> Some `Software_executable 154 + | "software-virtual-machine" -> Some `Software_virtual_machine 155 + | "sound-recording" -> Some `Sound_recording 156 + | "standard" -> Some `Standard 157 + | "statute" -> Some `Statute 158 + | "thesis" -> Some `Thesis 159 + | "unpublished" -> Some `Unpublished 160 + | "video" -> Some `Video 161 + | "website" -> Some `Website 162 + | _ -> None 163 + ;; 149 164 150 - let to_string = function 151 - | `Art -> "art" 152 - | `Article -> "article" 153 - | `Audiovisual -> "audiovisual" 154 - | `Bill -> "bill" 155 - | `Blog -> "blog" 156 - | `Book -> "book" 157 - | `Catalogue -> "catalogue" 158 - | `Conference -> "conference" 159 - | `Conference_paper -> "conference-paper" 160 - | `Data -> "data" 161 - | `Database -> "database" 162 - | `Dictionary -> "dictionary" 163 - | `Edited_work -> "edited-work" 164 - | `Encyclopedia -> "encyclopedia" 165 - | `Film_broadcast -> "film-broadcast" 166 - | `Generic -> "generic" 167 - | `Government_document -> "government-document" 168 - | `Grant -> "grant" 169 - | `Hearing -> "hearing" 170 - | `Historical_work -> "historical-work" 171 - | `Legal_case -> "legal-case" 172 - | `Legal_rule -> "legal-rule" 173 - | `Magazine_article -> "magazine-article" 174 - | `Manual -> "manual" 175 - | `Map -> "map" 176 - | `Multimedia -> "multimedia" 177 - | `Music -> "music" 178 - | `Newspaper_article -> "newspaper-article" 179 - | `Pamphlet -> "pamphlet" 180 - | `Patent -> "patent" 181 - | `Personal_communication -> "personal-communication" 182 - | `Proceedings -> "proceedings" 183 - | `Report -> "report" 184 - | `Serial -> "serial" 185 - | `Slides -> "slides" 186 - | `Software -> "software" 187 - | `Software_code -> "software-code" 188 - | `Software_container -> "software-container" 189 - | `Software_executable -> "software-executable" 190 - | `Software_virtual_machine -> "software-virtual-machine" 191 - | `Sound_recording -> "sound-recording" 192 - | `Standard -> "standard" 193 - | `Statute -> "statute" 194 - | `Thesis -> "thesis" 195 - | `Unpublished -> "unpublished" 196 - | `Video -> "video" 197 - | `Website -> "website" 198 - end) 165 + let to_string = function 166 + | `Art -> "art" 167 + | `Article -> "article" 168 + | `Audiovisual -> "audiovisual" 169 + | `Bill -> "bill" 170 + | `Blog -> "blog" 171 + | `Book -> "book" 172 + | `Catalogue -> "catalogue" 173 + | `Conference -> "conference" 174 + | `Conference_paper -> "conference-paper" 175 + | `Data -> "data" 176 + | `Database -> "database" 177 + | `Dictionary -> "dictionary" 178 + | `Edited_work -> "edited-work" 179 + | `Encyclopedia -> "encyclopedia" 180 + | `Film_broadcast -> "film-broadcast" 181 + | `Generic -> "generic" 182 + | `Government_document -> "government-document" 183 + | `Grant -> "grant" 184 + | `Hearing -> "hearing" 185 + | `Historical_work -> "historical-work" 186 + | `Legal_case -> "legal-case" 187 + | `Legal_rule -> "legal-rule" 188 + | `Magazine_article -> "magazine-article" 189 + | `Manual -> "manual" 190 + | `Map -> "map" 191 + | `Multimedia -> "multimedia" 192 + | `Music -> "music" 193 + | `Newspaper_article -> "newspaper-article" 194 + | `Pamphlet -> "pamphlet" 195 + | `Patent -> "patent" 196 + | `Personal_communication -> "personal-communication" 197 + | `Proceedings -> "proceedings" 198 + | `Report -> "report" 199 + | `Serial -> "serial" 200 + | `Slides -> "slides" 201 + | `Software -> "software" 202 + | `Software_code -> "software-code" 203 + | `Software_container -> "software-container" 204 + | `Software_executable -> "software-executable" 205 + | `Software_virtual_machine -> "software-virtual-machine" 206 + | `Sound_recording -> "sound-recording" 207 + | `Standard -> "standard" 208 + | `Statute -> "statute" 209 + | `Thesis -> "thesis" 210 + | `Unpublished -> "unpublished" 211 + | `Video -> "video" 212 + | `Website -> "website" 213 + ;; 214 + end) 199 215 200 216 module Status = Make_enum (struct 201 - type t = [ 202 - | `Abstract 203 - | `Advance_online 204 - | `In_preparation 205 - | `In_press 206 - | `Preprint 207 - | `Submitted 208 - ] 209 - let type_name = "status" 217 + type t = 218 + [ `Abstract 219 + | `Advance_online 220 + | `In_preparation 221 + | `In_press 222 + | `Preprint 223 + | `Submitted 224 + ] 210 225 211 - let of_string = function 212 - | "abstract" -> Some `Abstract 213 - | "advance-online" -> Some `Advance_online 214 - | "in-preparation" -> Some `In_preparation 215 - | "in-press" -> Some `In_press 216 - | "preprint" -> Some `Preprint 217 - | "submitted" -> Some `Submitted 218 - | _ -> None 226 + let type_name = "status" 219 227 220 - let to_string = function 221 - | `Abstract -> "abstract" 222 - | `Advance_online -> "advance-online" 223 - | `In_preparation -> "in-preparation" 224 - | `In_press -> "in-press" 225 - | `Preprint -> "preprint" 226 - | `Submitted -> "submitted" 227 - end) 228 + let of_string = function 229 + | "abstract" -> Some `Abstract 230 + | "advance-online" -> Some `Advance_online 231 + | "in-preparation" -> Some `In_preparation 232 + | "in-press" -> Some `In_press 233 + | "preprint" -> Some `Preprint 234 + | "submitted" -> Some `Submitted 235 + | _ -> None 236 + ;; 237 + 238 + let to_string = function 239 + | `Abstract -> "abstract" 240 + | `Advance_online -> "advance-online" 241 + | `In_preparation -> "in-preparation" 242 + | `In_press -> "in-press" 243 + | `Preprint -> "preprint" 244 + | `Submitted -> "submitted" 245 + ;; 246 + end) 228 247 229 248 module Cff_type = Make_enum (struct 230 - type t = [ `Software | `Dataset ] 231 - let type_name = "CFF type" 249 + type t = 250 + [ `Software 251 + | `Dataset 252 + ] 232 253 233 - let of_string = function 234 - | "software" -> Some `Software 235 - | "dataset" -> Some `Dataset 236 - | _ -> None 254 + let type_name = "CFF type" 255 + 256 + let of_string = function 257 + | "software" -> Some `Software 258 + | "dataset" -> Some `Dataset 259 + | _ -> None 260 + ;; 237 261 238 - let to_string = function 239 - | `Software -> "software" 240 - | `Dataset -> "dataset" 241 - end) 262 + let to_string = function 263 + | `Software -> "software" 264 + | `Dataset -> "dataset" 265 + ;; 266 + end)
+31 -23
lib/cff_enums.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 48 48 description: Software Heritage archive 49 49 ]} *) 50 50 module Identifier_type : sig 51 - type t = [ `Doi | `Url | `Swh | `Other ] 52 51 (** Identifier types. *) 52 + type t = 53 + [ `Doi 54 + | `Url 55 + | `Swh 56 + | `Other 57 + ] 53 58 59 + (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *) 54 60 val of_string : string -> t option 55 - (** Parse from YAML string: ["doi"], ["url"], ["swh"], ["other"]. *) 56 61 57 - val to_string : t -> string 58 62 (** Convert to YAML string representation. *) 63 + val to_string : t -> string 59 64 60 65 val equal : t -> t -> bool 61 66 val compare : t -> t -> int 62 67 val pp : Format.formatter -> t -> unit 63 68 69 + (** JSON/YAML codec. *) 64 70 val jsont : t Jsont.t 65 - (** JSON/YAML codec. *) 66 71 end 67 72 68 73 (** Reference type for bibliographic entries. ··· 138 143 - [`Standard] - Technical standard 139 144 - [`Unpublished] - Unpublished work *) 140 145 module Reference_type : sig 141 - type t = [ 142 - | `Art 146 + (** All supported reference types. *) 147 + type t = 148 + [ `Art 143 149 | `Article 144 150 | `Audiovisual 145 151 | `Bill ··· 186 192 | `Unpublished 187 193 | `Video 188 194 | `Website 189 - ] 190 - (** All supported reference types. *) 195 + ] 191 196 192 - val of_string : string -> t option 193 197 (** Parse from YAML string. Hyphenated names like ["conference-paper"] 194 198 map to underscored variants like [`Conference_paper]. *) 199 + val of_string : string -> t option 195 200 196 - val to_string : t -> string 197 201 (** Convert to YAML string representation. 198 202 Underscored variants like [`Conference_paper] become ["conference-paper"]. *) 203 + val to_string : t -> string 199 204 200 205 val equal : t -> t -> bool 201 206 val compare : t -> t -> int 202 207 val pp : Format.formatter -> t -> unit 203 208 204 - val jsont : t Jsont.t 205 209 (** JSON/YAML codec. *) 210 + val jsont : t Jsont.t 206 211 end 207 212 208 213 (** Publication status for works in progress. ··· 230 235 status: submitted 231 236 ]} *) 232 237 module Status : sig 233 - type t = [ 234 - | `Abstract 238 + (** Publication status values. *) 239 + type t = 240 + [ `Abstract 235 241 | `Advance_online 236 242 | `In_preparation 237 243 | `In_press 238 244 | `Preprint 239 245 | `Submitted 240 - ] 241 - (** Publication status values. *) 246 + ] 242 247 243 - val of_string : string -> t option 244 248 (** Parse from YAML string: ["abstract"], ["advance-online"], etc. *) 249 + val of_string : string -> t option 245 250 251 + (** Convert to YAML string representation. *) 246 252 val to_string : t -> string 247 - (** Convert to YAML string representation. *) 248 253 249 254 val equal : t -> t -> bool 250 255 val compare : t -> t -> int 251 256 val pp : Format.formatter -> t -> unit 252 257 253 - val jsont : t Jsont.t 254 258 (** JSON/YAML codec. *) 259 + val jsont : t Jsont.t 255 260 end 256 261 257 262 (** CFF file type: software or dataset. ··· 271 276 # ... 272 277 ]} *) 273 278 module Cff_type : sig 274 - type t = [ `Software | `Dataset ] 275 279 (** CFF file types. *) 280 + type t = 281 + [ `Software 282 + | `Dataset 283 + ] 276 284 277 - val of_string : string -> t option 278 285 (** Parse from YAML string: ["software"] or ["dataset"]. *) 286 + val of_string : string -> t option 279 287 280 - val to_string : t -> string 281 288 (** Convert to YAML string representation. *) 289 + val to_string : t -> string 282 290 283 291 val equal : t -> t -> bool 284 292 val compare : t -> t -> int 285 293 val pp : Format.formatter -> t -> unit 286 294 287 - val jsont : t Jsont.t 288 295 (** JSON/YAML codec. *) 296 + val jsont : t Jsont.t 289 297 end
+17 -23
lib/cff_identifier.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (** Identifier type for CFF. *) 7 7 8 - type t = { 9 - type_ : Cff_enums.Identifier_type.t; 10 - value : string; 11 - description : string option; 12 - } 13 - 14 - let make ~type_ ~value ?description () = 15 - { type_; value; description } 8 + type t = 9 + { type_ : Cff_enums.Identifier_type.t 10 + ; value : string 11 + ; description : string option 12 + } 16 13 14 + let make ~type_ ~value ?description () = { type_; value; description } 17 15 let type_ t = t.type_ 18 16 let value t = t.value 19 17 let description t = t.description 20 18 21 19 let equal a b = 22 - Cff_enums.Identifier_type.equal a.type_ b.type_ && 23 - String.equal a.value b.value 20 + Cff_enums.Identifier_type.equal a.type_ b.type_ && String.equal a.value b.value 21 + ;; 24 22 25 23 let compare a b = 26 24 match Cff_enums.Identifier_type.compare a.type_ b.type_ with 27 25 | 0 -> String.compare a.value b.value 28 26 | n -> n 27 + ;; 29 28 30 - let pp ppf t = 31 - Format.fprintf ppf "%a: %s" 32 - Cff_enums.Identifier_type.pp t.type_ 33 - t.value 29 + let pp ppf t = Format.fprintf ppf "%a: %s" Cff_enums.Identifier_type.pp t.type_ t.value 34 30 35 31 let jsont = 36 - Jsont.Object.map ~kind:"Identifier" 37 - (fun type_ value description -> { type_; value; description }) 38 - |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont 39 - ~enc:(fun i -> i.type_) 40 - |> Jsont.Object.mem "value" Jsont.string 41 - ~enc:(fun i -> i.value) 42 - |> Jsont.Object.opt_mem "description" Jsont.string 43 - ~enc:(fun i -> i.description) 32 + Jsont.Object.map ~kind:"Identifier" (fun type_ value description -> 33 + { type_; value; description }) 34 + |> Jsont.Object.mem "type" Cff_enums.Identifier_type.jsont ~enc:(fun i -> i.type_) 35 + |> Jsont.Object.mem "value" Jsont.string ~enc:(fun i -> i.value) 36 + |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun i -> i.description) 44 37 |> Jsont.Object.skip_unknown 45 38 |> Jsont.Object.finish 39 + ;;
+15 -14
lib/cff_identifier.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 66 66 - [rel]: Release 67 67 - [snp]: Snapshot *) 68 68 69 - type t 70 69 (** An identifier with type, value, and optional description. *) 70 + type t 71 71 72 - val make : 73 - type_:Cff_enums.Identifier_type.t -> 74 - value:string -> 75 - ?description:string -> 76 - unit -> t 77 72 (** Create an identifier. 78 73 79 74 @param type_ The identifier type ([`Doi], [`Url], [`Swh], or [`Other]) 80 75 @param value The identifier value (DOI, URL, SWH ID, etc.) 81 76 @param description Optional human-readable description *) 77 + val make 78 + : type_:Cff_enums.Identifier_type.t 79 + -> value:string 80 + -> ?description:string 81 + -> unit 82 + -> t 82 83 84 + (** The identifier type. *) 83 85 val type_ : t -> Cff_enums.Identifier_type.t 84 - (** The identifier type. *) 85 86 86 - val value : t -> string 87 87 (** The identifier value. 88 88 89 89 For DOIs, this is just the DOI (e.g., ["10.5281/zenodo.1234567"]), 90 90 not the full URL. *) 91 + val value : t -> string 91 92 92 - val description : t -> string option 93 93 (** Optional description explaining what this identifier refers to. 94 94 95 95 Examples: 96 96 - ["The concept DOI for all versions"] 97 97 - ["Version 1.0.0 archive"] 98 98 - ["Release on GitHub"] *) 99 + val description : t -> string option 99 100 101 + (** Identifier equality (compares all fields). *) 100 102 val equal : t -> t -> bool 101 - (** Identifier equality (compares all fields). *) 102 103 103 - val compare : t -> t -> int 104 104 (** Identifier comparison. *) 105 + val compare : t -> t -> int 105 106 106 - val pp : Format.formatter -> t -> unit 107 107 (** Pretty-print as "[type]: value (description)". *) 108 + val pp : Format.formatter -> t -> unit 108 109 110 + (** JSON/YAML codec for identifiers. *) 109 111 val jsont : t Jsont.t 110 - (** JSON/YAML codec for identifiers. *)
+63 -35
lib/cff_license.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (** SPDX license handling for CFF. *) 7 7 8 - type t = [ `Expr of Spdx_licenses.t | `Raw of string list ] 8 + type t = 9 + [ `Spdx of Spdx_licenses.t 10 + | `Other of string list * string option 11 + ] 9 12 10 - let of_spdx spdx = `Expr spdx 13 + let of_spdx spdx = `Spdx spdx 11 14 12 15 let of_string s = 13 16 match Spdx_licenses.parse s with 14 - | Ok spdx -> `Expr spdx 15 - | Error _ -> `Raw [s] 17 + | Ok spdx -> `Spdx spdx 18 + | Error _ -> `Other ([ s ], None) 19 + ;; 16 20 17 21 let of_strings ss = 18 - (* Try to parse as OR combination, fall back to Raw *) 22 + (* Try to parse as OR combination, fall back to Other *) 19 23 let try_parse_all () = 20 24 let rec build = function 21 25 | [] -> None 22 - | [s] -> 26 + | [ s ] -> 23 27 (match Spdx_licenses.parse s with 24 28 | Ok spdx -> Some spdx 25 29 | Error _ -> None) ··· 31 35 build ss 32 36 in 33 37 match try_parse_all () with 34 - | Some spdx -> `Expr spdx 35 - | None -> `Raw ss 38 + | Some spdx -> `Spdx spdx 39 + | None -> `Other (ss, None) 40 + ;; 41 + 42 + let with_url url = function 43 + | `Spdx _ as t -> t (* SPDX licenses have well-known URLs, ignore provided URL *) 44 + | `Other (ids, _) -> `Other (ids, Some url) 45 + ;; 46 + 47 + let with_url_opt url_opt t = 48 + match url_opt with 49 + | None -> t 50 + | Some url -> with_url url t 51 + ;; 36 52 37 53 let to_spdx = function 38 - | `Expr spdx -> Some spdx 39 - | `Raw _ -> None 54 + | `Spdx spdx -> Some spdx 55 + | `Other _ -> None 56 + ;; 40 57 41 58 let to_strings = function 42 - | `Expr spdx -> [Spdx_licenses.to_string spdx] 43 - | `Raw ss -> ss 59 + | `Spdx spdx -> [ Spdx_licenses.to_string spdx ] 60 + | `Other (ss, _) -> ss 61 + ;; 62 + 63 + let url = function 64 + | `Spdx _ -> None 65 + | `Other (_, url) -> url 66 + ;; 44 67 45 68 let pp ppf = function 46 - | `Expr spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx) 47 - | `Raw ss -> 48 - match ss with 49 - | [s] -> Format.pp_print_string ppf s 50 - | _ -> 51 - Format.fprintf ppf "[%a]" 52 - (Format.pp_print_list ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ") 53 - Format.pp_print_string) ss 69 + | `Spdx spdx -> Format.pp_print_string ppf (Spdx_licenses.to_string spdx) 70 + | `Other (ss, url_opt) -> 71 + (match ss with 72 + | [ s ] -> Format.pp_print_string ppf s 73 + | _ -> 74 + Format.fprintf 75 + ppf 76 + "[%a]" 77 + (Format.pp_print_list 78 + ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ") 79 + Format.pp_print_string) 80 + ss); 81 + Option.iter (Format.fprintf ppf " <%s>") url_opt 82 + ;; 54 83 55 - (* Jsont codec - lenient, accepts any string/array *) 56 84 let jsont = 57 85 let string_codec = 58 - Jsont.string |> Jsont.map 59 - ~dec:(fun s -> of_string s) 60 - ~enc:(function 61 - | `Expr spdx -> Spdx_licenses.to_string spdx 62 - | `Raw [s] -> s 63 - | `Raw _ -> assert false) 86 + Jsont.string 87 + |> Jsont.map ~dec:of_string ~enc:(function 88 + | `Spdx spdx -> Spdx_licenses.to_string spdx 89 + | `Other ([ s ], _) -> s 90 + | `Other _ -> assert false) 64 91 in 65 92 let array_codec = 66 - Jsont.(array string) |> Jsont.map 67 - ~dec:(fun ss -> of_strings (Array.to_list ss)) 68 - ~enc:(fun t -> Array.of_list (to_strings t)) 93 + Jsont.(array string) 94 + |> Jsont.map 95 + ~dec:(fun ss -> of_strings (Array.to_list ss)) 96 + ~enc:(fun t -> Array.of_list (to_strings t)) 69 97 in 70 98 Jsont.any 71 99 ~dec_string:string_codec 72 100 ~dec_array:array_codec 73 - ~enc:(fun t -> 74 - match t with 75 - | `Expr (Spdx_licenses.Simple _) -> string_codec 76 - | `Raw [_] -> string_codec 101 + ~enc:(function 102 + | `Spdx (Spdx_licenses.Simple _) -> string_codec 103 + | `Other ([ _ ], _) -> string_codec 77 104 | _ -> array_codec) 78 105 () 106 + ;;
+58 -30
lib/cff_license.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (** SPDX license expressions for CFF. 7 7 8 8 CFF uses {{:https://spdx.org/licenses/}SPDX license identifiers} 9 - for the [license] field. This module wraps {!Spdx_licenses.t} with 10 - support for invalid/unknown licenses to enable round-tripping. 9 + for the [license] field. This module combines license identification 10 + with optional URLs for non-standard licenses. 11 11 12 12 {1 License Representation} 13 13 14 14 Licenses are represented as either: 15 - - [`Expr spdx] - A valid, parsed SPDX license expression 16 - - [`Raw strings] - Unparsed strings for invalid/unknown licenses 15 + - [`Spdx expr] - A valid SPDX expression (URL is implicit/well-known) 16 + - [`Other (ids, url_opt)] - Unknown license ID(s) with optional URL 17 17 18 18 The parser is lenient: it tries to parse as SPDX but preserves 19 - invalid strings for round-tripping. 19 + invalid/unknown identifiers for round-tripping. 20 + 21 + {1 Why Combined?} 22 + 23 + The CFF spec has separate [license] and [license-url] fields, but they 24 + have a hidden relationship: 25 + - SPDX licenses have well-known URLs (e.g., MIT → https://spdx.org/licenses/MIT.html) 26 + - [license-url] is only meaningful for non-SPDX licenses 27 + 28 + This type makes that relationship explicit: [`Spdx] licenses don't need 29 + a URL, while [`Other] licenses can optionally include one. 20 30 21 31 {1 Examples} 22 32 23 - {2 Single License} 33 + {2 Standard SPDX License} 24 34 {[ 25 35 license: MIT 26 36 ]} 37 + Parsed as [`Spdx (Simple (LicenseID "MIT"))]. 27 38 28 39 {2 SPDX Expression} 29 40 {[ 30 - license: GPL-3.0-or-later WITH Classpath-exception-2.0 41 + license: Apache-2.0 OR MIT 31 42 ]} 43 + Parsed as [`Spdx (OR ...)]. 32 44 33 - {2 Multiple Licenses (OR)} 45 + {2 Custom License with URL} 34 46 {[ 35 - license: 36 - - Apache-2.0 37 - - MIT 47 + license: ACME-Proprietary-1.0 48 + license-url: https://acme.com/license 38 49 ]} 39 - This is parsed as [Apache-2.0 OR MIT]. *) 50 + Parsed as [`Other (["ACME-Proprietary-1.0"], Some "https://acme.com/license")]. *) 40 51 41 - type t = [ `Expr of Spdx_licenses.t | `Raw of string list ] 42 - (** The license type: either a valid SPDX expression or raw strings. *) 52 + (** License type: SPDX expression or unknown ID(s) with optional URL. *) 53 + type t = 54 + [ `Spdx of Spdx_licenses.t 55 + | `Other of string list * string option 56 + ] 43 57 44 58 (** {1 Construction} *) 45 59 60 + (** [of_spdx expr] wraps a valid SPDX expression. *) 46 61 val of_spdx : Spdx_licenses.t -> t 47 - (** [of_spdx spdx] wraps a valid SPDX expression. *) 48 62 49 - val of_string : string -> t 50 63 (** [of_string s] parses [s] as an SPDX expression. 51 - Returns [`Expr] on success, [`Raw [s]] on parse failure. *) 64 + Returns [`Spdx] on success, [`Other ([s], None)] on parse failure. *) 65 + val of_string : string -> t 52 66 53 - val of_strings : string list -> t 54 67 (** [of_strings ss] parses a list of license strings. 55 - If all strings are valid license IDs, returns an [`Expr] with OR combination. 56 - Otherwise returns [`Raw ss] to preserve the original strings. *) 68 + If all are valid SPDX IDs, returns [`Spdx] with OR combination. 69 + Otherwise returns [`Other (ss, None)]. *) 70 + val of_strings : string list -> t 71 + 72 + (** [with_url url t] adds a URL to the license. 73 + - For [`Spdx], returns unchanged (SPDX URLs are well-known) 74 + - For [`Other (ids, _)], returns [`Other (ids, Some url)] *) 75 + val with_url : string -> t -> t 76 + 77 + (** [with_url_opt url_opt t] optionally adds a URL. 78 + Convenience for combining during jsont decoding. *) 79 + val with_url_opt : string option -> t -> t 57 80 58 81 (** {1 Access} *) 59 82 83 + (** [to_spdx t] returns [Some expr] if valid SPDX, [None] otherwise. *) 60 84 val to_spdx : t -> Spdx_licenses.t option 61 - (** [to_spdx t] returns [Some spdx] if [t] is a valid expression, 62 - [None] if it contains unparsed raw strings. *) 63 85 86 + (** [to_strings t] returns the license identifier(s) as strings. *) 64 87 val to_strings : t -> string list 65 - (** [to_strings t] returns the license as a list of strings. 66 - For [`Expr], returns the normalized SPDX string. 67 - For [`Raw], returns the original strings. *) 88 + 89 + (** [url t] returns the license URL. 90 + - For [`Spdx], always [None] (use SPDX's well-known URLs) 91 + - For [`Other], returns the URL if provided *) 92 + val url : t -> string option 68 93 69 94 (** {1 Formatting} *) 70 95 71 - val pp : Format.formatter -> t -> unit 72 96 (** Pretty-print the license. *) 97 + val pp : Format.formatter -> t -> unit 73 98 74 99 (** {1 Codec} *) 75 100 76 - val jsont : t Jsont.t 77 - (** JSON/YAML codec for licenses. 101 + (** JSON/YAML codec for the license field only. 102 + 103 + This handles just the [license] field. The [license-url] field 104 + is handled separately by the parent codec which combines them 105 + using {!with_url}. 78 106 79 - Handles both single string and array of strings. 80 107 Lenient: accepts any string without validation for round-tripping. *) 108 + val jsont : t Jsont.t
+665 -385
lib/cff_reference.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 7 7 8 8 (** Core identity of a reference. *) 9 9 module Core = struct 10 - type t = { 11 - type_ : Cff_enums.Reference_type.t; 12 - title : string; 13 - authors : Cff_author.t list; 14 - abstract : string option; 15 - abbreviation : string option; 16 - } 10 + type t = 11 + { type_ : Cff_enums.Reference_type.t 12 + ; title : string 13 + ; authors : Cff_author.t list 14 + ; abstract : string option 15 + ; abbreviation : string option 16 + } 17 17 18 18 let make ~type_ ~title ~authors ?abstract ?abbreviation () = 19 19 { type_; title; authors; abstract; abbreviation } 20 + ;; 20 21 21 22 let type_ t = t.type_ 22 23 let title t = t.title 23 24 let authors t = t.authors 24 25 let abstract t = t.abstract 25 26 let abbreviation t = t.abbreviation 26 - 27 - let pp ppf t = 28 - Format.fprintf ppf "%s (%a)" 29 - t.title Cff_enums.Reference_type.pp t.type_ 27 + let pp ppf t = Format.fprintf ppf "%s (%a)" t.title Cff_enums.Reference_type.pp t.type_ 30 28 end 31 29 32 30 (** Publication information (journal, volume, pages, etc.). *) 33 31 module Publication = struct 34 - type t = { 35 - journal : string option; 36 - volume : string option; 37 - issue : string option; 38 - pages : string option; 39 - start : string option; 40 - end_ : string option; 41 - edition : string option; 42 - section : string option; 43 - status : Cff_enums.Status.t option; 44 - } 32 + type t = 33 + { journal : string option 34 + ; volume : string option 35 + ; issue : string option 36 + ; pages : string option 37 + ; start : string option 38 + ; end_ : string option 39 + ; edition : string option 40 + ; section : string option 41 + ; status : Cff_enums.Status.t option 42 + } 45 43 46 - let empty = { 47 - journal = None; volume = None; issue = None; pages = None; 48 - start = None; end_ = None; edition = None; section = None; 49 - status = None; 50 - } 44 + let empty = 45 + { journal = None 46 + ; volume = None 47 + ; issue = None 48 + ; pages = None 49 + ; start = None 50 + ; end_ = None 51 + ; edition = None 52 + ; section = None 53 + ; status = None 54 + } 55 + ;; 51 56 52 - let make ?journal ?volume ?issue ?pages ?start ?end_ ?edition 53 - ?section ?status () = 57 + let make ?journal ?volume ?issue ?pages ?start ?end_ ?edition ?section ?status () = 54 58 { journal; volume; issue; pages; start; end_; edition; section; status } 59 + ;; 55 60 56 61 let journal t = t.journal 57 62 let volume t = t.volume ··· 64 69 let status t = t.status 65 70 66 71 let is_empty t = 67 - t.journal = None && t.volume = None && t.issue = None && 68 - t.pages = None && t.start = None && t.end_ = None && 69 - t.edition = None && t.section = None && t.status = None 72 + t.journal = None 73 + && t.volume = None 74 + && t.issue = None 75 + && t.pages = None 76 + && t.start = None 77 + && t.end_ = None 78 + && t.edition = None 79 + && t.section = None 80 + && t.status = None 81 + ;; 70 82 end 71 83 72 84 (** Collection information (proceedings, book series, etc.). *) 73 85 module Collection = struct 74 - type t = { 75 - collection_title : string option; 76 - collection_type : string option; 77 - collection_doi : string option; 78 - volume_title : string option; 79 - number_volumes : string option; 80 - } 86 + type t = 87 + { collection_title : string option 88 + ; collection_type : string option 89 + ; collection_doi : string option 90 + ; volume_title : string option 91 + ; number_volumes : string option 92 + } 81 93 82 - let empty = { 83 - collection_title = None; collection_type = None; 84 - collection_doi = None; volume_title = None; number_volumes = None; 85 - } 94 + let empty = 95 + { collection_title = None 96 + ; collection_type = None 97 + ; collection_doi = None 98 + ; volume_title = None 99 + ; number_volumes = None 100 + } 101 + ;; 86 102 87 - let make ?collection_title ?collection_type ?collection_doi 88 - ?volume_title ?number_volumes () = 89 - { collection_title; collection_type; collection_doi; 90 - volume_title; number_volumes } 103 + let make 104 + ?collection_title 105 + ?collection_type 106 + ?collection_doi 107 + ?volume_title 108 + ?number_volumes 109 + () 110 + = 111 + { collection_title; collection_type; collection_doi; volume_title; number_volumes } 112 + ;; 91 113 92 114 let collection_title t = t.collection_title 93 115 let collection_type t = t.collection_type ··· 96 118 let number_volumes t = t.number_volumes 97 119 98 120 let is_empty t = 99 - t.collection_title = None && t.collection_type = None && 100 - t.collection_doi = None && t.volume_title = None && 101 - t.number_volumes = None 121 + t.collection_title = None 122 + && t.collection_type = None 123 + && t.collection_doi = None 124 + && t.volume_title = None 125 + && t.number_volumes = None 126 + ;; 102 127 end 103 128 104 129 (** Date information. *) 105 130 module Dates = struct 106 - type t = { 107 - date_accessed : Cff_date.t option; 108 - date_downloaded : Cff_date.t option; 109 - date_published : Cff_date.t option; 110 - date_released : Cff_date.t option; 111 - year : int option; 112 - year_original : int option; 113 - month : int option; 114 - issue_date : string option; 115 - } 131 + type t = 132 + { date_accessed : Cff_date.t option 133 + ; date_downloaded : Cff_date.t option 134 + ; date_published : Cff_date.t option 135 + ; date_released : Cff_date.t option 136 + ; year : int option 137 + ; year_original : int option 138 + ; month : int option 139 + ; issue_date : string option 140 + } 116 141 117 - let empty = { 118 - date_accessed = None; date_downloaded = None; 119 - date_published = None; date_released = None; 120 - year = None; year_original = None; month = None; issue_date = None; 121 - } 142 + let empty = 143 + { date_accessed = None 144 + ; date_downloaded = None 145 + ; date_published = None 146 + ; date_released = None 147 + ; year = None 148 + ; year_original = None 149 + ; month = None 150 + ; issue_date = None 151 + } 152 + ;; 122 153 123 - let make ?date_accessed ?date_downloaded ?date_published ?date_released 124 - ?year ?year_original ?month ?issue_date () = 125 - { date_accessed; date_downloaded; date_published; date_released; 126 - year; year_original; month; issue_date } 154 + let make 155 + ?date_accessed 156 + ?date_downloaded 157 + ?date_published 158 + ?date_released 159 + ?year 160 + ?year_original 161 + ?month 162 + ?issue_date 163 + () 164 + = 165 + { date_accessed 166 + ; date_downloaded 167 + ; date_published 168 + ; date_released 169 + ; year 170 + ; year_original 171 + ; month 172 + ; issue_date 173 + } 174 + ;; 127 175 128 176 let date_accessed t = t.date_accessed 129 177 let date_downloaded t = t.date_downloaded ··· 135 183 let issue_date t = t.issue_date 136 184 137 185 let is_empty t = 138 - t.date_accessed = None && t.date_downloaded = None && 139 - t.date_published = None && t.date_released = None && 140 - t.year = None && t.year_original = None && 141 - t.month = None && t.issue_date = None 186 + t.date_accessed = None 187 + && t.date_downloaded = None 188 + && t.date_published = None 189 + && t.date_released = None 190 + && t.year = None 191 + && t.year_original = None 192 + && t.month = None 193 + && t.issue_date = None 194 + ;; 142 195 end 143 196 144 197 (** Identifiers and links. *) 145 198 module Identifiers = struct 146 - type t = { 147 - doi : string option; 148 - url : string option; 149 - repository : string option; 150 - repository_code : string option; 151 - repository_artifact : string option; 152 - isbn : string option; 153 - issn : string option; 154 - pmcid : string option; 155 - nihmsid : string option; 156 - identifiers : Cff_identifier.t list option; 157 - } 199 + type t = 200 + { doi : string option 201 + ; url : string option 202 + ; repository : string option 203 + ; repository_code : string option 204 + ; repository_artifact : string option 205 + ; isbn : string option 206 + ; issn : string option 207 + ; pmcid : string option 208 + ; nihmsid : string option 209 + ; identifiers : Cff_identifier.t list option 210 + } 158 211 159 - let empty = { 160 - doi = None; url = None; repository = None; 161 - repository_code = None; repository_artifact = None; 162 - isbn = None; issn = None; pmcid = None; nihmsid = None; 163 - identifiers = None; 164 - } 212 + let empty = 213 + { doi = None 214 + ; url = None 215 + ; repository = None 216 + ; repository_code = None 217 + ; repository_artifact = None 218 + ; isbn = None 219 + ; issn = None 220 + ; pmcid = None 221 + ; nihmsid = None 222 + ; identifiers = None 223 + } 224 + ;; 165 225 166 - let make ?doi ?url ?repository ?repository_code ?repository_artifact 167 - ?isbn ?issn ?pmcid ?nihmsid ?identifiers () = 168 - { doi; url; repository; repository_code; repository_artifact; 169 - isbn; issn; pmcid; nihmsid; identifiers } 226 + let make 227 + ?doi 228 + ?url 229 + ?repository 230 + ?repository_code 231 + ?repository_artifact 232 + ?isbn 233 + ?issn 234 + ?pmcid 235 + ?nihmsid 236 + ?identifiers 237 + () 238 + = 239 + { doi 240 + ; url 241 + ; repository 242 + ; repository_code 243 + ; repository_artifact 244 + ; isbn 245 + ; issn 246 + ; pmcid 247 + ; nihmsid 248 + ; identifiers 249 + } 250 + ;; 170 251 171 252 let doi t = t.doi 172 253 let url t = t.url ··· 180 261 let identifiers t = t.identifiers 181 262 182 263 let is_empty t = 183 - t.doi = None && t.url = None && t.repository = None && 184 - t.repository_code = None && t.repository_artifact = None && 185 - t.isbn = None && t.issn = None && t.pmcid = None && 186 - t.nihmsid = None && t.identifiers = None 264 + t.doi = None 265 + && t.url = None 266 + && t.repository = None 267 + && t.repository_code = None 268 + && t.repository_artifact = None 269 + && t.isbn = None 270 + && t.issn = None 271 + && t.pmcid = None 272 + && t.nihmsid = None 273 + && t.identifiers = None 274 + ;; 187 275 end 188 276 189 277 (** Related entities (editors, publisher, etc.). *) 190 278 module Entities = struct 191 - type t = { 192 - editors : Cff_author.t list option; 193 - editors_series : Cff_author.t list option; 194 - translators : Cff_author.t list option; 195 - recipients : Cff_author.t list option; 196 - senders : Cff_author.t list option; 197 - contact : Cff_author.t list option; 198 - publisher : Cff_author.Entity.t option; 199 - institution : Cff_author.Entity.t option; 200 - conference : Cff_author.Entity.t option; 201 - database_provider : Cff_author.Entity.t option; 202 - location : Cff_author.Entity.t option; 203 - } 279 + type t = 280 + { editors : Cff_author.t list option 281 + ; editors_series : Cff_author.t list option 282 + ; translators : Cff_author.t list option 283 + ; recipients : Cff_author.t list option 284 + ; senders : Cff_author.t list option 285 + ; contact : Cff_author.t list option 286 + ; publisher : Cff_author.Entity.t option 287 + ; institution : Cff_author.Entity.t option 288 + ; conference : Cff_author.Entity.t option 289 + ; database_provider : Cff_author.Entity.t option 290 + ; location : Cff_author.Entity.t option 291 + } 204 292 205 - let empty = { 206 - editors = None; editors_series = None; translators = None; 207 - recipients = None; senders = None; contact = None; 208 - publisher = None; institution = None; conference = None; 209 - database_provider = None; location = None; 210 - } 293 + let empty = 294 + { editors = None 295 + ; editors_series = None 296 + ; translators = None 297 + ; recipients = None 298 + ; senders = None 299 + ; contact = None 300 + ; publisher = None 301 + ; institution = None 302 + ; conference = None 303 + ; database_provider = None 304 + ; location = None 305 + } 306 + ;; 211 307 212 - let make ?editors ?editors_series ?translators ?recipients ?senders 213 - ?contact ?publisher ?institution ?conference ?database_provider 214 - ?location () = 215 - { editors; editors_series; translators; recipients; senders; 216 - contact; publisher; institution; conference; database_provider; 217 - location } 308 + let make 309 + ?editors 310 + ?editors_series 311 + ?translators 312 + ?recipients 313 + ?senders 314 + ?contact 315 + ?publisher 316 + ?institution 317 + ?conference 318 + ?database_provider 319 + ?location 320 + () 321 + = 322 + { editors 323 + ; editors_series 324 + ; translators 325 + ; recipients 326 + ; senders 327 + ; contact 328 + ; publisher 329 + ; institution 330 + ; conference 331 + ; database_provider 332 + ; location 333 + } 334 + ;; 218 335 219 336 let editors t = t.editors 220 337 let editors_series t = t.editors_series ··· 229 346 let location t = t.location 230 347 231 348 let is_empty t = 232 - t.editors = None && t.editors_series = None && t.translators = None && 233 - t.recipients = None && t.senders = None && t.contact = None && 234 - t.publisher = None && t.institution = None && t.conference = None && 235 - t.database_provider = None && t.location = None 349 + t.editors = None 350 + && t.editors_series = None 351 + && t.translators = None 352 + && t.recipients = None 353 + && t.senders = None 354 + && t.contact = None 355 + && t.publisher = None 356 + && t.institution = None 357 + && t.conference = None 358 + && t.database_provider = None 359 + && t.location = None 360 + ;; 236 361 end 237 362 238 363 (** Metadata and description. *) 239 364 module Metadata = struct 240 - type t = { 241 - keywords : string list option; 242 - languages : string list option; 243 - license : Cff_license.t option; 244 - license_url : string option; 245 - copyright : string option; 246 - scope : string option; 247 - notes : string option; 248 - } 365 + type t = 366 + { keywords : string list option 367 + ; languages : string list option 368 + ; license : Cff_license.t option 369 + ; copyright : string option 370 + ; scope : string option 371 + ; notes : string option 372 + } 249 373 250 - let empty = { 251 - keywords = None; languages = None; license = None; 252 - license_url = None; copyright = None; scope = None; notes = None; 253 - } 374 + let empty = 375 + { keywords = None 376 + ; languages = None 377 + ; license = None 378 + ; copyright = None 379 + ; scope = None 380 + ; notes = None 381 + } 382 + ;; 254 383 255 - let make ?keywords ?languages ?license ?license_url ?copyright 256 - ?scope ?notes () = 257 - { keywords; languages; license; license_url; copyright; scope; notes } 384 + let make ?keywords ?languages ?license ?copyright ?scope ?notes () = 385 + { keywords; languages; license; copyright; scope; notes } 386 + ;; 258 387 259 388 let keywords t = t.keywords 260 389 let languages t = t.languages 261 390 let license t = t.license 262 - let license_url t = t.license_url 263 391 let copyright t = t.copyright 264 392 let scope t = t.scope 265 393 let notes t = t.notes 266 394 267 395 let is_empty t = 268 - t.keywords = None && t.languages = None && t.license = None && 269 - t.license_url = None && t.copyright = None && 270 - t.scope = None && t.notes = None 396 + t.keywords = None 397 + && t.languages = None 398 + && t.license = None 399 + && t.copyright = None 400 + && t.scope = None 401 + && t.notes = None 402 + ;; 271 403 end 272 404 273 405 (** Technical and domain-specific fields. *) 274 406 module Technical = struct 275 - type t = { 276 - commit : string option; 277 - version : string option; 278 - filename : string option; 279 - format : string option; 280 - medium : string option; 281 - data_type : string option; 282 - database : string option; 283 - number : string option; 284 - patent_states : string list option; 285 - thesis_type : string option; 286 - term : string option; 287 - entry : string option; 288 - department : string option; 289 - loc_start : string option; 290 - loc_end : string option; 291 - } 407 + type t = 408 + { commit : string option 409 + ; version : string option 410 + ; filename : string option 411 + ; format : string option 412 + ; medium : string option 413 + ; data_type : string option 414 + ; database : string option 415 + ; number : string option 416 + ; patent_states : string list option 417 + ; thesis_type : string option 418 + ; term : string option 419 + ; entry : string option 420 + ; department : string option 421 + ; loc_start : string option 422 + ; loc_end : string option 423 + } 292 424 293 - let empty = { 294 - commit = None; version = None; filename = None; format = None; 295 - medium = None; data_type = None; database = None; number = None; 296 - patent_states = None; thesis_type = None; term = None; entry = None; 297 - department = None; loc_start = None; loc_end = None; 298 - } 425 + let empty = 426 + { commit = None 427 + ; version = None 428 + ; filename = None 429 + ; format = None 430 + ; medium = None 431 + ; data_type = None 432 + ; database = None 433 + ; number = None 434 + ; patent_states = None 435 + ; thesis_type = None 436 + ; term = None 437 + ; entry = None 438 + ; department = None 439 + ; loc_start = None 440 + ; loc_end = None 441 + } 442 + ;; 299 443 300 - let make ?commit ?version ?filename ?format ?medium ?data_type 301 - ?database ?number ?patent_states ?thesis_type ?term ?entry 302 - ?department ?loc_start ?loc_end () = 303 - { commit; version; filename; format; medium; data_type; database; 304 - number; patent_states; thesis_type; term; entry; department; 305 - loc_start; loc_end } 444 + let make 445 + ?commit 446 + ?version 447 + ?filename 448 + ?format 449 + ?medium 450 + ?data_type 451 + ?database 452 + ?number 453 + ?patent_states 454 + ?thesis_type 455 + ?term 456 + ?entry 457 + ?department 458 + ?loc_start 459 + ?loc_end 460 + () 461 + = 462 + { commit 463 + ; version 464 + ; filename 465 + ; format 466 + ; medium 467 + ; data_type 468 + ; database 469 + ; number 470 + ; patent_states 471 + ; thesis_type 472 + ; term 473 + ; entry 474 + ; department 475 + ; loc_start 476 + ; loc_end 477 + } 478 + ;; 306 479 307 480 let commit t = t.commit 308 481 let version t = t.version ··· 321 494 let loc_end t = t.loc_end 322 495 323 496 let is_empty t = 324 - t.commit = None && t.version = None && t.filename = None && 325 - t.format = None && t.medium = None && t.data_type = None && 326 - t.database = None && t.number = None && t.patent_states = None && 327 - t.thesis_type = None && t.term = None && t.entry = None && 328 - t.department = None && t.loc_start = None && t.loc_end = None 497 + t.commit = None 498 + && t.version = None 499 + && t.filename = None 500 + && t.format = None 501 + && t.medium = None 502 + && t.data_type = None 503 + && t.database = None 504 + && t.number = None 505 + && t.patent_states = None 506 + && t.thesis_type = None 507 + && t.term = None 508 + && t.entry = None 509 + && t.department = None 510 + && t.loc_start = None 511 + && t.loc_end = None 512 + ;; 329 513 end 330 514 331 515 (** Complete reference type. *) 332 - type t = { 333 - core : Core.t; 334 - publication : Publication.t; 335 - collection : Collection.t; 336 - dates : Dates.t; 337 - identifiers : Identifiers.t; 338 - entities : Entities.t; 339 - metadata : Metadata.t; 340 - technical : Technical.t; 341 - } 516 + type t = 517 + { core : Core.t 518 + ; publication : Publication.t 519 + ; collection : Collection.t 520 + ; dates : Dates.t 521 + ; identifiers : Identifiers.t 522 + ; entities : Entities.t 523 + ; metadata : Metadata.t 524 + ; technical : Technical.t 525 + } 342 526 343 - let make ~core 344 - ?(publication = Publication.empty) 345 - ?(collection = Collection.empty) 346 - ?(dates = Dates.empty) 347 - ?(identifiers = Identifiers.empty) 348 - ?(entities = Entities.empty) 349 - ?(metadata = Metadata.empty) 350 - ?(technical = Technical.empty) 351 - () = 352 - { core; publication; collection; dates; identifiers; 353 - entities; metadata; technical } 527 + let make 528 + ~core 529 + ?(publication = Publication.empty) 530 + ?(collection = Collection.empty) 531 + ?(dates = Dates.empty) 532 + ?(identifiers = Identifiers.empty) 533 + ?(entities = Entities.empty) 534 + ?(metadata = Metadata.empty) 535 + ?(technical = Technical.empty) 536 + () 537 + = 538 + { core; publication; collection; dates; identifiers; entities; metadata; technical } 539 + ;; 354 540 355 541 let make_simple ~type_ ~title ~authors ?doi ?year ?journal () = 356 542 let core = Core.make ~type_ ~title ~authors () in ··· 358 544 let dates = Dates.make ?year () in 359 545 let identifiers = Identifiers.make ?doi () in 360 546 make ~core ~publication ~dates ~identifiers () 547 + ;; 361 548 362 549 (* Accessors for sub-records *) 363 550 let core t = t.core ··· 375 562 let authors t = Core.authors t.core 376 563 let doi t = Identifiers.doi t.identifiers 377 564 let year t = Dates.year t.dates 378 - 379 - let pp ppf t = 380 - Core.pp ppf t.core 565 + let pp ppf t = Core.pp ppf t.core 381 566 382 567 (* Helper for string that can also be int (for pages, etc.) *) 383 568 let string_or_int_jsont = 384 569 Jsont.any 385 - ~dec_number:(Jsont.number |> Jsont.map 386 - ~dec:(fun f -> string_of_int (int_of_float f)) 387 - ~enc:float_of_string) 570 + ~dec_number: 571 + (Jsont.number 572 + |> Jsont.map ~dec:(fun f -> string_of_int (int_of_float f)) ~enc:float_of_string) 388 573 ~dec_string:Jsont.string 389 574 ~enc:(fun s -> 390 575 match float_of_string_opt s with 391 - | Some _ -> Jsont.number |> Jsont.map ~dec:(fun _ -> assert false) ~enc:float_of_string 576 + | Some _ -> 577 + Jsont.number |> Jsont.map ~dec:(fun _ -> assert false) ~enc:float_of_string 392 578 | None -> Jsont.string) 393 579 () 580 + ;; 394 581 395 582 (* Helper to convert array jsont to list jsont *) 396 583 let list_jsont elt = 397 584 Jsont.(array elt |> map ~dec:Stdlib.Array.to_list ~enc:Stdlib.Array.of_list) 585 + ;; 398 586 399 587 (* Jsont codec for the full reference type *) 400 588 let jsont = ··· 402 590 let identifiers_list_jsont = list_jsont Cff_identifier.jsont in 403 591 let string_list_jsont = list_jsont Jsont.string in 404 592 (* We need to decode all 60+ fields and then group into sub-records *) 405 - Jsont.Object.map ~kind:"Reference" 406 - (fun type_ title authors abstract abbreviation 593 + Jsont.Object.map 594 + ~kind:"Reference" 595 + (fun 596 + type_ 597 + title 598 + authors 599 + abstract 600 + abbreviation 407 601 (* Publication *) 408 - journal volume issue pages start end_ edition section status 602 + journal 603 + volume 604 + issue 605 + pages 606 + start 607 + end_ 608 + edition 609 + section 610 + status 409 611 (* Collection *) 410 - collection_title collection_type collection_doi volume_title number_volumes 612 + collection_title 613 + collection_type 614 + collection_doi 615 + volume_title 616 + number_volumes 411 617 (* Dates *) 412 - date_accessed date_downloaded date_published date_released 413 - year year_original month issue_date 618 + date_accessed 619 + date_downloaded 620 + date_published 621 + date_released 622 + year 623 + year_original 624 + month 625 + issue_date 414 626 (* Identifiers *) 415 - doi url repository repository_code repository_artifact 416 - isbn issn pmcid nihmsid identifiers_list 627 + doi 628 + url 629 + repository 630 + repository_code 631 + repository_artifact 632 + isbn 633 + issn 634 + pmcid 635 + nihmsid 636 + identifiers_list 417 637 (* Entities *) 418 - editors editors_series translators recipients senders contact 419 - publisher institution conference database_provider location_entity 638 + editors 639 + editors_series 640 + translators 641 + recipients 642 + senders 643 + contact 644 + publisher 645 + institution 646 + conference 647 + database_provider 648 + location_entity 420 649 (* Metadata *) 421 - keywords languages license license_url copyright scope notes 650 + keywords 651 + languages 652 + license 653 + license_url 654 + copyright 655 + scope 656 + notes 422 657 (* Technical *) 423 - commit version filename format medium data_type database 424 - number patent_states thesis_type term entry department 425 - loc_start loc_end -> 426 - let core = { Core.type_; title; authors; abstract; abbreviation } in 427 - let publication = { Publication.journal; volume; issue; pages; 428 - start; end_; edition; section; status } in 429 - let collection = { Collection.collection_title; collection_type; 430 - collection_doi; volume_title; number_volumes } in 431 - let dates = { Dates.date_accessed; date_downloaded; date_published; 432 - date_released; year; year_original; month; issue_date } in 433 - let identifiers = { Identifiers.doi; url; repository; repository_code; 434 - repository_artifact; isbn; issn; pmcid; nihmsid; 435 - identifiers = identifiers_list } in 436 - let entities = { Entities.editors; editors_series; translators; 437 - recipients; senders; contact; publisher; institution; 438 - conference; database_provider; location = location_entity } in 439 - let metadata = { Metadata.keywords; languages; license; license_url; 440 - copyright; scope; notes } in 441 - let technical = { Technical.commit; version; filename; format; medium; 442 - data_type; database; number; patent_states; thesis_type; 443 - term; entry; department; loc_start; loc_end } in 444 - { core; publication; collection; dates; identifiers; 445 - entities; metadata; technical }) 658 + commit 659 + version 660 + filename 661 + format 662 + medium 663 + data_type 664 + database 665 + number 666 + patent_states 667 + thesis_type 668 + term 669 + entry 670 + department 671 + loc_start 672 + loc_end 673 + -> 674 + let core = { Core.type_; title; authors; abstract; abbreviation } in 675 + let publication = 676 + { Publication.journal 677 + ; volume 678 + ; issue 679 + ; pages 680 + ; start 681 + ; end_ 682 + ; edition 683 + ; section 684 + ; status 685 + } 686 + in 687 + let collection = 688 + { Collection.collection_title 689 + ; collection_type 690 + ; collection_doi 691 + ; volume_title 692 + ; number_volumes 693 + } 694 + in 695 + let dates = 696 + { Dates.date_accessed 697 + ; date_downloaded 698 + ; date_published 699 + ; date_released 700 + ; year 701 + ; year_original 702 + ; month 703 + ; issue_date 704 + } 705 + in 706 + let identifiers = 707 + { Identifiers.doi 708 + ; url 709 + ; repository 710 + ; repository_code 711 + ; repository_artifact 712 + ; isbn 713 + ; issn 714 + ; pmcid 715 + ; nihmsid 716 + ; identifiers = identifiers_list 717 + } 718 + in 719 + let entities = 720 + { Entities.editors 721 + ; editors_series 722 + ; translators 723 + ; recipients 724 + ; senders 725 + ; contact 726 + ; publisher 727 + ; institution 728 + ; conference 729 + ; database_provider 730 + ; location = location_entity 731 + } 732 + in 733 + let license = Option.map (Cff_license.with_url_opt license_url) license in 734 + let metadata = 735 + { Metadata.keywords; languages; license; copyright; scope; notes } 736 + in 737 + let technical = 738 + { Technical.commit 739 + ; version 740 + ; filename 741 + ; format 742 + ; medium 743 + ; data_type 744 + ; database 745 + ; number 746 + ; patent_states 747 + ; thesis_type 748 + ; term 749 + ; entry 750 + ; department 751 + ; loc_start 752 + ; loc_end 753 + } 754 + in 755 + { core 756 + ; publication 757 + ; collection 758 + ; dates 759 + ; identifiers 760 + ; entities 761 + ; metadata 762 + ; technical 763 + }) 446 764 (* Core fields *) 447 - |> Jsont.Object.mem "type" Cff_enums.Reference_type.jsont 448 - ~enc:(fun r -> r.core.type_) 449 - |> Jsont.Object.mem "title" Jsont.string 450 - ~enc:(fun r -> r.core.title) 451 - |> Jsont.Object.mem "authors" authors_list_jsont 452 - ~enc:(fun r -> r.core.authors) 453 - |> Jsont.Object.opt_mem "abstract" Jsont.string 454 - ~enc:(fun r -> r.core.abstract) 455 - |> Jsont.Object.opt_mem "abbreviation" Jsont.string 456 - ~enc:(fun r -> r.core.abbreviation) 765 + |> Jsont.Object.mem "type" Cff_enums.Reference_type.jsont ~enc:(fun r -> r.core.type_) 766 + |> Jsont.Object.mem "title" Jsont.string ~enc:(fun r -> r.core.title) 767 + |> Jsont.Object.mem "authors" authors_list_jsont ~enc:(fun r -> r.core.authors) 768 + |> Jsont.Object.opt_mem "abstract" Jsont.string ~enc:(fun r -> r.core.abstract) 769 + |> Jsont.Object.opt_mem "abbreviation" Jsont.string ~enc:(fun r -> r.core.abbreviation) 457 770 (* Publication fields *) 458 - |> Jsont.Object.opt_mem "journal" Jsont.string 459 - ~enc:(fun r -> r.publication.journal) 460 - |> Jsont.Object.opt_mem "volume" string_or_int_jsont 461 - ~enc:(fun r -> r.publication.volume) 462 - |> Jsont.Object.opt_mem "issue" string_or_int_jsont 463 - ~enc:(fun r -> r.publication.issue) 464 - |> Jsont.Object.opt_mem "pages" string_or_int_jsont 465 - ~enc:(fun r -> r.publication.pages) 466 - |> Jsont.Object.opt_mem "start" string_or_int_jsont 467 - ~enc:(fun r -> r.publication.start) 468 - |> Jsont.Object.opt_mem "end" string_or_int_jsont 469 - ~enc:(fun r -> r.publication.end_) 470 - |> Jsont.Object.opt_mem "edition" Jsont.string 471 - ~enc:(fun r -> r.publication.edition) 472 - |> Jsont.Object.opt_mem "section" string_or_int_jsont 473 - ~enc:(fun r -> r.publication.section) 474 - |> Jsont.Object.opt_mem "status" Cff_enums.Status.jsont 475 - ~enc:(fun r -> r.publication.status) 771 + |> Jsont.Object.opt_mem "journal" Jsont.string ~enc:(fun r -> r.publication.journal) 772 + |> Jsont.Object.opt_mem "volume" string_or_int_jsont ~enc:(fun r -> 773 + r.publication.volume) 774 + |> Jsont.Object.opt_mem "issue" string_or_int_jsont ~enc:(fun r -> r.publication.issue) 775 + |> Jsont.Object.opt_mem "pages" string_or_int_jsont ~enc:(fun r -> r.publication.pages) 776 + |> Jsont.Object.opt_mem "start" string_or_int_jsont ~enc:(fun r -> r.publication.start) 777 + |> Jsont.Object.opt_mem "end" string_or_int_jsont ~enc:(fun r -> r.publication.end_) 778 + |> Jsont.Object.opt_mem "edition" Jsont.string ~enc:(fun r -> r.publication.edition) 779 + |> Jsont.Object.opt_mem "section" string_or_int_jsont ~enc:(fun r -> 780 + r.publication.section) 781 + |> Jsont.Object.opt_mem "status" Cff_enums.Status.jsont ~enc:(fun r -> 782 + r.publication.status) 476 783 (* Collection fields *) 477 - |> Jsont.Object.opt_mem "collection-title" Jsont.string 478 - ~enc:(fun r -> r.collection.collection_title) 479 - |> Jsont.Object.opt_mem "collection-type" Jsont.string 480 - ~enc:(fun r -> r.collection.collection_type) 481 - |> Jsont.Object.opt_mem "collection-doi" Jsont.string 482 - ~enc:(fun r -> r.collection.collection_doi) 483 - |> Jsont.Object.opt_mem "volume-title" Jsont.string 484 - ~enc:(fun r -> r.collection.volume_title) 485 - |> Jsont.Object.opt_mem "number-volumes" string_or_int_jsont 486 - ~enc:(fun r -> r.collection.number_volumes) 784 + |> Jsont.Object.opt_mem "collection-title" Jsont.string ~enc:(fun r -> 785 + r.collection.collection_title) 786 + |> Jsont.Object.opt_mem "collection-type" Jsont.string ~enc:(fun r -> 787 + r.collection.collection_type) 788 + |> Jsont.Object.opt_mem "collection-doi" Jsont.string ~enc:(fun r -> 789 + r.collection.collection_doi) 790 + |> Jsont.Object.opt_mem "volume-title" Jsont.string ~enc:(fun r -> 791 + r.collection.volume_title) 792 + |> Jsont.Object.opt_mem "number-volumes" string_or_int_jsont ~enc:(fun r -> 793 + r.collection.number_volumes) 487 794 (* Date fields *) 488 - |> Jsont.Object.opt_mem "date-accessed" Cff_date.jsont 489 - ~enc:(fun r -> r.dates.date_accessed) 490 - |> Jsont.Object.opt_mem "date-downloaded" Cff_date.jsont 491 - ~enc:(fun r -> r.dates.date_downloaded) 492 - |> Jsont.Object.opt_mem "date-published" Cff_date.jsont 493 - ~enc:(fun r -> r.dates.date_published) 494 - |> Jsont.Object.opt_mem "date-released" Cff_date.jsont 495 - ~enc:(fun r -> r.dates.date_released) 496 - |> Jsont.Object.opt_mem "year" Jsont.int 497 - ~enc:(fun r -> r.dates.year) 498 - |> Jsont.Object.opt_mem "year-original" Jsont.int 499 - ~enc:(fun r -> r.dates.year_original) 500 - |> Jsont.Object.opt_mem "month" Jsont.int 501 - ~enc:(fun r -> r.dates.month) 502 - |> Jsont.Object.opt_mem "issue-date" Jsont.string 503 - ~enc:(fun r -> r.dates.issue_date) 795 + |> Jsont.Object.opt_mem "date-accessed" Cff_date.jsont ~enc:(fun r -> 796 + r.dates.date_accessed) 797 + |> Jsont.Object.opt_mem "date-downloaded" Cff_date.jsont ~enc:(fun r -> 798 + r.dates.date_downloaded) 799 + |> Jsont.Object.opt_mem "date-published" Cff_date.jsont ~enc:(fun r -> 800 + r.dates.date_published) 801 + |> Jsont.Object.opt_mem "date-released" Cff_date.jsont ~enc:(fun r -> 802 + r.dates.date_released) 803 + |> Jsont.Object.opt_mem "year" Jsont.int ~enc:(fun r -> r.dates.year) 804 + |> Jsont.Object.opt_mem "year-original" Jsont.int ~enc:(fun r -> r.dates.year_original) 805 + |> Jsont.Object.opt_mem "month" Jsont.int ~enc:(fun r -> r.dates.month) 806 + |> Jsont.Object.opt_mem "issue-date" Jsont.string ~enc:(fun r -> r.dates.issue_date) 504 807 (* Identifier fields *) 505 - |> Jsont.Object.opt_mem "doi" Jsont.string 506 - ~enc:(fun r -> r.identifiers.doi) 507 - |> Jsont.Object.opt_mem "url" Jsont.string 508 - ~enc:(fun r -> r.identifiers.url) 509 - |> Jsont.Object.opt_mem "repository" Jsont.string 510 - ~enc:(fun r -> r.identifiers.repository) 511 - |> Jsont.Object.opt_mem "repository-code" Jsont.string 512 - ~enc:(fun r -> r.identifiers.repository_code) 513 - |> Jsont.Object.opt_mem "repository-artifact" Jsont.string 514 - ~enc:(fun r -> r.identifiers.repository_artifact) 515 - |> Jsont.Object.opt_mem "isbn" Jsont.string 516 - ~enc:(fun r -> r.identifiers.isbn) 517 - |> Jsont.Object.opt_mem "issn" string_or_int_jsont 518 - ~enc:(fun r -> r.identifiers.issn) 519 - |> Jsont.Object.opt_mem "pmcid" Jsont.string 520 - ~enc:(fun r -> r.identifiers.pmcid) 521 - |> Jsont.Object.opt_mem "nihmsid" Jsont.string 522 - ~enc:(fun r -> r.identifiers.nihmsid) 523 - |> Jsont.Object.opt_mem "identifiers" identifiers_list_jsont 524 - ~enc:(fun r -> r.identifiers.identifiers) 808 + |> Jsont.Object.opt_mem "doi" Jsont.string ~enc:(fun r -> r.identifiers.doi) 809 + |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun r -> r.identifiers.url) 810 + |> Jsont.Object.opt_mem "repository" Jsont.string ~enc:(fun r -> 811 + r.identifiers.repository) 812 + |> Jsont.Object.opt_mem "repository-code" Jsont.string ~enc:(fun r -> 813 + r.identifiers.repository_code) 814 + |> Jsont.Object.opt_mem "repository-artifact" Jsont.string ~enc:(fun r -> 815 + r.identifiers.repository_artifact) 816 + |> Jsont.Object.opt_mem "isbn" Jsont.string ~enc:(fun r -> r.identifiers.isbn) 817 + |> Jsont.Object.opt_mem "issn" string_or_int_jsont ~enc:(fun r -> r.identifiers.issn) 818 + |> Jsont.Object.opt_mem "pmcid" Jsont.string ~enc:(fun r -> r.identifiers.pmcid) 819 + |> Jsont.Object.opt_mem "nihmsid" Jsont.string ~enc:(fun r -> r.identifiers.nihmsid) 820 + |> Jsont.Object.opt_mem "identifiers" identifiers_list_jsont ~enc:(fun r -> 821 + r.identifiers.identifiers) 525 822 (* Entity fields *) 526 - |> Jsont.Object.opt_mem "editors" authors_list_jsont 527 - ~enc:(fun r -> r.entities.editors) 528 - |> Jsont.Object.opt_mem "editors-series" authors_list_jsont 529 - ~enc:(fun r -> r.entities.editors_series) 530 - |> Jsont.Object.opt_mem "translators" authors_list_jsont 531 - ~enc:(fun r -> r.entities.translators) 532 - |> Jsont.Object.opt_mem "recipients" authors_list_jsont 533 - ~enc:(fun r -> r.entities.recipients) 534 - |> Jsont.Object.opt_mem "senders" authors_list_jsont 535 - ~enc:(fun r -> r.entities.senders) 536 - |> Jsont.Object.opt_mem "contact" authors_list_jsont 537 - ~enc:(fun r -> r.entities.contact) 538 - |> Jsont.Object.opt_mem "publisher" Cff_author.Entity.jsont 539 - ~enc:(fun r -> r.entities.publisher) 540 - |> Jsont.Object.opt_mem "institution" Cff_author.Entity.jsont 541 - ~enc:(fun r -> r.entities.institution) 542 - |> Jsont.Object.opt_mem "conference" Cff_author.Entity.jsont 543 - ~enc:(fun r -> r.entities.conference) 544 - |> Jsont.Object.opt_mem "database-provider" Cff_author.Entity.jsont 545 - ~enc:(fun r -> r.entities.database_provider) 546 - |> Jsont.Object.opt_mem "location" Cff_author.Entity.jsont 547 - ~enc:(fun r -> r.entities.location) 823 + |> Jsont.Object.opt_mem "editors" authors_list_jsont ~enc:(fun r -> r.entities.editors) 824 + |> Jsont.Object.opt_mem "editors-series" authors_list_jsont ~enc:(fun r -> 825 + r.entities.editors_series) 826 + |> Jsont.Object.opt_mem "translators" authors_list_jsont ~enc:(fun r -> 827 + r.entities.translators) 828 + |> Jsont.Object.opt_mem "recipients" authors_list_jsont ~enc:(fun r -> 829 + r.entities.recipients) 830 + |> Jsont.Object.opt_mem "senders" authors_list_jsont ~enc:(fun r -> r.entities.senders) 831 + |> Jsont.Object.opt_mem "contact" authors_list_jsont ~enc:(fun r -> r.entities.contact) 832 + |> Jsont.Object.opt_mem "publisher" Cff_author.Entity.jsont ~enc:(fun r -> 833 + r.entities.publisher) 834 + |> Jsont.Object.opt_mem "institution" Cff_author.Entity.jsont ~enc:(fun r -> 835 + r.entities.institution) 836 + |> Jsont.Object.opt_mem "conference" Cff_author.Entity.jsont ~enc:(fun r -> 837 + r.entities.conference) 838 + |> Jsont.Object.opt_mem "database-provider" Cff_author.Entity.jsont ~enc:(fun r -> 839 + r.entities.database_provider) 840 + |> Jsont.Object.opt_mem "location" Cff_author.Entity.jsont ~enc:(fun r -> 841 + r.entities.location) 548 842 (* Metadata fields *) 549 - |> Jsont.Object.opt_mem "keywords" string_list_jsont 550 - ~enc:(fun r -> r.metadata.keywords) 551 - |> Jsont.Object.opt_mem "languages" string_list_jsont 552 - ~enc:(fun r -> r.metadata.languages) 553 - |> Jsont.Object.opt_mem "license" Cff_license.jsont 554 - ~enc:(fun r -> r.metadata.license) 555 - |> Jsont.Object.opt_mem "license-url" Jsont.string 556 - ~enc:(fun r -> r.metadata.license_url) 557 - |> Jsont.Object.opt_mem "copyright" Jsont.string 558 - ~enc:(fun r -> r.metadata.copyright) 559 - |> Jsont.Object.opt_mem "scope" Jsont.string 560 - ~enc:(fun r -> r.metadata.scope) 561 - |> Jsont.Object.opt_mem "notes" Jsont.string 562 - ~enc:(fun r -> r.metadata.notes) 843 + |> Jsont.Object.opt_mem "keywords" string_list_jsont ~enc:(fun r -> r.metadata.keywords) 844 + |> Jsont.Object.opt_mem "languages" string_list_jsont ~enc:(fun r -> 845 + r.metadata.languages) 846 + |> Jsont.Object.opt_mem "license" Cff_license.jsont ~enc:(fun r -> r.metadata.license) 847 + |> Jsont.Object.opt_mem "license-url" Jsont.string ~enc:(fun r -> 848 + Option.bind r.metadata.license Cff_license.url) 849 + |> Jsont.Object.opt_mem "copyright" Jsont.string ~enc:(fun r -> r.metadata.copyright) 850 + |> Jsont.Object.opt_mem "scope" Jsont.string ~enc:(fun r -> r.metadata.scope) 851 + |> Jsont.Object.opt_mem "notes" Jsont.string ~enc:(fun r -> r.metadata.notes) 563 852 (* Technical fields *) 564 - |> Jsont.Object.opt_mem "commit" Jsont.string 565 - ~enc:(fun r -> r.technical.commit) 566 - |> Jsont.Object.opt_mem "version" string_or_int_jsont 567 - ~enc:(fun r -> r.technical.version) 568 - |> Jsont.Object.opt_mem "filename" Jsont.string 569 - ~enc:(fun r -> r.technical.filename) 570 - |> Jsont.Object.opt_mem "format" Jsont.string 571 - ~enc:(fun r -> r.technical.format) 572 - |> Jsont.Object.opt_mem "medium" Jsont.string 573 - ~enc:(fun r -> r.technical.medium) 574 - |> Jsont.Object.opt_mem "data-type" Jsont.string 575 - ~enc:(fun r -> r.technical.data_type) 576 - |> Jsont.Object.opt_mem "database" Jsont.string 577 - ~enc:(fun r -> r.technical.database) 578 - |> Jsont.Object.opt_mem "number" string_or_int_jsont 579 - ~enc:(fun r -> r.technical.number) 580 - |> Jsont.Object.opt_mem "patent-states" string_list_jsont 581 - ~enc:(fun r -> r.technical.patent_states) 582 - |> Jsont.Object.opt_mem "thesis-type" Jsont.string 583 - ~enc:(fun r -> r.technical.thesis_type) 584 - |> Jsont.Object.opt_mem "term" Jsont.string 585 - ~enc:(fun r -> r.technical.term) 586 - |> Jsont.Object.opt_mem "entry" Jsont.string 587 - ~enc:(fun r -> r.technical.entry) 588 - |> Jsont.Object.opt_mem "department" Jsont.string 589 - ~enc:(fun r -> r.technical.department) 590 - |> Jsont.Object.opt_mem "loc-start" string_or_int_jsont 591 - ~enc:(fun r -> r.technical.loc_start) 592 - |> Jsont.Object.opt_mem "loc-end" string_or_int_jsont 593 - ~enc:(fun r -> r.technical.loc_end) 853 + |> Jsont.Object.opt_mem "commit" Jsont.string ~enc:(fun r -> r.technical.commit) 854 + |> Jsont.Object.opt_mem "version" string_or_int_jsont ~enc:(fun r -> 855 + r.technical.version) 856 + |> Jsont.Object.opt_mem "filename" Jsont.string ~enc:(fun r -> r.technical.filename) 857 + |> Jsont.Object.opt_mem "format" Jsont.string ~enc:(fun r -> r.technical.format) 858 + |> Jsont.Object.opt_mem "medium" Jsont.string ~enc:(fun r -> r.technical.medium) 859 + |> Jsont.Object.opt_mem "data-type" Jsont.string ~enc:(fun r -> r.technical.data_type) 860 + |> Jsont.Object.opt_mem "database" Jsont.string ~enc:(fun r -> r.technical.database) 861 + |> Jsont.Object.opt_mem "number" string_or_int_jsont ~enc:(fun r -> r.technical.number) 862 + |> Jsont.Object.opt_mem "patent-states" string_list_jsont ~enc:(fun r -> 863 + r.technical.patent_states) 864 + |> Jsont.Object.opt_mem "thesis-type" Jsont.string ~enc:(fun r -> 865 + r.technical.thesis_type) 866 + |> Jsont.Object.opt_mem "term" Jsont.string ~enc:(fun r -> r.technical.term) 867 + |> Jsont.Object.opt_mem "entry" Jsont.string ~enc:(fun r -> r.technical.entry) 868 + |> Jsont.Object.opt_mem "department" Jsont.string ~enc:(fun r -> r.technical.department) 869 + |> Jsont.Object.opt_mem "loc-start" string_or_int_jsont ~enc:(fun r -> 870 + r.technical.loc_start) 871 + |> Jsont.Object.opt_mem "loc-end" string_or_int_jsont ~enc:(fun r -> 872 + r.technical.loc_end) 594 873 |> Jsont.Object.skip_unknown 595 874 |> Jsont.Object.finish 875 + ;;
+200 -194
lib/cff_reference.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 76 76 module Core : sig 77 77 type t 78 78 79 - val make : 80 - type_:Cff_enums.Reference_type.t -> 81 - title:string -> 82 - authors:Cff_author.t list -> 83 - ?abstract:string -> 84 - ?abbreviation:string -> 85 - unit -> t 86 79 (** Create a core record. 87 80 88 81 @param type_ The reference type (article, book, software, etc.) 89 82 @param title The title of the work 90 83 @param authors List of persons and/or entities *) 84 + val make 85 + : type_:Cff_enums.Reference_type.t 86 + -> title:string 87 + -> authors:Cff_author.t list 88 + -> ?abstract:string 89 + -> ?abbreviation:string 90 + -> unit 91 + -> t 91 92 92 - val type_ : t -> Cff_enums.Reference_type.t 93 93 (** The reference type. Determines which other fields are applicable. *) 94 + val type_ : t -> Cff_enums.Reference_type.t 94 95 95 - val title : t -> string 96 96 (** The title of the referenced work. *) 97 + val title : t -> string 97 98 99 + (** The authors/creators of the work. *) 98 100 val authors : t -> Cff_author.t list 99 - (** The authors/creators of the work. *) 100 101 101 - val abstract : t -> string option 102 102 (** A description or abstract of the work. *) 103 + val abstract : t -> string option 103 104 104 - val abbreviation : t -> string option 105 105 (** Abbreviated form of the title (e.g., for journal names). *) 106 + val abbreviation : t -> string option 106 107 107 108 val pp : Format.formatter -> t -> unit 108 109 end ··· 115 116 module Publication : sig 116 117 type t 117 118 118 - val empty : t 119 119 (** Empty publication record with all fields as [None]. *) 120 + val empty : t 120 121 121 - val make : 122 - ?journal:string -> 123 - ?volume:string -> 124 - ?issue:string -> 125 - ?pages:string -> 126 - ?start:string -> 127 - ?end_:string -> 128 - ?edition:string -> 129 - ?section:string -> 130 - ?status:Cff_enums.Status.t -> 131 - unit -> t 122 + val make 123 + : ?journal:string 124 + -> ?volume:string 125 + -> ?issue:string 126 + -> ?pages:string 127 + -> ?start:string 128 + -> ?end_:string 129 + -> ?edition:string 130 + -> ?section:string 131 + -> ?status:Cff_enums.Status.t 132 + -> unit 133 + -> t 132 134 133 - val journal : t -> string option 134 135 (** The name of the journal or magazine. *) 136 + val journal : t -> string option 135 137 136 - val volume : t -> string option 137 138 (** The volume number of the journal. *) 139 + val volume : t -> string option 138 140 141 + (** The issue number within the volume. *) 139 142 val issue : t -> string option 140 - (** The issue number within the volume. *) 141 143 144 + (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *) 142 145 val pages : t -> string option 143 - (** Page range (e.g., ["123-145"]). Alternative to [start]/[end_]. *) 144 146 145 - val start : t -> string option 146 147 (** Starting page number. *) 148 + val start : t -> string option 147 149 150 + (** Ending page number. *) 148 151 val end_ : t -> string option 149 - (** Ending page number. *) 150 152 153 + (** The edition of the work (e.g., ["2nd edition"]). *) 151 154 val edition : t -> string option 152 - (** The edition of the work (e.g., ["2nd edition"]). *) 153 155 154 - val section : t -> string option 155 156 (** The section of a work (e.g., newspaper section). *) 157 + val section : t -> string option 156 158 157 - val status : t -> Cff_enums.Status.t option 158 159 (** Publication status: preprint, in-press, submitted, etc. *) 160 + val status : t -> Cff_enums.Status.t option 159 161 162 + (** [true] if all fields are [None]. *) 160 163 val is_empty : t -> bool 161 - (** [true] if all fields are [None]. *) 162 164 end 163 165 164 166 (** Collection metadata for works in edited volumes. ··· 170 172 171 173 val empty : t 172 174 173 - val make : 174 - ?collection_title:string -> 175 - ?collection_type:string -> 176 - ?collection_doi:string -> 177 - ?volume_title:string -> 178 - ?number_volumes:string -> 179 - unit -> t 175 + val make 176 + : ?collection_title:string 177 + -> ?collection_type:string 178 + -> ?collection_doi:string 179 + -> ?volume_title:string 180 + -> ?number_volumes:string 181 + -> unit 182 + -> t 180 183 181 - val collection_title : t -> string option 182 184 (** Title of the collection (proceedings, book series, etc.). *) 185 + val collection_title : t -> string option 183 186 187 + (** Type of collection (e.g., ["proceedings"], ["book series"]). *) 184 188 val collection_type : t -> string option 185 - (** Type of collection (e.g., ["proceedings"], ["book series"]). *) 186 189 187 - val collection_doi : t -> string option 188 190 (** DOI of the collection itself (not the individual work). *) 191 + val collection_doi : t -> string option 189 192 193 + (** Title of the specific volume within a multi-volume collection. *) 190 194 val volume_title : t -> string option 191 - (** Title of the specific volume within a multi-volume collection. *) 192 195 193 - val number_volumes : t -> string option 194 196 (** Total number of volumes in the collection. *) 197 + val number_volumes : t -> string option 195 198 196 199 val is_empty : t -> bool 197 200 end ··· 211 214 212 215 val empty : t 213 216 214 - val make : 215 - ?date_accessed:Cff_date.t -> 216 - ?date_downloaded:Cff_date.t -> 217 - ?date_published:Cff_date.t -> 218 - ?date_released:Cff_date.t -> 219 - ?year:int -> 220 - ?year_original:int -> 221 - ?month:int -> 222 - ?issue_date:string -> 223 - unit -> t 217 + val make 218 + : ?date_accessed:Cff_date.t 219 + -> ?date_downloaded:Cff_date.t 220 + -> ?date_published:Cff_date.t 221 + -> ?date_released:Cff_date.t 222 + -> ?year:int 223 + -> ?year_original:int 224 + -> ?month:int 225 + -> ?issue_date:string 226 + -> unit 227 + -> t 224 228 225 - val date_accessed : t -> Cff_date.t option 226 229 (** Date when an online resource was accessed for citation. *) 230 + val date_accessed : t -> Cff_date.t option 227 231 228 - val date_downloaded : t -> Cff_date.t option 229 232 (** Date when a resource was downloaded. *) 233 + val date_downloaded : t -> Cff_date.t option 230 234 235 + (** Formal publication date. *) 231 236 val date_published : t -> Cff_date.t option 232 - (** Formal publication date. *) 233 237 234 - val date_released : t -> Cff_date.t option 235 238 (** Release date (typically for software). *) 239 + val date_released : t -> Cff_date.t option 236 240 241 + (** Publication year when full date is unknown. *) 237 242 val year : t -> int option 238 - (** Publication year when full date is unknown. *) 239 243 240 - val year_original : t -> int option 241 244 (** Year of original publication (for reprints, translations). *) 245 + val year_original : t -> int option 242 246 247 + (** Publication month (1-12) when only month/year is known. *) 243 248 val month : t -> int option 244 - (** Publication month (1-12) when only month/year is known. *) 245 249 246 - val issue_date : t -> string option 247 250 (** Issue date as a string (for periodicals with specific dates). *) 251 + val issue_date : t -> string option 248 252 249 253 val is_empty : t -> bool 250 254 end ··· 263 267 264 268 val empty : t 265 269 266 - val make : 267 - ?doi:string -> 268 - ?url:string -> 269 - ?repository:string -> 270 - ?repository_code:string -> 271 - ?repository_artifact:string -> 272 - ?isbn:string -> 273 - ?issn:string -> 274 - ?pmcid:string -> 275 - ?nihmsid:string -> 276 - ?identifiers:Cff_identifier.t list -> 277 - unit -> t 270 + val make 271 + : ?doi:string 272 + -> ?url:string 273 + -> ?repository:string 274 + -> ?repository_code:string 275 + -> ?repository_artifact:string 276 + -> ?isbn:string 277 + -> ?issn:string 278 + -> ?pmcid:string 279 + -> ?nihmsid:string 280 + -> ?identifiers:Cff_identifier.t list 281 + -> unit 282 + -> t 278 283 284 + (** Digital Object Identifier (e.g., ["10.1234/example"]). *) 279 285 val doi : t -> string option 280 - (** Digital Object Identifier (e.g., ["10.1234/example"]). *) 281 286 282 - val url : t -> string option 283 287 (** URL where the work can be accessed. *) 288 + val url : t -> string option 284 289 285 - val repository : t -> string option 286 290 (** General repository URL. *) 291 + val repository : t -> string option 287 292 293 + (** Source code repository (GitHub, GitLab, etc.). *) 288 294 val repository_code : t -> string option 289 - (** Source code repository (GitHub, GitLab, etc.). *) 290 295 291 - val repository_artifact : t -> string option 292 296 (** Built artifact repository (npm, PyPI, Docker Hub, etc.). *) 297 + val repository_artifact : t -> string option 293 298 294 - val isbn : t -> string option 295 299 (** International Standard Book Number. *) 300 + val isbn : t -> string option 296 301 302 + (** International Standard Serial Number (for journals). *) 297 303 val issn : t -> string option 298 - (** International Standard Serial Number (for journals). *) 299 304 300 - val pmcid : t -> string option 301 305 (** PubMed Central identifier. *) 306 + val pmcid : t -> string option 302 307 303 - val nihmsid : t -> string option 304 308 (** NIH Manuscript Submission System identifier. *) 309 + val nihmsid : t -> string option 305 310 311 + (** Additional typed identifiers (DOI, URL, SWH, other). *) 306 312 val identifiers : t -> Cff_identifier.t list option 307 - (** Additional typed identifiers (DOI, URL, SWH, other). *) 308 313 309 314 val is_empty : t -> bool 310 315 end ··· 321 326 322 327 val empty : t 323 328 324 - val make : 325 - ?editors:Cff_author.t list -> 326 - ?editors_series:Cff_author.t list -> 327 - ?translators:Cff_author.t list -> 328 - ?recipients:Cff_author.t list -> 329 - ?senders:Cff_author.t list -> 330 - ?contact:Cff_author.t list -> 331 - ?publisher:Cff_author.Entity.t -> 332 - ?institution:Cff_author.Entity.t -> 333 - ?conference:Cff_author.Entity.t -> 334 - ?database_provider:Cff_author.Entity.t -> 335 - ?location:Cff_author.Entity.t -> 336 - unit -> t 329 + val make 330 + : ?editors:Cff_author.t list 331 + -> ?editors_series:Cff_author.t list 332 + -> ?translators:Cff_author.t list 333 + -> ?recipients:Cff_author.t list 334 + -> ?senders:Cff_author.t list 335 + -> ?contact:Cff_author.t list 336 + -> ?publisher:Cff_author.Entity.t 337 + -> ?institution:Cff_author.Entity.t 338 + -> ?conference:Cff_author.Entity.t 339 + -> ?database_provider:Cff_author.Entity.t 340 + -> ?location:Cff_author.Entity.t 341 + -> unit 342 + -> t 337 343 338 - val editors : t -> Cff_author.t list option 339 344 (** Editors of the work (for edited volumes). *) 345 + val editors : t -> Cff_author.t list option 340 346 347 + (** Series editors (for book series). *) 341 348 val editors_series : t -> Cff_author.t list option 342 - (** Series editors (for book series). *) 343 349 344 - val translators : t -> Cff_author.t list option 345 350 (** Translators of the work. *) 351 + val translators : t -> Cff_author.t list option 346 352 347 - val recipients : t -> Cff_author.t list option 348 353 (** Recipients (for personal communications). *) 354 + val recipients : t -> Cff_author.t list option 349 355 356 + (** Senders (for personal communications). *) 350 357 val senders : t -> Cff_author.t list option 351 - (** Senders (for personal communications). *) 352 358 353 - val contact : t -> Cff_author.t list option 354 359 (** Contact persons for the work. *) 360 + val contact : t -> Cff_author.t list option 355 361 356 - val publisher : t -> Cff_author.Entity.t option 357 362 (** Publishing organization. *) 363 + val publisher : t -> Cff_author.Entity.t option 358 364 365 + (** Academic/research institution (for theses, reports). *) 359 366 val institution : t -> Cff_author.Entity.t option 360 - (** Academic/research institution (for theses, reports). *) 361 367 362 - val conference : t -> Cff_author.Entity.t option 363 368 (** Conference where the work was presented. *) 369 + val conference : t -> Cff_author.Entity.t option 364 370 365 - val database_provider : t -> Cff_author.Entity.t option 366 371 (** Provider of a database (for data references). *) 372 + val database_provider : t -> Cff_author.Entity.t option 367 373 374 + (** Location entity (city, venue for conferences). *) 368 375 val location : t -> Cff_author.Entity.t option 369 - (** Location entity (city, venue for conferences). *) 370 376 371 377 val is_empty : t -> bool 372 378 end ··· 379 385 380 386 val empty : t 381 387 382 - val make : 383 - ?keywords:string list -> 384 - ?languages:string list -> 385 - ?license:Cff_license.t -> 386 - ?license_url:string -> 387 - ?copyright:string -> 388 - ?scope:string -> 389 - ?notes:string -> 390 - unit -> t 388 + val make 389 + : ?keywords:string list 390 + -> ?languages:string list 391 + -> ?license:Cff_license.t 392 + -> ?copyright:string 393 + -> ?scope:string 394 + -> ?notes:string 395 + -> unit 396 + -> t 391 397 398 + (** Descriptive keywords for the work. *) 392 399 val keywords : t -> string list option 393 - (** Descriptive keywords for the work. *) 394 400 395 - val languages : t -> string list option 396 401 (** Languages the work is available in (ISO 639 codes). *) 402 + val languages : t -> string list option 397 403 404 + (** SPDX license identifier(s), or unknown license with optional URL. *) 398 405 val license : t -> Cff_license.t option 399 - (** SPDX license identifier(s). *) 400 406 401 - val license_url : t -> string option 402 - (** URL to license text (for non-SPDX licenses). *) 403 - 407 + (** Copyright statement. *) 404 408 val copyright : t -> string option 405 - (** Copyright statement. *) 406 409 407 - val scope : t -> string option 408 410 (** Scope of the reference (what aspect it covers). *) 411 + val scope : t -> string option 409 412 413 + (** Additional notes or comments. *) 410 414 val notes : t -> string option 411 - (** Additional notes or comments. *) 412 415 413 416 val is_empty : t -> bool 414 417 end ··· 426 429 427 430 val empty : t 428 431 429 - val make : 430 - ?commit:string -> 431 - ?version:string -> 432 - ?filename:string -> 433 - ?format:string -> 434 - ?medium:string -> 435 - ?data_type:string -> 436 - ?database:string -> 437 - ?number:string -> 438 - ?patent_states:string list -> 439 - ?thesis_type:string -> 440 - ?term:string -> 441 - ?entry:string -> 442 - ?department:string -> 443 - ?loc_start:string -> 444 - ?loc_end:string -> 445 - unit -> t 432 + val make 433 + : ?commit:string 434 + -> ?version:string 435 + -> ?filename:string 436 + -> ?format:string 437 + -> ?medium:string 438 + -> ?data_type:string 439 + -> ?database:string 440 + -> ?number:string 441 + -> ?patent_states:string list 442 + -> ?thesis_type:string 443 + -> ?term:string 444 + -> ?entry:string 445 + -> ?department:string 446 + -> ?loc_start:string 447 + -> ?loc_end:string 448 + -> unit 449 + -> t 446 450 447 - val commit : t -> string option 448 451 (** Git commit hash or VCS revision. *) 452 + val commit : t -> string option 449 453 450 - val version : t -> string option 451 454 (** Version string of the software/data. *) 455 + val version : t -> string option 452 456 453 - val filename : t -> string option 454 457 (** Name of the file being referenced. *) 458 + val filename : t -> string option 455 459 460 + (** Format of the work (e.g., ["PDF"], ["HTML"]). *) 456 461 val format : t -> string option 457 - (** Format of the work (e.g., ["PDF"], ["HTML"]). *) 458 462 463 + (** Physical medium (e.g., ["CD-ROM"], ["print"]). *) 459 464 val medium : t -> string option 460 - (** Physical medium (e.g., ["CD-ROM"], ["print"]). *) 461 465 462 - val data_type : t -> string option 463 466 (** Type of data (for datasets). *) 467 + val data_type : t -> string option 464 468 469 + (** Name of the database. *) 465 470 val database : t -> string option 466 - (** Name of the database. *) 467 471 472 + (** Report/patent/standard number. *) 468 473 val number : t -> string option 469 - (** Report/patent/standard number. *) 470 474 471 - val patent_states : t -> string list option 472 475 (** Countries where a patent is held. *) 476 + val patent_states : t -> string list option 473 477 474 - val thesis_type : t -> string option 475 478 (** Type of thesis (["PhD"], ["Master's"], etc.). *) 479 + val thesis_type : t -> string option 476 480 481 + (** Dictionary/encyclopedia term being referenced. *) 477 482 val term : t -> string option 478 - (** Dictionary/encyclopedia term being referenced. *) 479 483 480 - val entry : t -> string option 481 484 (** Encyclopedia entry name. *) 485 + val entry : t -> string option 482 486 487 + (** Academic department (for theses). *) 483 488 val department : t -> string option 484 - (** Academic department (for theses). *) 485 489 490 + (** Starting line/location in source code. *) 486 491 val loc_start : t -> string option 487 - (** Starting line/location in source code. *) 488 492 489 - val loc_end : t -> string option 490 493 (** Ending line/location in source code. *) 494 + val loc_end : t -> string option 491 495 492 496 val is_empty : t -> bool 493 497 end ··· 497 501 (** The complete reference type combining all sub-records. *) 498 502 type t 499 503 500 - val make : 501 - core:Core.t -> 502 - ?publication:Publication.t -> 503 - ?collection:Collection.t -> 504 - ?dates:Dates.t -> 505 - ?identifiers:Identifiers.t -> 506 - ?entities:Entities.t -> 507 - ?metadata:Metadata.t -> 508 - ?technical:Technical.t -> 509 - unit -> t 510 504 (** Construct a reference from sub-records. 511 505 512 506 Only [core] is required; other sub-records default to empty. *) 507 + val make 508 + : core:Core.t 509 + -> ?publication:Publication.t 510 + -> ?collection:Collection.t 511 + -> ?dates:Dates.t 512 + -> ?identifiers:Identifiers.t 513 + -> ?entities:Entities.t 514 + -> ?metadata:Metadata.t 515 + -> ?technical:Technical.t 516 + -> unit 517 + -> t 513 518 514 - val make_simple : 515 - type_:Cff_enums.Reference_type.t -> 516 - title:string -> 517 - authors:Cff_author.t list -> 518 - ?doi:string -> 519 - ?year:int -> 520 - ?journal:string -> 521 - unit -> t 522 519 (** Convenience constructor for simple references. 523 520 524 521 Creates a reference with just the most common fields. Suitable 525 522 for quick article or software references. *) 523 + val make_simple 524 + : type_:Cff_enums.Reference_type.t 525 + -> title:string 526 + -> authors:Cff_author.t list 527 + -> ?doi:string 528 + -> ?year:int 529 + -> ?journal:string 530 + -> unit 531 + -> t 526 532 527 533 (** {2 Sub-record Accessors} *) 528 534 529 - val core : t -> Core.t 530 535 (** The core identity fields. *) 536 + val core : t -> Core.t 531 537 532 - val publication : t -> Publication.t 533 538 (** Publication metadata (journal, volume, pages). *) 539 + val publication : t -> Publication.t 534 540 541 + (** Collection metadata (proceedings, book series). *) 535 542 val collection : t -> Collection.t 536 - (** Collection metadata (proceedings, book series). *) 537 543 538 - val dates : t -> Dates.t 539 544 (** Date-related fields. *) 545 + val dates : t -> Dates.t 540 546 541 - val identifiers : t -> Identifiers.t 542 547 (** Identifiers and links. *) 548 + val identifiers : t -> Identifiers.t 543 549 550 + (** Related entities (editors, publisher). *) 544 551 val entities : t -> Entities.t 545 - (** Related entities (editors, publisher). *) 546 552 547 - val metadata : t -> Metadata.t 548 553 (** Descriptive metadata (keywords, license). *) 554 + val metadata : t -> Metadata.t 549 555 550 - val technical : t -> Technical.t 551 556 (** Technical fields (commit, version, format). *) 557 + val technical : t -> Technical.t 552 558 553 559 (** {2 Direct Accessors for Common Fields} 554 560 555 561 Convenience accessors that delegate to sub-records. *) 556 562 557 - val type_ : t -> Cff_enums.Reference_type.t 558 563 (** Shortcut for [Core.type_ (core t)]. *) 564 + val type_ : t -> Cff_enums.Reference_type.t 559 565 560 - val title : t -> string 561 566 (** Shortcut for [Core.title (core t)]. *) 567 + val title : t -> string 562 568 563 - val authors : t -> Cff_author.t list 564 569 (** Shortcut for [Core.authors (core t)]. *) 570 + val authors : t -> Cff_author.t list 565 571 572 + (** Shortcut for [Identifiers.doi (identifiers t)]. *) 566 573 val doi : t -> string option 567 - (** Shortcut for [Identifiers.doi (identifiers t)]. *) 568 574 569 - val year : t -> int option 570 575 (** Shortcut for [Dates.year (dates t)]. *) 576 + val year : t -> int option 571 577 572 578 (** {1 Formatting and Codec} *) 573 579 574 - val pp : Format.formatter -> t -> unit 575 580 (** Pretty-print a reference in a human-readable format. *) 581 + val pp : Format.formatter -> t -> unit 576 582 583 + (** JSON/YAML codec for serialization. *) 577 584 val jsont : t Jsont.t 578 - (** JSON/YAML codec for serialization. *)
+15 -5
lib_eio/cff_eio.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 8 8 (* Custom error type for CFF parsing errors *) 9 9 type Eio.Exn.err += E of string 10 10 11 - let () = Eio.Exn.register_pp (fun f -> function 12 - | E msg -> Format.fprintf f "Cff %s" msg; true 11 + let () = 12 + Eio.Exn.register_pp (fun f -> function 13 + | E msg -> 14 + Format.fprintf f "Cff %s" msg; 15 + true 13 16 | _ -> false) 17 + ;; 14 18 15 19 let err msg = Eio.Exn.create (E msg) 16 20 ··· 19 23 match Yamlt.decode ~layout:true Cff.jsont reader with 20 24 | Ok cff -> cff 21 25 | Error msg -> raise (err msg) 26 + ;; 22 27 23 28 let to_yaml_string t = 24 29 let buf = Buffer.create 1024 in ··· 26 31 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with 27 32 | Ok () -> Buffer.contents buf 28 33 | Error msg -> raise (err msg) 34 + ;; 29 35 30 36 let of_yaml_flow flow = 31 37 let reader = Bytesrw_eio.bytes_reader_of_flow flow in 32 38 match Yamlt.decode ~layout:true Cff.jsont reader with 33 39 | Ok cff -> cff 34 40 | Error msg -> raise (err msg) 41 + ;; 35 42 36 43 let to_yaml_flow flow t = 37 44 let writer = Bytesrw_eio.bytes_writer_of_flow flow in 38 45 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with 39 46 | Ok () -> () 40 47 | Error msg -> raise (err msg) 48 + ;; 41 49 42 50 let of_file ~fs path = 43 51 let data = Eio.Path.load Eio.Path.(fs / path) in 44 - try of_yaml_string data 45 - with Eio.Exn.Io _ as ex -> 52 + try of_yaml_string data with 53 + | Eio.Exn.Io _ as ex -> 46 54 let bt = Printexc.get_raw_backtrace () in 47 55 Eio.Exn.reraise_with_context ex bt "parsing CFF file %S" path 56 + ;; 48 57 49 58 let to_file ~fs path t = 50 59 let data = to_yaml_string t in 51 60 Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) data 61 + ;;
+10 -9
lib_eio/cff_eio.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 28 28 Parsing and encoding errors are raised as {!Eio.Exn.Io} exceptions 29 29 with the error type {!E}. *) 30 30 31 - type Eio.Exn.err += E of string 32 - (** CFF parsing or encoding error. The string contains the error message. *) 31 + type Eio.Exn.err += 32 + | E of string 33 + (** CFF parsing or encoding error. The string contains the error message. *) 33 34 34 35 (** {1 String Functions} *) 35 36 36 - val of_yaml_string : string -> Cff.t 37 37 (** [of_yaml_string s] parses a CFF from YAML string [s]. 38 38 39 39 @raise Eio.Exn.Io on parse error. *) 40 + val of_yaml_string : string -> Cff.t 40 41 41 - val to_yaml_string : Cff.t -> string 42 42 (** [to_yaml_string cff] serializes [cff] to a YAML string. 43 43 44 44 The output uses YAML block style for readability. 45 45 46 46 @raise Eio.Exn.Io on encoding error. *) 47 + val to_yaml_string : Cff.t -> string 47 48 48 49 (** {1 Flow Functions} *) 49 50 50 - val of_yaml_flow : _ Eio.Flow.source -> Cff.t 51 51 (** [of_yaml_flow flow] parses a CFF from an Eio source flow. 52 52 53 53 Reads directly from the flow using bytesrw-eio. 54 54 55 55 @raise Eio.Exn.Io on parse error. *) 56 + val of_yaml_flow : _ Eio.Flow.source -> Cff.t 56 57 57 - val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit 58 58 (** [to_yaml_flow flow cff] serializes [cff] to an Eio sink flow. 59 59 60 60 Writes directly to the flow using bytesrw-eio. 61 61 62 62 @raise Eio.Exn.Io on encoding error. *) 63 + val to_yaml_flow : _ Eio.Flow.sink -> Cff.t -> unit 63 64 64 65 (** {1 File Functions} *) 65 66 66 - val of_file : fs:_ Eio.Path.t -> string -> Cff.t 67 67 (** [of_file ~fs path] reads and parses a [CITATION.cff] file. 68 68 69 69 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env]) 70 70 @param path Path to the CFF file 71 71 @raise Eio.Exn.Io if the file cannot be read or contains invalid CFF data. 72 72 The exception context includes the file path. *) 73 + val of_file : fs:_ Eio.Path.t -> string -> Cff.t 73 74 74 - val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit 75 75 (** [to_file ~fs path cff] writes [cff] to a file at [path]. 76 76 77 77 Creates or overwrites the file. ··· 79 79 @param fs The Eio filesystem (e.g., [Eio.Stdenv.fs env]) 80 80 @param path Path to write the CFF file 81 81 @raise Eio.Exn.Io on I/O or encoding failure. *) 82 + val to_file : fs:_ Eio.Path.t -> string -> Cff.t -> unit
+8 -4
lib_unix/cff_unix.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 8 8 let of_yaml_string s = 9 9 let reader = Bytesrw.Bytes.Reader.of_string s in 10 10 Yamlt.decode ~layout:true Cff.jsont reader 11 + ;; 11 12 12 13 let to_yaml_string t = 13 14 let buf = Buffer.create 1024 in ··· 15 16 match Yamlt.encode ~format:Yamlt.Block Cff.jsont t ~eod:true writer with 16 17 | Ok () -> Ok (Buffer.contents buf) 17 18 | Error e -> Error e 19 + ;; 18 20 19 21 let of_file path = 20 22 match In_channel.with_open_text path In_channel.input_all with 21 23 | s -> of_yaml_string s 22 24 | exception Sys_error e -> Error e 25 + ;; 23 26 24 27 let to_file path t = 25 28 match to_yaml_string t with 26 29 | Error e -> Error e 27 30 | Ok s -> 28 - match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with 29 - | () -> Ok () 30 - | exception Sys_error e -> Error e 31 + (match Out_channel.with_open_text path (fun oc -> Out_channel.output_string oc s) with 32 + | () -> Ok () 33 + | exception Sys_error e -> Error e) 34 + ;;
+5 -5
lib_unix/cff_unix.mli
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 ··· 22 22 23 23 {1 Functions} *) 24 24 25 - val of_yaml_string : string -> (Cff.t, string) result 26 25 (** [of_yaml_string s] parses a CFF from YAML string [s]. 27 26 28 27 Returns [Ok cff] on success or [Error msg] with a descriptive error 29 28 message on failure. *) 29 + val of_yaml_string : string -> (Cff.t, string) result 30 30 31 - val to_yaml_string : Cff.t -> (string, string) result 32 31 (** [to_yaml_string cff] serializes [cff] to a YAML string. 33 32 34 33 The output uses YAML block style for readability. *) 34 + val to_yaml_string : Cff.t -> (string, string) result 35 35 36 - val of_file : string -> (Cff.t, string) result 37 36 (** [of_file path] reads and parses a [CITATION.cff] file. 38 37 39 38 Returns [Ok cff] on success or [Error msg] if the file cannot be 40 39 read or contains invalid CFF data. *) 40 + val of_file : string -> (Cff.t, string) result 41 41 42 - val to_file : string -> Cff.t -> (unit, string) result 43 42 (** [to_file path cff] writes [cff] to a file at [path]. 44 43 45 44 Creates or overwrites the file. Returns [Error msg] on I/O failure. *) 45 + val to_file : string -> Cff.t -> (unit, string) result
+4 -2
test/dune
··· 2 2 (name test_cff) 3 3 (package cff) 4 4 (libraries cff cff.unix alcotest) 5 - (deps (source_tree ../vendor/git/citation-file-format/examples))) 5 + (deps 6 + (source_tree ../vendor/git/citation-file-format/examples))) 6 7 7 8 (test 8 9 (name test_cff_eio) 9 10 (package cff) 10 11 (libraries cff cff.eio alcotest eio_main) 11 - (deps (source_tree ../vendor/git/citation-file-format/examples))) 12 + (deps 13 + (source_tree ../vendor/git/citation-file-format/examples)))
+184 -153
test/test_cff.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (* Test the CFF library by parsing upstream fixtures *) 7 7 8 - let minimal_cff = {| 8 + let minimal_cff = 9 + {| 9 10 cff-version: 1.2.0 10 11 message: If you use this software in your work, please cite it using the following metadata 11 12 title: Ruby CFF Library ··· 13 14 - family-names: Haines 14 15 given-names: Robert 15 16 |} 17 + ;; 16 18 17 - let simple_cff = {| 19 + let simple_cff = 20 + {| 18 21 cff-version: 1.2.0 19 22 message: Please cite this software using these metadata. 20 23 title: My Research Software ··· 26 29 doi: 10.5281/zenodo.1234567 27 30 date-released: 2021-08-11 28 31 |} 32 + ;; 29 33 30 34 let test_parse_minimal () = 31 35 match Cff_unix.of_yaml_string minimal_cff with ··· 33 37 Alcotest.(check string) "cff-version" "1.2.0" (Cff.cff_version cff); 34 38 Alcotest.(check string) "title" "Ruby CFF Library" (Cff.title cff); 35 39 Alcotest.(check int) "authors count" 1 (List.length (Cff.authors cff)) 36 - | Error e -> 37 - Alcotest.fail (Printf.sprintf "Failed to parse minimal CFF: %s" e) 40 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse minimal CFF: %s" e) 41 + ;; 38 42 39 43 let test_parse_simple () = 40 44 match Cff_unix.of_yaml_string simple_cff with ··· 47 51 | Some (2021, 8, 11) -> () 48 52 | Some d -> Alcotest.fail (Printf.sprintf "Wrong date: %s" (Cff.Date.to_string d)) 49 53 | None -> Alcotest.fail "Missing date-released") 50 - | Error e -> 51 - Alcotest.fail (Printf.sprintf "Failed to parse simple CFF: %s" e) 54 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse simple CFF: %s" e) 55 + ;; 52 56 53 57 let test_create_programmatic () = 54 - let author = Cff.Author.Person 55 - (Cff.Person.make ~family_names:"Smith" ~given_names:"Jane" ()) in 56 - let cff = Cff.make 57 - ~title:"My Software" 58 - ~authors:[author] 59 - ~version:"1.0.0" 60 - () in 58 + let author = Cff.Author.person ~family_names:"Smith" ~given_names:"Jane" () in 59 + let cff = Cff.make ~title:"My Software" ~authors:[ author ] ~version:"1.0.0" () in 61 60 Alcotest.(check string) "cff-version" "1.2.0" (Cff.cff_version cff); 62 61 Alcotest.(check string) "title" "My Software" (Cff.title cff); 63 62 Alcotest.(check (option string)) "version" (Some "1.0.0") (Cff.version cff) 63 + ;; 64 64 65 65 let test_roundtrip () = 66 66 match Cff_unix.of_yaml_string simple_cff with 67 67 | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 68 68 | Ok cff1 -> 69 - match Cff_unix.to_yaml_string cff1 with 70 - | Error e -> Alcotest.fail (Printf.sprintf "Failed to encode: %s" e) 71 - | Ok yaml -> 72 - match Cff_unix.of_yaml_string yaml with 73 - | Error e -> Alcotest.fail (Printf.sprintf "Failed to reparse: %s" e) 74 - | Ok cff2 -> 75 - Alcotest.(check string) "title preserved" (Cff.title cff1) (Cff.title cff2); 76 - Alcotest.(check string) "cff-version preserved" (Cff.cff_version cff1) (Cff.cff_version cff2) 69 + (match Cff_unix.to_yaml_string cff1 with 70 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to encode: %s" e) 71 + | Ok yaml -> 72 + (match Cff_unix.of_yaml_string yaml with 73 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to reparse: %s" e) 74 + | Ok cff2 -> 75 + Alcotest.(check string) "title preserved" (Cff.title cff1) (Cff.title cff2); 76 + Alcotest.(check string) 77 + "cff-version preserved" 78 + (Cff.cff_version cff1) 79 + (Cff.cff_version cff2))) 80 + ;; 77 81 78 82 let test_parse_key_complete () = 79 - let path = "../vendor/git/citation-file-format/examples/1.2.0/pass/key-complete/CITATION.cff" in 83 + let path = 84 + "../vendor/git/citation-file-format/examples/1.2.0/pass/key-complete/CITATION.cff" 85 + in 80 86 match Cff_unix.of_file path with 81 87 | Ok cff -> 82 88 (* Check basic fields *) ··· 84 90 Alcotest.(check string) "title" "Citation File Format 1.0.0" (Cff.title cff); 85 91 Alcotest.(check (option string)) "version" (Some "1.0.0") (Cff.version cff); 86 92 Alcotest.(check (option string)) "doi" (Some "10.5281/zenodo.1003150") (Cff.doi cff); 87 - Alcotest.(check (option string)) "abstract" 88 - (Some "This is an awesome piece of research software!") (Cff.abstract cff); 89 - Alcotest.(check (option string)) "commit" 90 - (Some "156a04c74a8a79d40c5d705cddf9d36735feab4d") (Cff.commit cff); 91 - 93 + Alcotest.(check (option string)) 94 + "abstract" 95 + (Some "This is an awesome piece of research software!") 96 + (Cff.abstract cff); 97 + Alcotest.(check (option string)) 98 + "commit" 99 + (Some "156a04c74a8a79d40c5d705cddf9d36735feab4d") 100 + (Cff.commit cff); 92 101 (* Check authors - should have 2 (1 person + 1 entity) *) 93 102 Alcotest.(check int) "authors count" 2 (List.length (Cff.authors cff)); 94 - 95 103 (* Check first author is a Person *) 96 104 (match List.hd (Cff.authors cff) with 97 - | Cff.Author.Person p -> 98 - Alcotest.(check (option string)) "person family-names" 99 - (Some "Real Person") (Cff.Person.family_names p); 100 - Alcotest.(check (option string)) "person given-names" 101 - (Some "One Truly") (Cff.Person.given_names p) 102 - | Cff.Author.Entity _ -> Alcotest.fail "Expected Person, got Entity"); 103 - 105 + | `Person p -> 106 + Alcotest.(check (option string)) 107 + "person family-names" 108 + (Some "Real Person") 109 + (Cff.Person.family_names p); 110 + Alcotest.(check (option string)) 111 + "person given-names" 112 + (Some "One Truly") 113 + (Cff.Person.given_names p) 114 + | `Entity _ -> Alcotest.fail "Expected Person, got Entity"); 104 115 (* Check second author is an Entity *) 105 116 (match List.nth (Cff.authors cff) 1 with 106 - | Cff.Author.Entity e -> 107 - Alcotest.(check string) "entity name" 108 - "Entity Project Team Conference entity" (Cff.Entity.name e) 109 - | Cff.Author.Person _ -> Alcotest.fail "Expected Entity, got Person"); 110 - 117 + | `Entity e -> 118 + Alcotest.(check string) 119 + "entity name" 120 + "Entity Project Team Conference entity" 121 + (Cff.Entity.name e) 122 + | `Person _ -> Alcotest.fail "Expected Entity, got Person"); 111 123 (* Check identifiers *) 112 124 (match Cff.identifiers cff with 113 - | Some ids -> 114 - Alcotest.(check int) "identifiers count" 4 (List.length ids) 125 + | Some ids -> Alcotest.(check int) "identifiers count" 4 (List.length ids) 115 126 | None -> Alcotest.fail "Expected identifiers"); 116 - 117 127 (* Check keywords *) 118 128 (match Cff.keywords cff with 119 129 | Some kws -> 120 130 Alcotest.(check int) "keywords count" 4 (List.length kws); 121 131 Alcotest.(check string) "first keyword" "One" (List.hd kws) 122 132 | None -> Alcotest.fail "Expected keywords"); 123 - 124 133 (* Check preferred-citation *) 125 134 (match Cff.preferred_citation cff with 126 135 | Some ref -> 127 - Alcotest.(check string) "preferred-citation title" "Book Title" (Cff.Reference.title ref) 136 + Alcotest.(check string) 137 + "preferred-citation title" 138 + "Book Title" 139 + (Cff.Reference.title ref) 128 140 | None -> Alcotest.fail "Expected preferred-citation"); 129 - 130 141 (* Check references *) 131 142 (match Cff.references cff with 132 - | Some refs -> 133 - Alcotest.(check int) "references count" 1 (List.length refs) 143 + | Some refs -> Alcotest.(check int) "references count" 1 (List.length refs) 134 144 | None -> Alcotest.fail "Expected references") 135 - | Error e -> 136 - Alcotest.fail (Printf.sprintf "Failed to parse key-complete CFF: %s" e) 145 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse key-complete CFF: %s" e) 146 + ;; 137 147 138 148 (* All 1.2.0 pass fixtures *) 139 149 (* Note: reference-article is skipped due to Yamlt parser limitation with 140 150 multi-line quoted strings (see issue with indentation in quoted scalars) *) 141 - let pass_fixtures_1_2_0 = [ 142 - "bjmorgan/bsym"; 143 - "esalmela/haplowinder"; 144 - "key-complete"; 145 - "ls1mardyn/ls1-mardyn"; 146 - "minimal"; 147 - "poc"; 148 - "reference-art"; 149 - (* "reference-article"; -- skipped: Yamlt multi-line quoted string issue *) 150 - "reference-blog"; 151 - "reference-book"; 152 - "reference-conference-paper"; 153 - "reference-edited-work"; 154 - "reference-report"; 155 - "reference-thesis"; 156 - "short"; 157 - "simple"; 158 - "software-container"; 159 - "software-executable"; 160 - "software-with-a-doi"; 161 - "software-with-a-doi-expanded"; 162 - "software-without-a-doi"; 163 - "software-without-a-doi-closed-source"; 164 - "software-with-reference"; 165 - "tue-excellent-buildings/bso-toolbox"; 166 - "xenon-middleware_xenon-adaptors-cloud"; 167 - ] 151 + let pass_fixtures_1_2_0 = 152 + [ "bjmorgan/bsym" 153 + ; "esalmela/haplowinder" 154 + ; "key-complete" 155 + ; "ls1mardyn/ls1-mardyn" 156 + ; "minimal" 157 + ; "poc" 158 + ; "reference-art" 159 + ; (* "reference-article"; -- skipped: Yamlt multi-line quoted string issue *) 160 + "reference-blog" 161 + ; "reference-book" 162 + ; "reference-conference-paper" 163 + ; "reference-edited-work" 164 + ; "reference-report" 165 + ; "reference-thesis" 166 + ; "short" 167 + ; "simple" 168 + ; "software-container" 169 + ; "software-executable" 170 + ; "software-with-a-doi" 171 + ; "software-with-a-doi-expanded" 172 + ; "software-without-a-doi" 173 + ; "software-without-a-doi-closed-source" 174 + ; "software-with-reference" 175 + ; "tue-excellent-buildings/bso-toolbox" 176 + ; "xenon-middleware_xenon-adaptors-cloud" 177 + ] 178 + ;; 168 179 169 180 let make_fixture_test name = 170 181 let test_name = String.map (fun c -> if c = '/' then '-' else c) name in 171 182 let test () = 172 - let path = Printf.sprintf "../vendor/git/citation-file-format/examples/1.2.0/pass/%s/CITATION.cff" name in 183 + let path = 184 + Printf.sprintf 185 + "../vendor/git/citation-file-format/examples/1.2.0/pass/%s/CITATION.cff" 186 + name 187 + in 173 188 match Cff_unix.of_file path with 174 189 | Ok cff -> 175 190 (* Basic sanity checks that apply to all valid CFF files *) 176 191 Alcotest.(check string) "cff-version" "1.2.0" (Cff.cff_version cff); 177 192 Alcotest.(check bool) "has title" true (String.length (Cff.title cff) > 0); 178 193 Alcotest.(check bool) "has authors" true (List.length (Cff.authors cff) > 0) 179 - | Error e -> 180 - Alcotest.fail (Printf.sprintf "Failed to parse %s: %s" name e) 194 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse %s: %s" name e) 181 195 in 182 196 Alcotest.test_case test_name `Quick test 197 + ;; 183 198 184 199 (* License parsing tests *) 185 200 186 - let cff_with_single_license = {| 201 + let cff_with_single_license = 202 + {| 187 203 cff-version: 1.2.0 188 204 message: Please cite 189 205 title: Test ··· 191 207 - family-names: Test 192 208 license: MIT 193 209 |} 210 + ;; 194 211 195 - let cff_with_license_expression = {| 212 + let cff_with_license_expression = 213 + {| 196 214 cff-version: 1.2.0 197 215 message: Please cite 198 216 title: Test ··· 200 218 - family-names: Test 201 219 license: GPL-3.0-or-later WITH Classpath-exception-2.0 202 220 |} 221 + ;; 203 222 204 - let cff_with_license_array = {| 223 + let cff_with_license_array = 224 + {| 205 225 cff-version: 1.2.0 206 226 message: Please cite 207 227 title: Test ··· 211 231 - Apache-2.0 212 232 - MIT 213 233 |} 234 + ;; 214 235 215 - let cff_with_unknown_license = {| 236 + let cff_with_unknown_license = 237 + {| 216 238 cff-version: 1.2.0 217 239 message: Please cite 218 240 title: Test ··· 220 242 - family-names: Test 221 243 license: Some-Unknown-License-v1.0 222 244 |} 245 + ;; 223 246 224 - let cff_with_unknown_license_array = {| 247 + let cff_with_unknown_license_array = 248 + {| 225 249 cff-version: 1.2.0 226 250 message: Please cite 227 251 title: Test ··· 231 255 - MIT 232 256 - Not-A-Real-License 233 257 |} 258 + ;; 234 259 235 260 let test_license_single () = 236 261 match Cff_unix.of_yaml_string cff_with_single_license with 237 262 | Ok cff -> 238 263 (match Cff.license cff with 239 - | Some (`Expr (Spdx_licenses.Simple (Spdx_licenses.LicenseID "MIT"))) -> () 240 - | Some (`Expr _) -> Alcotest.fail "Expected simple MIT license" 241 - | Some (`Raw _) -> Alcotest.fail "License should have parsed as valid SPDX" 264 + | Some (`Spdx (Spdx_licenses.Simple (Spdx_licenses.LicenseID "MIT"))) -> () 265 + | Some (`Spdx _) -> Alcotest.fail "Expected simple MIT license" 266 + | Some (`Other _) -> Alcotest.fail "License should have parsed as valid SPDX" 242 267 | None -> Alcotest.fail "Missing license") 243 - | Error e -> 244 - Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 268 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 269 + ;; 245 270 246 271 let test_license_expression () = 247 272 match Cff_unix.of_yaml_string cff_with_license_expression with 248 273 | Ok cff -> 249 274 (match Cff.license cff with 250 - | Some (`Expr (Spdx_licenses.WITH _)) -> () 251 - | Some (`Expr _) -> Alcotest.fail "Expected WITH expression" 252 - | Some (`Raw _) -> Alcotest.fail "License should have parsed as valid SPDX" 275 + | Some (`Spdx (Spdx_licenses.WITH _)) -> () 276 + | Some (`Spdx _) -> Alcotest.fail "Expected WITH expression" 277 + | Some (`Other _) -> Alcotest.fail "License should have parsed as valid SPDX" 253 278 | None -> Alcotest.fail "Missing license") 254 - | Error e -> 255 - Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 279 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 280 + ;; 256 281 257 282 let test_license_array () = 258 283 match Cff_unix.of_yaml_string cff_with_license_array with 259 284 | Ok cff -> 260 285 (match Cff.license cff with 261 - | Some (`Expr (Spdx_licenses.OR _)) -> () 262 - | Some (`Expr _) -> Alcotest.fail "Expected OR expression" 263 - | Some (`Raw _) -> Alcotest.fail "License should have parsed as valid SPDX" 286 + | Some (`Spdx (Spdx_licenses.OR _)) -> () 287 + | Some (`Spdx _) -> Alcotest.fail "Expected OR expression" 288 + | Some (`Other _) -> Alcotest.fail "License should have parsed as valid SPDX" 264 289 | None -> Alcotest.fail "Missing license") 265 - | Error e -> 266 - Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 290 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 291 + ;; 267 292 268 293 let test_license_unknown () = 269 294 match Cff_unix.of_yaml_string cff_with_unknown_license with 270 295 | Ok cff -> 271 296 (match Cff.license cff with 272 - | Some (`Raw ["Some-Unknown-License-v1.0"]) -> () 273 - | Some (`Raw ss) -> 274 - Alcotest.fail (Printf.sprintf "Wrong raw value: [%s]" (String.concat "; " ss)) 275 - | Some (`Expr _) -> Alcotest.fail "Unknown license should be Raw, not Expr" 297 + | Some (`Other ([ "Some-Unknown-License-v1.0" ], None)) -> () 298 + | Some (`Other (ss, _)) -> 299 + Alcotest.fail (Printf.sprintf "Wrong value: [%s]" (String.concat "; " ss)) 300 + | Some (`Spdx _) -> Alcotest.fail "Unknown license should be Other, not Spdx" 276 301 | None -> Alcotest.fail "Missing license") 277 - | Error e -> 278 - Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 302 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 303 + ;; 279 304 280 305 let test_license_unknown_in_array () = 281 306 match Cff_unix.of_yaml_string cff_with_unknown_license_array with 282 307 | Ok cff -> 283 308 (match Cff.license cff with 284 - | Some (`Raw ["MIT"; "Not-A-Real-License"]) -> () 285 - | Some (`Raw ss) -> 286 - Alcotest.fail (Printf.sprintf "Wrong raw value: [%s]" (String.concat "; " ss)) 287 - | Some (`Expr _) -> Alcotest.fail "Array with unknown should be Raw" 309 + | Some (`Other ([ "MIT"; "Not-A-Real-License" ], None)) -> () 310 + | Some (`Other (ss, _)) -> 311 + Alcotest.fail (Printf.sprintf "Wrong value: [%s]" (String.concat "; " ss)) 312 + | Some (`Spdx _) -> Alcotest.fail "Array with unknown should be Other" 288 313 | None -> Alcotest.fail "Missing license") 289 - | Error e -> 290 - Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 314 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 315 + ;; 291 316 292 317 let test_license_unknown_roundtrip () = 293 318 match Cff_unix.of_yaml_string cff_with_unknown_license with 294 319 | Error e -> Alcotest.fail (Printf.sprintf "Failed to parse: %s" e) 295 320 | Ok cff1 -> 296 - match Cff_unix.to_yaml_string cff1 with 297 - | Error e -> Alcotest.fail (Printf.sprintf "Failed to encode: %s" e) 298 - | Ok yaml -> 299 - match Cff_unix.of_yaml_string yaml with 300 - | Error e -> Alcotest.fail (Printf.sprintf "Failed to reparse: %s" e) 301 - | Ok cff2 -> 302 - (match Cff.license cff2 with 303 - | Some (`Raw ["Some-Unknown-License-v1.0"]) -> () 304 - | Some (`Raw ss) -> 305 - Alcotest.fail (Printf.sprintf "Roundtrip changed value: [%s]" (String.concat "; " ss)) 306 - | Some (`Expr _) -> Alcotest.fail "Roundtrip changed Raw to Expr" 307 - | None -> Alcotest.fail "Roundtrip lost license") 321 + (match Cff_unix.to_yaml_string cff1 with 322 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to encode: %s" e) 323 + | Ok yaml -> 324 + (match Cff_unix.of_yaml_string yaml with 325 + | Error e -> Alcotest.fail (Printf.sprintf "Failed to reparse: %s" e) 326 + | Ok cff2 -> 327 + (match Cff.license cff2 with 328 + | Some (`Other ([ "Some-Unknown-License-v1.0" ], None)) -> () 329 + | Some (`Other (ss, _)) -> 330 + Alcotest.fail 331 + (Printf.sprintf "Roundtrip changed value: [%s]" (String.concat "; " ss)) 332 + | Some (`Spdx _) -> Alcotest.fail "Roundtrip changed Other to Spdx" 333 + | None -> Alcotest.fail "Roundtrip lost license"))) 334 + ;; 308 335 309 336 (* Test that we correctly reject or handle known-invalid files *) 310 337 let test_fail_invalid_date () = 311 - let path = "../vendor/git/citation-file-format/examples/1.2.0/fail/tue-excellent-buildings/bso-toolbox-invalid-date/CITATION.cff" in 338 + let path = 339 + "../vendor/git/citation-file-format/examples/1.2.0/fail/tue-excellent-buildings/bso-toolbox-invalid-date/CITATION.cff" 340 + in 312 341 match Cff_unix.of_file path with 313 342 | Ok _ -> 314 343 (* Our parser might be lenient - that's OK for now *) ··· 316 345 | Error _ -> 317 346 (* Expected to fail due to invalid date "2020-05-xx" *) 318 347 () 348 + ;; 319 349 320 350 (* Test fail fixture with additional key - should parse since we skip unknown *) 321 351 let test_fail_additional_key () = 322 - let path = "../vendor/git/citation-file-format/examples/1.2.0/fail/additional-key/CITATION.cff" in 352 + let path = 353 + "../vendor/git/citation-file-format/examples/1.2.0/fail/additional-key/CITATION.cff" 354 + in 323 355 match Cff_unix.of_file path with 324 356 | Ok cff -> 325 357 (* Our parser is lenient and skips unknown keys *) 326 358 Alcotest.(check string) "title" "My Research Tool" (Cff.title cff) 327 359 | Error e -> 328 360 Alcotest.fail (Printf.sprintf "Should parse with unknown keys skipped: %s" e) 361 + ;; 329 362 330 363 let () = 331 - Alcotest.run "CFF" [ 332 - "parsing", [ 333 - Alcotest.test_case "minimal" `Quick test_parse_minimal; 334 - Alcotest.test_case "simple" `Quick test_parse_simple; 335 - Alcotest.test_case "key-complete" `Quick test_parse_key_complete; 336 - ]; 337 - "creation", [ 338 - Alcotest.test_case "programmatic" `Quick test_create_programmatic; 339 - ]; 340 - "roundtrip", [ 341 - Alcotest.test_case "simple roundtrip" `Quick test_roundtrip; 342 - ]; 343 - "license", [ 344 - Alcotest.test_case "single license" `Quick test_license_single; 345 - Alcotest.test_case "license expression" `Quick test_license_expression; 346 - Alcotest.test_case "license array" `Quick test_license_array; 347 - Alcotest.test_case "unknown license" `Quick test_license_unknown; 348 - Alcotest.test_case "unknown in array" `Quick test_license_unknown_in_array; 349 - Alcotest.test_case "unknown roundtrip" `Quick test_license_unknown_roundtrip; 350 - ]; 351 - "1.2.0 fixtures", List.map make_fixture_test pass_fixtures_1_2_0; 352 - "fail fixtures", [ 353 - Alcotest.test_case "invalid-date" `Quick test_fail_invalid_date; 354 - Alcotest.test_case "additional-key" `Quick test_fail_additional_key; 355 - ]; 356 - ] 364 + Alcotest.run 365 + "CFF" 366 + [ ( "parsing" 367 + , [ Alcotest.test_case "minimal" `Quick test_parse_minimal 368 + ; Alcotest.test_case "simple" `Quick test_parse_simple 369 + ; Alcotest.test_case "key-complete" `Quick test_parse_key_complete 370 + ] ) 371 + ; "creation", [ Alcotest.test_case "programmatic" `Quick test_create_programmatic ] 372 + ; "roundtrip", [ Alcotest.test_case "simple roundtrip" `Quick test_roundtrip ] 373 + ; ( "license" 374 + , [ Alcotest.test_case "single license" `Quick test_license_single 375 + ; Alcotest.test_case "license expression" `Quick test_license_expression 376 + ; Alcotest.test_case "license array" `Quick test_license_array 377 + ; Alcotest.test_case "unknown license" `Quick test_license_unknown 378 + ; Alcotest.test_case "unknown in array" `Quick test_license_unknown_in_array 379 + ; Alcotest.test_case "unknown roundtrip" `Quick test_license_unknown_roundtrip 380 + ] ) 381 + ; "1.2.0 fixtures", List.map make_fixture_test pass_fixtures_1_2_0 382 + ; ( "fail fixtures" 383 + , [ Alcotest.test_case "invalid-date" `Quick test_fail_invalid_date 384 + ; Alcotest.test_case "additional-key" `Quick test_fail_additional_key 385 + ] ) 386 + ] 387 + ;;
+55 -24
test/test_cff_eio.ml
··· 1 1 (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 The ocaml-cff programmers. All rights reserved. 2 + Copyright (c) 2026 The ocaml-cff programmers. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 6 (* Test the CFF Eio backend *) 7 7 8 - let minimal_cff = {| 8 + let minimal_cff = 9 + {| 9 10 cff-version: 1.2.0 10 11 message: If you use this software, please cite it 11 12 title: Test Software ··· 13 14 - family-names: Smith 14 15 given-names: Jane 15 16 |} 17 + ;; 16 18 17 - let simple_cff = {| 19 + let simple_cff = 20 + {| 18 21 cff-version: 1.2.0 19 22 message: Please cite this software using these metadata. 20 23 title: My Research Software ··· 26 29 doi: 10.5281/zenodo.1234567 27 30 date-released: 2021-08-11 28 31 |} 32 + ;; 29 33 30 34 let test_parse_string () = 31 35 let cff = Cff_eio.of_yaml_string minimal_cff in 32 36 Alcotest.(check string) "cff-version" "1.2.0" (Cff.cff_version cff); 33 37 Alcotest.(check string) "title" "Test Software" (Cff.title cff); 34 38 Alcotest.(check int) "authors count" 1 (List.length (Cff.authors cff)) 39 + ;; 35 40 36 41 let test_roundtrip_string () = 37 42 let cff1 = Cff_eio.of_yaml_string simple_cff in 38 43 let yaml = Cff_eio.to_yaml_string cff1 in 39 44 let cff2 = Cff_eio.of_yaml_string yaml in 40 45 Alcotest.(check string) "title preserved" (Cff.title cff1) (Cff.title cff2); 41 - Alcotest.(check string) "cff-version preserved" (Cff.cff_version cff1) (Cff.cff_version cff2) 46 + Alcotest.(check string) 47 + "cff-version preserved" 48 + (Cff.cff_version cff1) 49 + (Cff.cff_version cff2) 50 + ;; 42 51 43 52 let test_parse_error () = 44 - let invalid_yaml = {| 53 + let invalid_yaml = 54 + {| 45 55 cff-version: 1.2.0 46 56 title: Missing authors field 47 - |} in 57 + |} 58 + in 48 59 match Cff_eio.of_yaml_string invalid_yaml with 49 60 | _ -> Alcotest.fail "Expected parse error" 50 61 | exception Eio.Exn.Io (Cff_eio.E _, _) -> () 51 - | exception ex -> Alcotest.fail (Printf.sprintf "Wrong exception type: %s" (Printexc.to_string ex)) 62 + | exception ex -> 63 + Alcotest.fail (Printf.sprintf "Wrong exception type: %s" (Printexc.to_string ex)) 64 + ;; 52 65 53 66 let test_file_read env = 54 67 let fs = Eio.Stdenv.fs env in 55 - let cff = Cff_eio.of_file ~fs "../vendor/git/citation-file-format/examples/1.2.0/pass/minimal/CITATION.cff" in 68 + let cff = 69 + Cff_eio.of_file 70 + ~fs 71 + "../vendor/git/citation-file-format/examples/1.2.0/pass/minimal/CITATION.cff" 72 + in 56 73 Alcotest.(check string) "cff-version" "1.2.0" (Cff.cff_version cff); 57 74 Alcotest.(check bool) "has title" true (String.length (Cff.title cff) > 0); 58 75 Alcotest.(check bool) "has authors" true (List.length (Cff.authors cff) > 0) 76 + ;; 59 77 60 78 let test_file_not_found env = 61 79 let fs = Eio.Stdenv.fs env in 62 80 match Cff_eio.of_file ~fs "nonexistent_file.cff" with 63 81 | _ -> Alcotest.fail "Expected file not found error" 64 82 | exception Eio.Exn.Io _ -> () 65 - | exception ex -> Alcotest.fail (Printf.sprintf "Wrong exception type: %s" (Printexc.to_string ex)) 83 + | exception ex -> 84 + Alcotest.fail (Printf.sprintf "Wrong exception type: %s" (Printexc.to_string ex)) 85 + ;; 66 86 67 87 let test_file_roundtrip env = 68 88 let fs = Eio.Stdenv.fs env in 69 - let cff1 = Cff_eio.of_file ~fs "../vendor/git/citation-file-format/examples/1.2.0/pass/simple/CITATION.cff" in 89 + let cff1 = 90 + Cff_eio.of_file 91 + ~fs 92 + "../vendor/git/citation-file-format/examples/1.2.0/pass/simple/CITATION.cff" 93 + in 70 94 let tmp_path = "_build/test_roundtrip.cff" in 71 95 Cff_eio.to_file ~fs tmp_path cff1; 72 96 let cff2 = Cff_eio.of_file ~fs tmp_path in 73 97 Alcotest.(check string) "title preserved" (Cff.title cff1) (Cff.title cff2); 74 - Alcotest.(check string) "cff-version preserved" (Cff.cff_version cff1) (Cff.cff_version cff2) 98 + Alcotest.(check string) 99 + "cff-version preserved" 100 + (Cff.cff_version cff1) 101 + (Cff.cff_version cff2) 102 + ;; 75 103 76 104 let () = 77 - Eio_main.run @@ fun env -> 78 - Alcotest.run "CFF Eio" [ 79 - "string parsing", [ 80 - Alcotest.test_case "parse string" `Quick test_parse_string; 81 - Alcotest.test_case "roundtrip string" `Quick test_roundtrip_string; 82 - Alcotest.test_case "parse error" `Quick test_parse_error; 83 - ]; 84 - "file operations", [ 85 - Alcotest.test_case "read file" `Quick (fun () -> test_file_read env); 86 - Alcotest.test_case "file not found" `Quick (fun () -> test_file_not_found env); 87 - Alcotest.test_case "file roundtrip" `Quick (fun () -> test_file_roundtrip env); 88 - ]; 89 - ] 105 + Eio_main.run 106 + @@ fun env -> 107 + Alcotest.run 108 + "CFF Eio" 109 + [ ( "string parsing" 110 + , [ Alcotest.test_case "parse string" `Quick test_parse_string 111 + ; Alcotest.test_case "roundtrip string" `Quick test_roundtrip_string 112 + ; Alcotest.test_case "parse error" `Quick test_parse_error 113 + ] ) 114 + ; ( "file operations" 115 + , [ Alcotest.test_case "read file" `Quick (fun () -> test_file_read env) 116 + ; Alcotest.test_case "file not found" `Quick (fun () -> test_file_not_found env) 117 + ; Alcotest.test_case "file roundtrip" `Quick (fun () -> test_file_roundtrip env) 118 + ] ) 119 + ] 120 + ;;