Skip to main content

acorn/schema/standard/
datacite.rs

1//! DataCite metadata schema models
2//!
3//! These types model the DataCite Metadata Schema 4.6 structure, including
4//! DOI records, creators, contributors, related identifiers, funding
5//! references, geolocation, rights, and other metadata properties
6#[cfg(feature = "std")]
7use crate::error::ApiResult;
8#[cfg(feature = "std")]
9use crate::io::License;
10#[cfg(feature = "std")]
11use crate::io::{read_file, write_file, InputOutput};
12use crate::prelude::*;
13use crate::schema::namespaces::DATACITE_IDENTIFIER_TYPE_CONTROLLED_VOCABULARY;
14use crate::schema::standard::crosswalk::mapping::{dcat_to_datacite, huwise_to_datacite, invenio_to_datacite};
15use crate::schema::standard::crosswalk::{self, CrosswalkError, FieldValue, Fields, SchemaBuilder, SchemaExtractor};
16use crate::schema::standard::{dcat, huwise, invenio};
17use crate::schema::validate::{is_doi, is_iso_639_1_language_code, is_latitude, is_longitude, is_polygon, is_rfc3339, is_semantic_version, is_year};
18#[cfg(not(feature = "std"))]
19use crate::util::License;
20#[cfg(feature = "std")]
21use crate::util::MimeType;
22use crate::util::ToProse;
23#[cfg(feature = "std")]
24use crate::PathBuf;
25#[cfg(feature = "std")]
26use color_eyre::eyre::eyre;
27use core::fmt;
28use schemars::JsonSchema;
29use serde::{Deserialize, Serialize};
30use serde_with::skip_serializing_none;
31use validator::Validate;
32
33/// Collection of DataCite DOI records
34pub type Catalog = Vec<Record>;
35/// Contributor type enumeration per DataCite 4.6 property 7.a
36///
37/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/contributorType/> for details
38#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
39pub enum ContributorType {
40    /// Person with knowledge of how to access, troubleshoot, or otherwise field issues related to the resource
41    ContactPerson,
42    /// Person/institution responsible for finding or gathering/collecting data under the guidelines of the author(s) or Principal Investigator (PI)
43    DataCollector,
44    /// Person tasked with reviewing, enhancing, cleaning, or standardizing metadata and the associated data submitted for storage, use, and maintenance within a data centre or repository
45    DataCurator,
46    /// Person or organization responsible for maintaining the finished resource
47    DataManager,
48    /// Institution tasked with responsibility to generate/disseminate copies of the resource in either electronic or print form
49    Distributor,
50    /// Person who oversees the details related to the publication format of the resource
51    Editor,
52    /// Typically, the organisation allowing the resource to be available on the internet through the provision of its hardware/software/operating support
53    HostingInstitution,
54    /// Typically, a person or organisation responsible for the artistry and form of a media product
55    Producer,
56    /// Person officially designated as head of project team or sub- project team instrumental in the work necessary to development of the resource
57    ProjectLeader,
58    /// Person officially designated as manager of a project
59    /// ### Note
60    /// > Project may consist of one or many project teams and sub-teams
61    ProjectManager,
62    /// Person on the membership list of a designated project/project team
63    ProjectMember,
64    /// Institution/organisation officially appointed by a Registration Authority to handle specific tasks within a defined area of responsibility
65    RegistrationAgency,
66    /// Standards-setting body from which Registration Agencies obtain official recognition and guidance
67    RegistrationAuthority,
68    /// Person without a specifically defined role in the development of the resource, but who is someone the author wishes to recognize
69    RelatedPerson,
70    /// Typically refers to a group of individuals with a lab, department, or division that has a specifically defined focus of activity
71    ResearchGroup,
72    /// Person involved in analysing data or the results of an experiment or formal study
73    /// ### Note
74    /// > May indicate an intern or assistant to one of the authors who helped with research but who was not so "key" as to be listed as an author
75    Researcher,
76    /// Person or institution owning or managing property rights, including intellectual property rights over the resource
77    RightsHolder,
78    /// Person or organisation that issued a contract or under the auspices of which a work has been written, printed, published, developed, etc.
79    Sponsor,
80    /// Designated administrator over one or more groups/teams working to produce a resource, or over one or more steps of a development process
81    Supervisor,
82    /// Person, organization, or automated system responsible for converting the content of a resource from one language into another, preserving its meaning and intended message
83    Translator,
84    /// Work package leader
85    WorkPackageLeader,
86    /// Any person or institution making a significant contribution to the development and/or maintenance of the resource, but whose contribution is not adequately described by any of the other values
87    /// ### Examples
88    /// - Photographer
89    /// - Artist
90    /// - Writer
91    Other,
92}
93/// Date type enumeration per DataCite 4.6 property 8.a
94///
95/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/dateType/> for details
96#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
97pub enum DateType {
98    /// Date that the publisher accepted the resource into their system
99    Accepted,
100    /// Date the resource is made publicly available (may be a range)
101    Available,
102    /// Date or date range in which the resource content was collected
103    Collected,
104    /// Specific, documented date at which the resource receives a copyrighted status, if applicable
105    Copyrighted,
106    /// Date or date range that the resource content applies to, describes, or covers
107    Coverage,
108    /// Date the resource itself was put together
109    Created,
110    /// Date that the resource is published or distributed
111    Issued,
112    /// Date the creator submits the resource to the publisher
113    Submitted,
114    /// Date the resource was last updated (may be a range)
115    Updated,
116    /// Date or date range during which the dataset or resource is accurate
117    Valid,
118    /// Date the resource is removed
119    Withdrawn,
120    /// Other date that does not fit into an existing category
121    Other,
122}
123/// Description type enumeration
124///
125/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/descriptionType/> for details
126#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
127pub enum DescriptionType {
128    /// Brief description of the resource and the context in which the resource was created
129    Abstract,
130    /// Methodology employed for the study or research
131    Methods,
132    /// Information about a repeating series, such as volume, issue, number
133    SeriesInformation,
134    /// Table of contents
135    TableOfContents,
136    /// Detailed information that may be associated with design, implementation, operation, use, and/or maintenance of a process, system, or instrument
137    TechnicalInfo,
138    /// Other
139    Other,
140}
141/// Funder identifier type enumeration per DataCite 4.6 property 19.2.a
142#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
143pub enum FunderIdentifierType {
144    /// Crossref Funder ID
145    #[serde(rename = "Crossref Funder ID")]
146    CrossrefFunderId,
147    /// Global Research Identifier Database ([GRID](https://www.grid.ac/))
148    /// ### Note
149    /// > GRID was retired in 2022 and replaced by ROR, but some funders may still use GRID identifiers
150    #[serde(rename = "GRID")]
151    Grid,
152    ///  International Standard Name Identifier ([ISNI](https://en.wikipedia.org/wiki/International_Standard_Name_Identifier))
153    #[serde(rename = "ISNI")]
154    Isni,
155    /// Research Organization Registry (see [`ROR`](crate::schema::pid::ROR))
156    #[serde(rename = "ROR")]
157    Ror,
158    /// Other
159    #[serde(rename = "Other")]
160    Other,
161}
162/// Name type enumeration per DataCite 4.6 property 7.1.a
163#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
164pub enum NameType {
165    /// Personal name
166    #[serde(rename = "Personal")]
167    Personal,
168    /// Organizational name
169    #[serde(rename = "Organizational")]
170    Organizational,
171}
172/// Related identifier type enumeration per DataCite 4.6 property 12.a
173///
174/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/relatedIdentifierType/> for details
175#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
176pub enum RelatedIdentifierType {
177    /// ARK
178    #[serde(rename = "ARK")]
179    Ark,
180    /// arXiv
181    #[serde(rename = "arXiv")]
182    Arxiv,
183    /// bibcode
184    #[serde(rename = "bibcode")]
185    Bibcode,
186    /// CSTR
187    #[serde(rename = "CSTR")]
188    Cstr,
189    /// DOI
190    #[serde(rename = "DOI")]
191    Doi,
192    /// EAN13
193    #[serde(rename = "EAN13")]
194    Ean13,
195    /// EISSN
196    #[serde(rename = "EISSN")]
197    Eissn,
198    /// Handle
199    #[serde(rename = "Handle")]
200    Handle,
201    /// IGSN
202    #[serde(rename = "IGSN")]
203    Igsn,
204    /// ISBN
205    #[serde(rename = "ISBN")]
206    Isbn,
207    /// ISSN
208    #[serde(rename = "ISSN")]
209    Issn,
210    /// ISTC
211    #[serde(rename = "ISTC")]
212    Istc,
213    /// LISSN
214    #[serde(rename = "LISSN")]
215    Lissn,
216    /// LSID
217    #[serde(rename = "LSID")]
218    Lsid,
219    /// PMID
220    #[serde(rename = "PMID")]
221    Pmid,
222    /// PURL
223    #[serde(rename = "PURL")]
224    Purl,
225    /// RRID
226    #[serde(rename = "RRID")]
227    Rrid,
228    /// UPC
229    #[serde(rename = "UPC")]
230    Upc,
231    /// URL
232    #[serde(rename = "URL")]
233    Url,
234    /// URN
235    #[serde(rename = "URN")]
236    Urn,
237    /// w3id
238    #[serde(rename = "w3id")]
239    W3id,
240}
241/// Relation type enumeration per DataCite 4.6 property 12.b
242///
243/// Description of the relationship of the resource being registered (A) and the related resource (B)
244///
245/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/relationType/> for details
246#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
247pub enum RelationType {
248    /// Indicates that A includes B in a citation
249    Cites,
250    /// Collects
251    Collects,
252    /// Compiles
253    Compiles,
254    /// Continues
255    Continues,
256    /// Describes
257    Describes,
258    /// Documents
259    Documents,
260    /// Has metadata
261    HasMetadata,
262    /// Has part
263    HasPart,
264    /// Has translation
265    HasTranslation,
266    /// Has version
267    HasVersion,
268    /// Indicates that B includes A in a citation
269    IsCitedBy,
270    /// Indicates A is collected by B
271    IsCollectedBy,
272    /// Indicates B is used to compile or create A
273    IsCompiledBy,
274    /// Is continued by
275    IsContinuedBy,
276    /// Is derived from
277    IsDerivedFrom,
278    /// Is described by
279    IsDescribedBy,
280    /// Is documented by
281    IsDocumentedBy,
282    /// Is identical to
283    IsIdenticalTo,
284    /// Is metadata for
285    IsMetadataFor,
286    /// Is new version of
287    IsNewVersionOf,
288    /// Is obsoleted by
289    IsObsoletedBy,
290    /// Is original form of
291    IsOriginalFormOf,
292    /// Is part of
293    IsPartOf,
294    /// Is previous version of
295    IsPreviousVersionOf,
296    /// Is published in
297    IsPublishedIn,
298    /// Is referenced by
299    IsReferencedBy,
300    /// Is required by
301    IsRequiredBy,
302    /// Is reviewed by
303    IsReviewedBy,
304    /// Is source of
305    IsSourceOf,
306    /// Is supplement to
307    IsSupplementTo,
308    /// Is supplemented by
309    IsSupplementedBy,
310    /// Is translation of
311    IsTranslationOf,
312    /// Is variant form of
313    IsVariantFormOf,
314    /// Is version of
315    IsVersionOf,
316    /// Obsoletes
317    Obsoletes,
318    /// References
319    References,
320    /// Requires
321    Requires,
322    /// Reviews
323    Reviews,
324}
325/// General resource type enumeration per DataCite 4.6 property 10.a
326#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
327pub enum ResourceTypeGeneral {
328    /// Series of visual representations imparting an impression of motion when shown in succession (may or may not include sound)
329    /// ### Dublin Core equivalent
330    /// > `MovingImage`
331    Audiovisual,
332    /// Umbrella term for resources provided to individual(s) or organization(s) in support of research, academic output, or training, such as a specific instance of funding, grant, investment, sponsorship, scholarship, recognition, or non-monetary materials
333    Award,
334    /// Medium for recording information in the form of writing or images, typically composed of many pages bound together and protected by a cover
335    /// ### Dublin Core equivalent
336    /// > `Text`
337    Book,
338    /// One of the main divisions of a book
339    /// ### Dublin Core equivalent
340    /// > `Text`
341    BookChapter,
342    /// An aggregation of resources, which may encompass collections of one resourceType as well as those of mixed types
343    /// ### Dublin Core equivalent
344    /// > `Collection`
345    Collection,
346    /// Virtual notebook environment used for literate programming
347    /// ### Dublin Core equivalent
348    /// > `InteractiveResource`
349    ComputationalNotebook,
350    /// Article that is written with the goal of being accepted to a conference
351    /// ### Dublin Core equivalent
352    /// > `Text`
353    ConferencePaper,
354    /// Collection of academic papers published in the context of an academic conference
355    /// ### Dublin Core equivalent
356    /// > `Text`
357    ConferenceProceeding,
358    /// Factual and objective publication with a focused intent to identify and describe specific data, sets of data, or data collections to facilitate discoverability
359    /// ### Dublin Core equivalent
360    /// > `Text`
361    DataPaper,
362    /// Data encoded in a defined structure
363    /// ### Dublin Core equivalent
364    /// > `Dataset`
365    Dataset,
366    /// Written essay, treatise, or thesis, especially one written by a candidate for the degree of Doctor of Philosophy
367    /// ### Dublin Core equivalent
368    /// > `Text`
369    Dissertation,
370    /// Non-persistent, time-based occurrence
371    /// ### Dublin Core equivalent
372    /// > `Event`
373    Event,
374    /// Visual representation other than text
375    /// ### Dublin Core equivalent
376    /// > `Image`
377    Image,
378    /// Resource requiring interaction from the user to be understood, executed, or experienced
379    /// ### Dublin Core equivalent
380    /// > `InteractiveResource`
381    InteractiveResource,
382    /// Device, tool or apparatus used to obtain, measure and/or analyze data
383    Instrument,
384    /// Scholarly publication consisting of articles that is published regularly throughout the year
385    /// ### Dublin Core equivalent
386    /// > `Text`
387    Journal,
388    /// Written composition on a topic of interest, which forms a separate part of a journal
389    /// ### Dublin Core equivalent
390    /// > `Text`
391    JournalArticle,
392    /// Abstract, conceptual, graphical, mathematical or visualization model that represents empirical objects, phenomena, or physical processes
393    Model,
394    /// Formal document that outlines how research outputs are to be handled both during a research project and after the project is completed
395    /// ### Dublin Core equivalent
396    /// > `Text`
397    OutputManagementPlan,
398    /// Evaluation of scientific, academic, or professional work by others working in the same field
399    /// ### Dublin Core equivalent
400    /// > `Text`
401    PeerReview,
402    /// Physical object or substance
403    /// ### Dublin Core equivalent
404    /// > `PhysicalObject`
405    PhysicalObject,
406    /// Preprint
407    /// ### Dublin Core equivalent
408    /// > `Text`
409    Preprint,
410    /// Planned endeavor or activity, frequently collaborative, intended to achieve a particular aim using allocated resources such as budget, time, and expertise
411    Project,
412    /// Report
413    /// ### Dublin Core equivalent
414    /// > `Text`
415    Report,
416    /// Service
417    /// ### Dublin Core equivalent
418    /// > `Service`
419    Service,
420    /// Software
421    /// ### Dublin Core equivalent
422    /// > `Software`
423    Software,
424    /// Resource primarily intended to be heard
425    /// ### Dublin Core equivalent
426    /// > `Sound`
427    Sound,
428    /// Something established by authority, custom, or general consent as a model, example, or point of reference
429    /// ### Dublin Core equivalent
430    /// > `Text`
431    Standard,
432    /// Ddetailed, time-stamped description of a research plan, often openly shared in a registry or published in a journal before the study is conducted to lend accountability and transparency in the hypothesis generating and testing process
433    /// ### Examples
434    /// - [OSF Registries](https://osf.io/registries)
435    /// - [ClinicalTrials.gov](https://clinicaltrials.gov/)
436    /// ### Dublin Core equivalent
437    /// > `Text`
438    StudyRegistration,
439    /// Resource consisting primarily of words for reading that is not covered by any other textual resource type in this list
440    /// ### Dublin Core equivalent
441    /// > `Text`
442    Text,
443    /// Structured series of steps which can be executed to produce a final outcome, allowing users a means to specify and enact their work in a more reproducible manner
444    Workflow,
445    /// Other
446    Other,
447}
448/// Title type enumeration per DataCite 4.6 property 3.a
449/// ### Note
450/// > The titleType subproperty is used when more than a single title is provided. Unless otherwise indicated by titleType, a title is considered to be the main title
451#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
452pub enum TitleType {
453    /// Alternative title
454    AlternativeTitle,
455    /// Subtitle
456    Subtitle,
457    /// Translated title
458    TranslatedTitle,
459    /// Other
460    Other,
461}
462/// Creator or contributor affiliation per DataCite 4.6 property 2.5
463#[skip_serializing_none]
464#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
465pub struct Affiliation {
466    /// Affiliation name
467    pub name: String,
468    /// Affiliation identifier
469    #[serde(rename = "affiliationIdentifier")]
470    pub affiliation_identifier: Option<String>,
471    /// Affiliation identifier scheme (ex. "ROR", "GRID", "ISNI")
472    #[serde(rename = "affiliationIdentifierScheme")]
473    pub affiliation_identifier_scheme: Option<String>,
474    /// Scheme URI
475    #[validate(url)]
476    #[serde(rename = "schemeURI")]
477    pub scheme_uri: Option<String>,
478}
479/// Alternate identifier per DataCite 4.6 property 11
480///
481/// An identifier other than the primary Identifier applied to the resource being registered.
482///
483/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/alternateidentifier/> for details
484#[skip_serializing_none]
485#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
486pub struct AlternateIdentifier {
487    /// Alternate identifier value (free text)
488    #[serde(rename = "alternateIdentifier")]
489    pub alternate_identifier: String,
490    /// Alternate identifier type (free text)
491    #[serde(rename = "alternateIdentifierType")]
492    pub alternate_identifier_type: String,
493}
494/// Record attributes containing all DataCite metadata properties
495///
496/// Should be an additional identifier for the same instance of the resource (i.e., same location, same file)
497#[skip_serializing_none]
498#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
499pub struct Attributes {
500    /// DOI string (property 1)
501    #[validate(custom(function = "is_doi"))]
502    pub doi: String,
503    /// Publication event type
504    pub event: Option<String>,
505    /// Dataset titles (property 3)
506    pub titles: Option<Vec<Title>>,
507    /// Dataset creators (property 2)
508    pub creators: Option<Vec<Creator>>,
509    /// Publisher information (property 4)
510    pub publisher: Option<Publisher>,
511    /// Publication year per DataCite 4.6 property 5
512    ///
513    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/publicationyear/> for details
514    #[validate(custom(function = "is_year"))]
515    #[serde(rename = "publicationYear")]
516    pub publication_year: Option<i32>,
517    /// Resource types (property 10)
518    #[serde(rename = "types")]
519    pub resource_types: Option<ResourceTypes>,
520    /// URL to the resource
521    #[validate(url)]
522    pub url: Option<String>,
523    /// Subject keywords (property 6)
524    #[validate(nested)]
525    pub subjects: Option<Vec<Subject>>,
526    /// Contributors (property 7)
527    #[validate(nested)]
528    pub contributors: Option<Vec<Contributor>>,
529    /// Dates (property 8)
530    #[validate(nested)]
531    pub dates: Option<Vec<Date>>,
532    /// Language code (property 9)
533    #[validate(custom(function = "is_iso_639_1_language_code"))]
534    pub language: Option<String>,
535    /// Alternate identifiers (property 11)
536    #[serde(rename = "alternateIdentifiers")]
537    pub alternate_identifiers: Option<Vec<AlternateIdentifier>>,
538    /// Related identifiers (property 12)
539    #[validate(nested)]
540    #[serde(rename = "relatedIdentifiers")]
541    pub related_identifiers: Option<Vec<RelatedIdentifier>>,
542    /// Sizes (property 13)
543    /// ### Examples
544    /// - "15 pages"
545    /// - "6 MB"
546    /// - "45 minutes"
547    pub sizes: Option<Vec<String>>,
548    /// Formats (property 14)
549    /// ### Note
550    /// > Use file extension or MIME type where possible, e.g., PDF, XML, MPG or application/pdf, text/xml, video/mpeg
551    pub formats: Option<Vec<String>>,
552    /// Version (property 15)
553    #[validate(custom(function = "is_semantic_version"))]
554    pub version: Option<String>,
555    /// Rights list (property 16)
556    #[validate(nested)]
557    #[serde(rename = "rightsList")]
558    pub rights_list: Option<Vec<Rights>>,
559    /// Descriptions of the resource (property 17)
560    #[validate(nested)]
561    pub descriptions: Option<Vec<Description>>,
562    /// Geographic locations (property 18)
563    #[validate(nested)]
564    #[serde(rename = "geoLocations")]
565    pub geo_locations: Option<Vec<GeoLocation>>,
566    /// Funding references (property 19)
567    #[validate(nested)]
568    #[serde(rename = "fundingReferences")]
569    pub funding_references: Option<Vec<FundingReference>>,
570    /// DataCite schema version
571    #[validate(url)]
572    #[serde(rename = "schemaVersion")]
573    pub schema_version: Option<String>,
574}
575/// Award number element with URI attribute (property 19.3)
576///
577/// In kernel-4 XML, the award URI is an attribute of the
578/// `<awardNumber>` element rather than a sibling field.
579#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
580pub struct AwardNumber {
581    /// Award URI
582    #[validate(url)]
583    #[serde(rename = "@awardURI")]
584    pub award_uri: Option<String>,
585    /// Award number value
586    #[serde(rename = "$text")]
587    pub value: String,
588}
589/// Contributor per DataCite 4.6 property 7
590///
591/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/contributor/> for details
592#[skip_serializing_none]
593#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
594pub struct Contributor {
595    /// Contributor name
596    pub name: String,
597    /// Contributor type
598    #[serde(rename = "contributorType")]
599    pub contributor_type: ContributorType,
600    /// Name type
601    #[serde(rename = "nameType")]
602    pub name_type: Option<NameType>,
603    /// Given name
604    #[serde(rename = "givenName")]
605    pub given_name: Option<String>,
606    /// Family name
607    #[serde(rename = "familyName")]
608    pub family_name: Option<String>,
609    /// Name identifiers
610    #[validate(nested)]
611    #[serde(rename = "nameIdentifiers")]
612    pub name_identifiers: Option<Vec<NameIdentifier>>,
613    /// Contributor affiliations
614    #[validate(nested)]
615    pub affiliation: Option<Vec<Affiliation>>,
616}
617/// Creator or author information per DataCite 4.6 property 2
618///
619/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/creator/> for details
620#[skip_serializing_none]
621#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
622pub struct Creator {
623    /// Creator name
624    #[serde(alias = "creatorName")]
625    pub name: String,
626    /// Name type
627    #[serde(rename = "nameType")]
628    pub name_type: Option<NameType>,
629    /// Given name
630    #[serde(rename = "givenName")]
631    pub given_name: Option<String>,
632    /// Family name
633    #[serde(rename = "familyName")]
634    pub family_name: Option<String>,
635    /// Name identifiers
636    #[serde(rename = "nameIdentifiers", alias = "nameIdentifier")]
637    pub name_identifiers: Option<Vec<NameIdentifier>>,
638    /// Creator affiliations
639    #[validate(nested)]
640    pub affiliation: Option<Vec<Affiliation>>,
641}
642/// Container for `<creators>` XML element
643#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
644pub struct Creators {
645    /// Creator list
646    #[validate(nested)]
647    pub creator: Vec<Creator>,
648}
649/// Date per DataCite 4.6 property 8
650///
651/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/date/> for details
652#[skip_serializing_none]
653#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
654pub struct Date {
655    /// Date value (YYYY, YYYY-MM-DD, or date range)
656    #[validate(custom(function = "is_rfc3339"))]
657    #[serde(alias = "$text")]
658    pub date: String,
659    /// Date type
660    #[serde(rename = "dateType", alias = "@dateType")]
661    pub date_type: DateType,
662    /// Additional date information
663    #[serde(rename = "dateInformation", alias = "@dateInformation")]
664    pub date_information: Option<String>,
665}
666/// Container for `<dates>` XML element
667#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
668pub struct Dates {
669    /// Date list
670    #[validate(nested)]
671    pub date: Vec<Date>,
672}
673/// Resource description per DataCite 4.6 property 17
674///
675/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/description/> for details
676#[skip_serializing_none]
677#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
678pub struct Description {
679    /// Description text
680    // TODO: Add prose validation
681    #[serde(alias = "$text")]
682    pub description: String,
683    /// Description type
684    #[serde(rename = "descriptionType", alias = "@descriptionType")]
685    pub description_type: Option<DescriptionType>,
686    /// Description language
687    #[validate(custom(function = "is_iso_639_1_language_code"))]
688    pub language: Option<String>,
689}
690/// Container for `<descriptions>` XML element
691#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
692pub struct Descriptions {
693    /// Description list
694    #[validate(nested)]
695    pub description: Vec<Description>,
696}
697/// Container for `<formats>` XML element
698#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
699pub struct Formats {
700    /// Format list per DataCite 4.6 property 14
701    ///
702    /// Use file extension or MIME type where possible, e.g., PDF, XML, MPG or application/pdf, text/xml, video/mpeg. Free text.
703    ///
704    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/format/> for details
705    pub format: Vec<String>,
706}
707/// Funder identifier element with typed attribute (property 19.2)
708///
709/// In kernel-4 XML, the funder identifier type is an attribute of the
710/// `<funderIdentifier>` element rather than a sibling field.
711#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
712pub struct FunderIdentifier {
713    /// Funder identifier type
714    #[serde(rename = "@funderIdentifierType")]
715    pub funder_identifier_type: Option<FunderIdentifierType>,
716    /// Funder identifier value
717    #[serde(rename = "$text")]
718    pub value: String,
719}
720/// Funding reference per DataCite 4.6 property 19
721///
722/// Information about financial support (funding) for the resource being registered.
723///
724/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/fundingreference/> for details
725#[skip_serializing_none]
726#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
727pub struct FundingReference {
728    /// Funder name
729    #[serde(rename = "funderName")]
730    pub funder_name: String,
731    /// Funder identifier
732    #[serde(rename = "funderIdentifier")]
733    pub funder_identifier: Option<String>,
734    /// Funder identifier type
735    #[serde(rename = "funderIdentifierType")]
736    pub funder_identifier_type: Option<FunderIdentifierType>,
737    /// Funder identifier scheme URI
738    #[validate(url)]
739    #[serde(rename = "schemeURI")]
740    pub scheme_uri: Option<String>,
741    /// Award number
742    #[serde(rename = "awardNumber")]
743    pub award_number: Option<String>,
744    /// Award URI
745    #[validate(url)]
746    #[serde(rename = "awardURI")]
747    pub award_uri: Option<String>,
748    /// Award title
749    #[serde(rename = "awardTitle")]
750    pub award_title: Option<String>,
751}
752/// Container for `<fundingReferences>` XML element
753#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
754pub struct FundingReferences {
755    /// Funding reference list
756    #[validate(nested)]
757    #[serde(rename = "fundingReference")]
758    pub funding_reference: Vec<KernelFundingReference>,
759}
760/// Geographic location per DataCite 4.6 property 18
761///
762/// Spatial region or named place where the data was gathered or about which the data is focused.
763///
764/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/geolocation/> for details
765#[skip_serializing_none]
766#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
767pub struct GeoLocation {
768    /// Point location
769    #[validate(nested)]
770    #[serde(rename = "geoLocationPoint")]
771    pub geo_location_point: Option<GeoLocationPoint>,
772    /// Bounding box
773    #[validate(nested)]
774    #[serde(rename = "geoLocationBox")]
775    pub geo_location_box: Option<GeoLocationBox>,
776    /// Place name
777    #[serde(rename = "geoLocationPlace")]
778    pub geo_location_place: Option<String>,
779    /// Polygon area
780    // TODO: Add custom validation function for checking inPolygonPoint is "in" polygon
781    #[validate(nested)]
782    #[serde(rename = "geoLocationPolygon")]
783    pub geo_location_polygon: Option<GeoLocationPolygon>,
784}
785/// Geographic bounding box per DataCite 4.6 property 18.2
786#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
787pub struct GeoLocationBox {
788    /// Western longitude
789    #[validate(custom(function = "is_longitude"))]
790    #[serde(rename = "westBoundLongitude")]
791    pub west_bound_longitude: f64,
792    /// Eastern longitude
793    #[validate(custom(function = "is_longitude"))]
794    #[serde(rename = "eastBoundLongitude")]
795    pub east_bound_longitude: f64,
796    /// Southern latitude
797    #[validate(custom(function = "is_latitude"))]
798    #[serde(rename = "southBoundLatitude")]
799    pub south_bound_latitude: f64,
800    /// Northern latitude
801    #[validate(custom(function = "is_latitude"))]
802    #[serde(rename = "northBoundLatitude")]
803    pub north_bound_latitude: f64,
804}
805/// Geographic point per DataCite 4.6 property 18.1
806#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
807pub struct GeoLocationPoint {
808    /// Longitude (-180 to 180)
809    #[validate(custom(function = "is_longitude"))]
810    #[serde(rename = "pointLongitude")]
811    pub longitude: f64,
812    /// Latitude (-90 to 90)
813    #[validate(custom(function = "is_latitude"))]
814    #[serde(rename = "pointLatitude")]
815    pub latitude: f64,
816}
817/// Geographic polygon per DataCite 4.6 property 18.4
818#[skip_serializing_none]
819#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
820pub struct GeoLocationPolygon {
821    /// Polygon points (last must equal first)
822    #[validate(nested, length(min = 4), custom(function = "is_polygon"))]
823    #[serde(rename = "polygonPoints", alias = "polygonPoint")]
824    pub polygon_points: Vec<GeoLocationPoint>,
825    /// Interior point for polygons larger than half the earth
826    #[validate(nested)]
827    #[serde(rename = "inPolygonPoint")]
828    pub in_polygon_point: Option<GeoLocationPoint>,
829}
830/// Container for `<geoLocations>` XML element
831#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
832pub struct GeoLocations {
833    /// Geo location list
834    #[validate(nested)]
835    #[serde(rename = "geoLocation")]
836    pub geo_location: Vec<GeoLocation>,
837}
838/// DataCite kernel-4 XML resource identifier per DataCite 4.6 property 1
839///
840/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/identifier/> for details
841#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
842pub struct Identifier {
843    /// Identifier type (e.g., "DOI")
844    #[serde(default = "default_identifier_type", rename = "@identifierType")]
845    pub identifier_type: String,
846    /// Identifier value
847    #[validate(custom(function = "is_doi"))]
848    #[serde(rename = "$text")]
849    pub value: String,
850}
851/// Funding reference within a DataCite kernel-4 XML resource (property 19)
852///
853/// This type models the hierarchical XML structure where `funderIdentifier`
854/// and `awardNumber` contain attributes, unlike the flat JSON API format
855/// modeled by [`FundingReference`].
856#[skip_serializing_none]
857#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
858pub struct KernelFundingReference {
859    /// Funder name
860    #[serde(rename = "funderName")]
861    pub funder_name: String,
862    /// Funder identifier with type attribute
863    #[serde(rename = "funderIdentifier")]
864    pub funder_identifier: Option<FunderIdentifier>,
865    /// Award number with URI attribute
866    #[validate(nested)]
867    #[serde(rename = "awardNumber")]
868    pub award_number: Option<AwardNumber>,
869    /// Award title
870    #[serde(rename = "awardTitle")]
871    pub award_title: Option<String>,
872}
873/// Persistent identifier for a creator or contributor
874#[skip_serializing_none]
875#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
876pub struct NameIdentifier {
877    /// Identifier value
878    #[serde(rename = "nameIdentifier", alias = "$text")]
879    pub name_identifier: String,
880    /// Identifier scheme
881    #[serde(rename = "nameIdentifierScheme", alias = "@nameIdentifierScheme")]
882    pub name_identifier_scheme: Option<String>,
883    /// Scheme URI
884    #[validate(url)]
885    #[serde(rename = "schemeUri", alias = "@schemeURI")]
886    pub scheme_uri: Option<String>,
887}
888/// Publisher information per DataCite 4.6 property 4
889///
890/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/publisher/> for details
891#[skip_serializing_none]
892#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
893pub struct Publisher {
894    /// Publisher name
895    pub name: String,
896    /// Publisher identifier
897    #[serde(rename = "publisherIdentifier")]
898    pub publisher_identifier: Option<String>,
899    /// Publisher identifier scheme
900    #[serde(rename = "publisherIdentifierScheme")]
901    pub publisher_identifier_scheme: Option<String>,
902    /// Scheme URI
903    #[validate(url)]
904    #[serde(rename = "schemeURI")]
905    pub scheme_uri: Option<String>,
906}
907/// Top-level DataCite DOI record
908#[skip_serializing_none]
909#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
910pub struct Record {
911    /// Unique DOI identifier
912    #[validate(custom(function = "is_doi"))]
913    pub id: String,
914    /// Record type (typically "dois")
915    #[serde(rename = "type")]
916    pub kind: String,
917    /// Record attributes containing metadata
918    #[validate(nested)]
919    pub attributes: Attributes,
920}
921/// Related identifier per DataCite 4.6 property 12
922///
923/// Identifiers of related resources. These must be globally unique identifiers.
924///
925/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/relatedidentifier/> for details
926#[skip_serializing_none]
927#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
928pub struct RelatedIdentifier {
929    /// Related identifier value
930    #[serde(rename = "relatedIdentifier", alias = "$text")]
931    pub related_identifier: String,
932    /// Related identifier type
933    #[serde(rename = "relatedIdentifierType", alias = "@relatedIdentifierType")]
934    pub related_identifier_type: Option<RelatedIdentifierType>,
935    /// Relation type
936    #[serde(rename = "relationType", alias = "@relationType")]
937    pub relation_type: Option<RelationType>,
938    /// Related metadata scheme
939    #[serde(rename = "relatedMetadataScheme")]
940    pub related_metadata_scheme: Option<String>,
941    /// Scheme URI
942    #[validate(url)]
943    #[serde(rename = "schemeURI")]
944    pub scheme_uri: Option<String>,
945    /// Scheme type
946    #[serde(rename = "schemeType")]
947    pub scheme_type: Option<String>,
948    /// Resource type of the related identifier
949    #[serde(rename = "resourceTypeGeneral")]
950    pub resource_type_general: Option<ResourceTypeGeneral>,
951}
952/// Container for `<relatedIdentifiers>` XML element
953#[skip_serializing_none]
954#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
955pub struct RelatedIdentifiers {
956    /// Related identifier list
957    #[validate(nested)]
958    #[serde(rename = "relatedIdentifier")]
959    pub related_identifier: Vec<RelatedIdentifier>,
960}
961/// Represents the top-level `<resource>` element in DataCite kernel-4 XML.
962/// Reuses existing leaf types ([`Creator`], [`GeoLocation`], [`Subject`], etc.)
963/// with container wrappers for the XML list-element pattern.
964#[skip_serializing_none]
965#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
966pub struct Resource {
967    /// Resource identifier (DataCite 4.6 property 1)
968    #[validate(nested)]
969    pub identifier: Identifier,
970    /// Creators (DataCite 4.6 property 2)
971    #[validate(nested)]
972    pub creators: Creators,
973    /// Titles (DataCite 4.6 property 3)
974    #[validate(nested)]
975    pub titles: Titles,
976    /// Publisher name (DataCite 4.6 property 4)
977    pub publisher: String,
978    /// Publication year per DataCite 4.6 property 5
979    ///
980    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/publicationyear/> for details
981    #[validate(custom(function = "is_year"))]
982    #[serde(rename = "publicationYear")]
983    pub publication_year: u32,
984    /// Resource type (DataCite 4.6 property 10)
985    #[serde(rename = "resourceType")]
986    pub resource_type: ResourceTypes,
987    /// Subjects (DataCite 4.6 property 6)
988    #[validate(nested)]
989    pub subjects: Option<Subjects>,
990    /// Dates (DataCite 4.6 property 8)
991    #[validate(nested)]
992    pub dates: Option<Dates>,
993    /// Language code per DataCite 4.6 property 9
994    ///
995    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/language/> for details
996    #[validate(custom(function = "is_iso_639_1_language_code"))]
997    pub language: Option<String>,
998    /// Related identifiers (DataCite 4.6 property 12)
999    #[serde(rename = "relatedIdentifiers")]
1000    pub related_identifiers: Option<RelatedIdentifiers>,
1001    /// Sizes (DataCite 4.6 property 13)
1002    #[validate(nested)]
1003    pub sizes: Option<Sizes>,
1004    /// Formats (DataCite 4.6 property 14)
1005    #[validate(nested)]
1006    pub formats: Option<Formats>,
1007    /// Version per DataCite 4.6 property 15
1008    ///
1009    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/version/> for details
1010    #[validate(custom(function = "is_semantic_version"))]
1011    pub version: Option<String>,
1012    /// Rights list (DataCite 4.6 property 16)
1013    #[validate(nested)]
1014    #[serde(rename = "rightsList")]
1015    pub rights_list: Option<RightsList>,
1016    /// Descriptions (DataCite 4.6 property 17)
1017    #[validate(nested)]
1018    pub descriptions: Option<Descriptions>,
1019    /// Geographic locations (DataCite 4.6 property 18)
1020    #[validate(nested)]
1021    #[serde(rename = "geoLocations")]
1022    pub geo_locations: Option<GeoLocations>,
1023    /// Funding references (DataCite 4.6 property 19)
1024    #[validate(nested)]
1025    #[serde(rename = "fundingReferences")]
1026    pub funding_references: Option<FundingReferences>,
1027}
1028/// Resource type information per DataCite 4.6 property 10
1029///
1030/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/resourcetype/> for details
1031#[skip_serializing_none]
1032#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1033pub struct ResourceTypes {
1034    /// General resource type
1035    #[serde(rename = "resourceTypeGeneral", alias = "@resourceTypeGeneral")]
1036    pub resource_type_general: Option<ResourceTypeGeneral>,
1037    /// Specific resource type
1038    #[serde(rename = "resourceType", alias = "$text")]
1039    pub resource_type: Option<String>,
1040}
1041/// Rights information per DataCite 4.6 property 16
1042///
1043/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/rights/> for details
1044#[skip_serializing_none]
1045#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1046pub struct Rights {
1047    /// Rights statement
1048    #[serde(alias = "$text")]
1049    pub rights: Option<String>,
1050    /// Rights URI
1051    #[validate(url)]
1052    #[serde(rename = "rightsURI", alias = "@rightsURI")]
1053    pub rights_uri: Option<String>,
1054    /// Rights identifier (e.g., CC-BY-4.0)
1055    #[validate(nested)]
1056    #[serde(rename = "rightsIdentifier", alias = "@rightsIdentifier")]
1057    pub rights_identifier: Option<License>,
1058    /// Rights identifier scheme (e.g., SPDX)
1059    #[serde(rename = "rightsIdentifierScheme", alias = "@rightsIdentifierScheme")]
1060    pub rights_identifier_scheme: Option<String>,
1061    /// Scheme URI
1062    #[validate(url)]
1063    #[serde(rename = "schemeURI", alias = "@schemeURI")]
1064    pub scheme_uri: Option<String>,
1065}
1066/// Container for `<rightsList>` XML element
1067#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1068pub struct RightsList {
1069    /// Rights list
1070    #[validate(nested)]
1071    pub rights: Vec<Rights>,
1072}
1073/// Container for `<sizes>` XML element
1074#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1075pub struct Sizes {
1076    /// Size list per DataCite 4.6 property 13
1077    ///
1078    /// Size (e.g., bytes, pages, inches, etc.) or duration (extent), e.g., hours, minutes, days, etc., of a resource. Free text.
1079    ///
1080    /// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/size/> for details
1081    pub size: Vec<String>,
1082}
1083/// Subject keyword per DataCite 4.6 property 6
1084///
1085/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/subject/> for details
1086#[skip_serializing_none]
1087#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1088pub struct Subject {
1089    /// Subject keyword
1090    #[serde(alias = "$text")]
1091    pub subject: String,
1092    /// Subject language
1093    #[validate(custom(function = "is_iso_639_1_language_code"))]
1094    pub language: Option<String>,
1095    /// Subject scheme
1096    #[serde(rename = "subjectScheme", alias = "@subjectScheme")]
1097    pub subject_scheme: Option<String>,
1098    /// Scheme URI
1099    #[validate(url)]
1100    #[serde(rename = "schemeURI", alias = "@schemeURI")]
1101    pub scheme_uri: Option<String>,
1102    /// Value URI
1103    #[validate(url)]
1104    #[serde(rename = "valueURI", alias = "@valueURI")]
1105    pub value_uri: Option<String>,
1106    /// Classification code
1107    #[serde(rename = "classificationCode", alias = "@classificationCode")]
1108    pub classification_code: Option<String>,
1109}
1110/// Container for `<subjects>` XML element
1111#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1112pub struct Subjects {
1113    /// Subject list
1114    #[validate(nested)]
1115    pub subject: Vec<Subject>,
1116}
1117/// Dataset title per DataCite 4.6 property 3
1118///
1119/// See <https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/title/> for details
1120#[skip_serializing_none]
1121#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1122pub struct Title {
1123    /// Title text
1124    #[serde(alias = "$text")]
1125    pub title: String,
1126    /// Title type
1127    #[serde(rename = "titleType", alias = "@titleType")]
1128    pub title_type: Option<TitleType>,
1129}
1130/// Container for `<titles>` XML element
1131#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
1132pub struct Titles {
1133    /// Title list
1134    pub title: Vec<Title>,
1135}
1136impl PartialEq for GeoLocationPoint {
1137    fn eq(&self, other: &Self) -> bool {
1138        self.longitude.to_bits() == other.longitude.to_bits() && self.latitude.to_bits() == other.latitude.to_bits()
1139    }
1140}
1141impl Eq for GeoLocationPoint {}
1142impl fmt::Display for GeoLocationPoint {
1143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1144        let Self { longitude, latitude } = self;
1145        write!(f, "({longitude}, {latitude})")
1146    }
1147}
1148impl TryFrom<huwise::Dataset> for Record {
1149    type Error = CrosswalkError;
1150
1151    fn try_from(dataset: huwise::Dataset) -> Result<Self, Self::Error> {
1152        let mapping = huwise_to_datacite();
1153        crosswalk::convert(&dataset, &mapping).map(|(record, _)| record)
1154    }
1155}
1156impl TryFrom<dcat::Dataset> for Record {
1157    type Error = CrosswalkError;
1158
1159    fn try_from(dataset: dcat::Dataset) -> Result<Self, Self::Error> {
1160        let mapping = dcat_to_datacite();
1161        crosswalk::convert(&dataset, &mapping).map(|(record, _)| record)
1162    }
1163}
1164impl TryFrom<&dcat::Dataset> for Record {
1165    type Error = CrosswalkError;
1166
1167    fn try_from(dataset: &dcat::Dataset) -> Result<Self, Self::Error> {
1168        Record::try_from(dataset.clone())
1169    }
1170}
1171impl TryFrom<invenio::Record> for Record {
1172    type Error = CrosswalkError;
1173
1174    fn try_from(record: invenio::Record) -> Result<Self, Self::Error> {
1175        let mapping = invenio_to_datacite();
1176        crosswalk::convert(&record, &mapping).map(|(record, _)| record)
1177    }
1178}
1179impl TryFrom<&invenio::Record> for Record {
1180    type Error = CrosswalkError;
1181
1182    fn try_from(record: &invenio::Record) -> Result<Self, Self::Error> {
1183        Record::try_from(record.clone())
1184    }
1185}
1186impl SchemaBuilder for Record {
1187    fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError> {
1188        let doi = fields.get_string("doi").unwrap_or_default();
1189        let mut titles = None;
1190        if let Some(title_str) = fields.get_string_opt("title") {
1191            titles = Some(vec![Title {
1192                title: title_str,
1193                title_type: None,
1194            }]);
1195        }
1196        let mut descriptions = None;
1197        if let Some(desc_str) = fields.get_string_opt("description") {
1198            descriptions = Some(vec![Description {
1199                description: desc_str,
1200                description_type: None,
1201                language: None,
1202            }]);
1203        }
1204        let language = fields.get_string_opt("language");
1205        let mut creators = None;
1206        if let Some(creator_names) = fields.get_string_vec_opt("creators") {
1207            creators = Some(
1208                creator_names
1209                    .into_iter()
1210                    .map(|name| Creator {
1211                        name,
1212                        name_type: None,
1213                        given_name: None,
1214                        family_name: None,
1215                        name_identifiers: None,
1216                        affiliation: None,
1217                    })
1218                    .collect(),
1219            );
1220        }
1221        let mut publisher = None;
1222        if let Some(pub_name) = fields.get_string_opt("publisher") {
1223            publisher = Some(Publisher {
1224                name: pub_name,
1225                publisher_identifier: None,
1226                publisher_identifier_scheme: None,
1227                scheme_uri: None,
1228            });
1229        }
1230        let publication_year = fields.get_number_opt("publication-year").map(|n| n as i32);
1231        let mut attributes = Attributes {
1232            doi,
1233            event: None,
1234            titles,
1235            creators,
1236            publisher,
1237            publication_year,
1238            resource_types: None,
1239            url: fields.get_iri_opt("url"),
1240            subjects: None,
1241            contributors: None,
1242            dates: None,
1243            language,
1244            alternate_identifiers: None,
1245            related_identifiers: None,
1246            sizes: None,
1247            formats: None,
1248            version: fields.get_string_opt("version"),
1249            rights_list: None,
1250            descriptions,
1251            geo_locations: None,
1252            funding_references: None,
1253            schema_version: None,
1254        };
1255        if let Some(license_iri) = fields.get_iri_opt("license") {
1256            attributes.rights_list = Some(vec![Rights {
1257                rights: None,
1258                rights_uri: Some(license_iri),
1259                rights_identifier: None,
1260                rights_identifier_scheme: None,
1261                scheme_uri: None,
1262            }]);
1263        }
1264        Ok(Record {
1265            id: attributes.doi.clone(),
1266            kind: "dois".to_string(),
1267            attributes,
1268        })
1269    }
1270}
1271impl SchemaExtractor for Record {
1272    fn extract_fields(&self) -> Fields {
1273        let mut fields = Fields::new();
1274        fields.insert("doi", FieldValue::String(self.attributes.doi.clone()));
1275        if let Some(titles) = &self.attributes.titles {
1276            if let Some(first) = titles.first() {
1277                fields.insert("title", FieldValue::String(first.title.clone()));
1278                if titles.len() > 1 {
1279                    let alt_titles: Vec<String> = titles.iter().skip(1).map(|t| t.title.clone()).collect();
1280                    fields.insert("alternative-titles", FieldValue::StringVec(alt_titles));
1281                }
1282            }
1283        }
1284        if let Some(descriptions) = &self.attributes.descriptions {
1285            if let Some(first) = descriptions.first() {
1286                fields.insert("description", FieldValue::String(first.description.clone()));
1287            }
1288        }
1289        if let Some(language) = &self.attributes.language {
1290            fields.insert("language", FieldValue::String(language.clone()));
1291        }
1292        if let Some(creators) = &self.attributes.creators {
1293            if !creators.is_empty() {
1294                let creator_names: Vec<String> = creators.iter().map(|c| c.name.clone()).collect();
1295                fields.insert("creators", FieldValue::StringVec(creator_names));
1296            }
1297        }
1298        if let Some(publisher) = &self.attributes.publisher {
1299            fields.insert("publisher", FieldValue::String(publisher.name.clone()));
1300        }
1301
1302        if let Some(pub_year) = self.attributes.publication_year {
1303            fields.insert("publication-year", FieldValue::Number(pub_year as f64));
1304        }
1305        if let Some(url) = &self.attributes.url {
1306            fields.insert("url", FieldValue::IRI(url.clone()));
1307        }
1308        if let Some(rights) = &self.attributes.rights_list {
1309            if let Some(first) = rights.first() {
1310                if let Some(uri) = &first.rights_uri {
1311                    fields.insert("license", FieldValue::IRI(uri.clone()));
1312                }
1313            }
1314        }
1315        if let Some(identifiers) = &self.attributes.alternate_identifiers {
1316            if !identifiers.is_empty() {
1317                let alt_ids: Vec<String> = identifiers.iter().map(|id| id.alternate_identifier.clone()).collect();
1318                fields.insert("alternate-identifiers", FieldValue::StringVec(alt_ids));
1319            }
1320        }
1321        if let Some(subjects) = &self.attributes.subjects {
1322            if !subjects.is_empty() {
1323                let subject_strings: Vec<String> = subjects.iter().map(|s| s.subject.clone()).collect();
1324                fields.insert("subjects", FieldValue::StringVec(subject_strings));
1325            }
1326        }
1327        if let Some(version) = &self.attributes.version {
1328            fields.insert("version", FieldValue::String(version.clone()));
1329        }
1330        fields
1331    }
1332}
1333impl ToProse for Record {
1334    fn to_prose(&self) -> String {
1335        self.attributes
1336            .titles
1337            .iter()
1338            .flatten()
1339            .map(|value| value.title.clone())
1340            .chain(self.attributes.descriptions.iter().flatten().map(|value| value.description.clone()))
1341            .chain(self.attributes.subjects.iter().flatten().map(|value| value.subject.clone()))
1342            .collect::<Vec<String>>()
1343            .join("\n\n")
1344    }
1345}
1346#[cfg(feature = "std")]
1347impl InputOutput for Record {
1348    fn read(path: impl Into<PathBuf>) -> ApiResult<Record> {
1349        let source = path.into();
1350        match MimeType::from(source.display().to_string()) {
1351            | MimeType::Json => Record::read_json(source),
1352            | MimeType::Yaml => Record::read_yaml(source),
1353            | _ => Err(eyre!("Unsupported DataCite data file extension")),
1354        }
1355    }
1356    fn read_json(path: PathBuf) -> ApiResult<Record> {
1357        #[derive(Deserialize)]
1358        #[serde(untagged)]
1359        enum JsonInput {
1360            One(Box<Record>),
1361            Many(Vec<Record>),
1362        }
1363
1364        read_file(path).and_then(|content| {
1365            serde_json::from_str::<JsonInput>(&content)
1366                .map_err(|why| eyre!("Failed to parse JSON DataCite record — {why}"))
1367                .and_then(|value| match value {
1368                    | JsonInput::One(record) => Ok(*record),
1369                    | JsonInput::Many(records) => match records.len() {
1370                        | 1 => records
1371                            .into_iter()
1372                            .next()
1373                            .ok_or_else(|| eyre!("Expected one DataCite record but found none")),
1374                        | len => Err(eyre!("Expected one DataCite record but found {len}")),
1375                    },
1376                })
1377        })
1378    }
1379    fn read_yaml(path: PathBuf) -> ApiResult<Record> {
1380        #[derive(Deserialize)]
1381        #[serde(untagged)]
1382        enum YamlInput {
1383            One(Box<Record>),
1384            Many(Vec<Record>),
1385        }
1386
1387        read_file(path).and_then(|content| {
1388            serde_norway::from_str::<YamlInput>(&content)
1389                .map_err(|why| eyre!("Failed to parse YAML DataCite record — {why}"))
1390                .and_then(|value| match value {
1391                    | YamlInput::One(record) => Ok(*record),
1392                    | YamlInput::Many(records) => match records.len() {
1393                        | 1 => records
1394                            .into_iter()
1395                            .next()
1396                            .ok_or_else(|| eyre!("Expected one DataCite record but found none")),
1397                        | len => Err(eyre!("Expected one DataCite record but found {len}")),
1398                    },
1399                })
1400        })
1401    }
1402    fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
1403        let output = path.into();
1404        match MimeType::from(output.display().to_string()) {
1405            | MimeType::Json => self.write_json(output),
1406            | MimeType::Yaml => self.write_yaml(output),
1407            | _ => Err(eyre!("Unsupported DataCite data file extension for writing")),
1408        }
1409    }
1410    fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
1411        let output = path.into().with_extension("json");
1412        serde_json::to_string_pretty(self)
1413            .map_err(|why| eyre!("Failed to serialize JSON DataCite record — {why}"))
1414            .and_then(|content| write_file(output, content))
1415    }
1416    fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
1417        let output = path.into().with_extension("yaml");
1418        serde_norway::to_string(self)
1419            .map_err(|why| eyre!("Failed to serialize YAML DataCite record — {why}"))
1420            .and_then(|content| write_file(output, content))
1421    }
1422}
1423fn default_identifier_type() -> String {
1424    DATACITE_IDENTIFIER_TYPE_CONTROLLED_VOCABULARY[0].to_string()
1425}