Skip to main content

acorn/schema/standard/dcat/
mod.rs

1//! DCAT (Data Catalog Vocabulary) v3 schema models
2//!
3//! These types model the DCAT v3 structure per the W3C Recommendation (22 August 2024).
4//! Covers catalogs, datasets, dataset series, distributions, data services, and
5//! supporting types for spatial/temporal coverage, checksums, and relationships.
6//!
7//! DCAT namespace: `http://www.w3.org/ns/dcat#`
8//!
9//! References:
10//! - [DCAT v3](https://www.w3.org/TR/vocab-dcat-3/)
11//! - [DCAT v2](https://www.w3.org/TR/vocab-dcat-2/)
12#[cfg(feature = "std")]
13use crate::error::ApiResult;
14#[cfg(feature = "std")]
15use crate::io::{read_file, write_file, InputOutput};
16use crate::prelude::*;
17use crate::schema::standard::crosswalk::{self, mapping::datacite_to_dcat, CrosswalkError, FieldValue, Fields, SchemaBuilder, SchemaExtractor};
18use crate::schema::standard::datacite::{self, RelationType};
19use crate::schema::validate::is_url;
20use crate::schema::{Date as PeriodOfTime, OneOrMany};
21#[cfg(feature = "std")]
22use crate::util::MimeType;
23use crate::util::{Checksum, ToProse};
24#[cfg(feature = "std")]
25use crate::PathBuf;
26#[cfg(feature = "std")]
27use color_eyre::eyre::eyre;
28use schemars::JsonSchema;
29use serde::{Deserialize, Serialize};
30use serde_with::skip_serializing_none;
31use validator::Validate;
32
33pub(crate) mod validate;
34use validate::{is_document_refs_urls, is_one_or_many_urls};
35
36/// A collection of datasets published separately but sharing characteristics (`dcat:DatasetSeries`)
37///
38/// Added in DCAT 3. Sub-class of `dcat:Dataset`.
39/// See <https://www.w3.org/TR/vocab-dcat-3/#Class:Dataset_Series>
40///
41/// Inherits all `Dataset` properties. Reuse `Dataset` with an appropriate
42/// `type_` value, or use this type to make the series nature explicit in code.
43pub type DatasetSeries = Dataset;
44/// A `conformsTo` value represented as either a URI or DCAT-US standard object.
45#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
46#[serde(untagged)]
47pub enum ConformsTo {
48    /// Standard URI.
49    Uri(String),
50    /// DCAT-US standard object.
51    Standard(ConformsToStandard),
52}
53/// A DCAT contact point represented as either a vCard URI or DCAT-US `Kind`.
54#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
55#[serde(untagged)]
56pub enum ContactPoint {
57    /// Contact point URI.
58    Uri(String),
59    /// DCAT-US vCard contact object.
60    Kind(Kind),
61}
62/// A document reference represented as either a URI or DCAT-US document object.
63#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
64#[serde(untagged)]
65pub enum DocumentRef {
66    /// Document URI.
67    Uri(String),
68    /// DCAT-US document object.
69    Document(Document),
70}
71/// A publisher represented as either a W3C DCAT agent or DCAT-US organization.
72#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
73#[serde(untagged)]
74pub enum Publisher {
75    /// DCAT-US organization.
76    Organization(UsOrganization),
77    /// W3C DCAT agent.
78    Agent(Agent),
79}
80/// An agent (person or organization) as a `foaf:Agent`
81///
82/// Used for `dcterms:creator`, `dcterms:publisher`, etc.
83#[skip_serializing_none]
84#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
85pub struct Agent {
86    /// Agent name (`foaf:name`)
87    pub name: Option<String>,
88    /// Agent homepage URI (`foaf:homepage`)
89    #[serde(default)]
90    #[validate(custom(function = "is_one_or_many_urls"))]
91    pub homepage: Option<OneOrMany<String>>,
92    /// Agent email (`foaf:mbox`)
93    pub email: Option<String>,
94    /// Agent identifier(s) e.g. ORCID, ROR (`dcterms:identifier`)
95    #[serde(default)]
96    pub identifier: Option<OneOrMany<String>>,
97}
98/// A curated collection of metadata about resources (`dcat:Catalog`)
99///
100/// Sub-class of `dcat:Dataset`. See <https://www.w3.org/TR/vocab-dcat-3/#Class:Catalog>
101#[skip_serializing_none]
102#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
103pub struct Catalog {
104    /// JSON-LD node identifier.
105    #[serde(rename = "@id")]
106    pub id: Option<String>,
107    /// JSON-LD node type.
108    #[serde(rename = "@type")]
109    pub jsonld_type: Option<String>,
110    /// Catalog title(s) (`dcterms:title`)
111    #[serde(default)]
112    pub title: Option<OneOrMany<String>>,
113    /// Description(s) (`dcterms:description`)
114    #[serde(default)]
115    pub description: Option<OneOrMany<String>>,
116    /// Unique identifier(s) (`dcterms:identifier`)
117    #[serde(default)]
118    pub identifier: Option<OneOrMany<String>>,
119    /// Publication date as ISO 8601 string (`dcterms:issued`)
120    pub issued: Option<String>,
121    /// Last modification date as ISO 8601 string (`dcterms:modified`)
122    pub modified: Option<String>,
123    /// Language code(s) per ISO 639-1 (`dcterms:language`)
124    #[serde(default)]
125    pub language: Option<OneOrMany<String>>,
126    /// Publisher (`dcterms:publisher`)
127    #[validate(nested)]
128    pub publisher: Option<Publisher>,
129    /// Creator(s) (`dcterms:creator`, DCAT 2)
130    #[validate(nested)]
131    pub creator: Option<Vec<Agent>>,
132    /// Contact point IRI(s) for vCard (`dcat:contactPoint`)
133    #[serde(rename = "contactPoint", default)]
134    #[validate(nested)]
135    pub contact_point: Option<OneOrMany<ContactPoint>>,
136    /// Keywords (`dcat:keyword`)
137    #[serde(default)]
138    pub keywords: Option<OneOrMany<String>>,
139    /// Theme/category IRI(s) (`dcat:theme`)
140    #[serde(default)]
141    pub themes: Option<OneOrMany<String>>,
142    /// License document URI (`dcterms:license`)
143    pub license: Option<String>,
144    /// Rights statement URI (`dcterms:rights`)
145    pub rights: Option<String>,
146    /// Access rights statement URI (`dcterms:accessRights`)
147    #[serde(rename = "accessRights")]
148    pub access_rights: Option<String>,
149    /// ODRL policy IRI (`odrl:hasPolicy`)
150    #[serde(rename = "hasPolicy")]
151    pub has_policy: Option<String>,
152    /// Standards conformed to, as IRIs (`dcterms:conformsTo`)
153    #[serde(rename = "conformsTo", default)]
154    #[validate(nested)]
155    pub conforms_to: Option<OneOrMany<ConformsTo>>,
156    /// Landing page URI(s) (`dcat:landingPage`)
157    #[serde(rename = "landingPage", default)]
158    #[validate(custom(function = "is_document_refs_urls"), nested)]
159    pub landing_page: Option<OneOrMany<DocumentRef>>,
160    /// Relations to other resources, as IRIs (`dcterms:relation`)
161    #[serde(default)]
162    pub relation: Option<OneOrMany<String>>,
163    /// Resource type IRI(s) (`dcterms:type`)
164    #[serde(rename = "type", default)]
165    pub type_: Option<OneOrMany<String>>,
166    /// Version indicator (`dcat:version`, DCAT 3)
167    pub version: Option<String>,
168    /// Version notes (`adms:versionNotes`, DCAT 3)
169    #[serde(rename = "versionNotes")]
170    pub version_notes: Option<String>,
171    /// IRI of the previous version (`dcat:previousVersion`, DCAT 3)
172    #[serde(rename = "previousVersion")]
173    pub previous_version: Option<String>,
174    /// IRI(s) of versioned snapshots (`dcat:hasVersion`, DCAT 3)
175    #[serde(rename = "hasVersion", default)]
176    pub has_version: Option<OneOrMany<String>>,
177    /// IRI of the current version (`dcat:hasCurrentVersion`, DCAT 3)
178    #[serde(rename = "hasCurrentVersion")]
179    pub has_current_version: Option<String>,
180    /// IRI of the resource this one replaces (`dcterms:replaces`, DCAT 3)
181    pub replaces: Option<String>,
182    /// Life-cycle status IRI (`adms:status`, DCAT 3)
183    pub status: Option<String>,
184    /// Related resources that reference this catalog (`dcterms:isReferencedBy`, DCAT 2)
185    #[serde(rename = "isReferencedBy", default)]
186    pub is_referenced_by: Option<OneOrMany<String>>,
187    /// Qualified relationships to other resources (`dcat:qualifiedRelation`, DCAT 2)
188    #[validate(nested)]
189    #[serde(rename = "qualifiedRelation")]
190    pub qualified_relation: Option<Vec<Relationship>>,
191    /// Inherited dataset distributions (`dcat:distribution`)
192    #[validate(nested)]
193    pub distribution: Option<Vec<Distribution>>,
194    /// Update frequency IRI (`dcterms:accrualPeriodicity`)
195    pub frequency: Option<String>,
196    /// Spatial coverage (`dcterms:spatial`)
197    #[validate(nested)]
198    pub spatial: Option<Vec<Location>>,
199    /// Minimum spatial separation in meters (`dcat:spatialResolutionInMeters`, DCAT 2)
200    #[serde(rename = "spatialResolutionInMeters")]
201    pub spatial_resolution_in_meters: Option<f64>,
202    /// Temporal coverage (`dcterms:temporal`)
203    #[validate(nested)]
204    pub temporal: Option<Vec<PeriodOfTime>>,
205    /// Minimum time period resolvable as ISO 8601 duration (`dcat:temporalResolution`, DCAT 2)
206    #[serde(rename = "temporalResolution")]
207    pub temporal_resolution: Option<String>,
208    /// Activity IRI(s) that generated this catalog (`prov:wasGeneratedBy`, DCAT 2)
209    #[serde(rename = "wasGeneratedBy", default)]
210    pub was_generated_by: Option<OneOrMany<String>>,
211    /// Catalog homepage URI (`foaf:homepage`)
212    #[serde(default)]
213    #[validate(custom(function = "is_document_refs_urls"), nested)]
214    pub homepage: Option<OneOrMany<DocumentRef>>,
215    /// Knowledge organization system IRI(s) for classifying resources (`dcat:themeTaxonomy`)
216    #[serde(rename = "themeTaxonomy", default)]
217    pub theme_taxonomy: Option<OneOrMany<String>>,
218    /// Dataset IRI(s) listed in this catalog (`dcat:dataset`)
219    #[serde(default)]
220    pub dataset: Option<OneOrMany<String>>,
221    /// Data service IRI(s) listed in this catalog (`dcat:service`)
222    #[validate(nested)]
223    pub service: Option<Vec<DataService>>,
224    /// Sub-catalog IRI(s) listed in this catalog (`dcat:catalog`)
225    #[serde(default)]
226    pub catalog: Option<OneOrMany<String>>,
227    /// Catalog records for resources in this catalog (`dcat:record`)
228    #[validate(nested)]
229    pub record: Option<Vec<CatalogRecord>>,
230}
231/// Metadata record for a cataloged resource (`dcat:CatalogRecord`)
232///
233/// Optional. Used when catalog-entry provenance (e.g., listing date) differs
234/// from resource provenance. See <https://www.w3.org/TR/vocab-dcat-3/#Class:Catalog_Record>
235#[skip_serializing_none]
236#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
237pub struct CatalogRecord {
238    /// Record title (`dcterms:title`)
239    pub title: Option<String>,
240    /// Record description (`dcterms:description`)
241    pub description: Option<String>,
242    /// Date the resource was listed in the catalog as ISO 8601 string (`dcterms:issued`)
243    pub issued: Option<String>,
244    /// Date of the most recent change to the catalog entry as ISO 8601 string (`dcterms:modified`)
245    pub modified: Option<String>,
246    /// IRI of the cataloged resource this record describes (`foaf:primaryTopic`)
247    #[serde(rename = "primaryTopic")]
248    pub primary_topic: String,
249    /// Standards the record conforms to, as IRIs (`dcterms:conformsTo`)
250    #[serde(rename = "conformsTo", default)]
251    pub conforms_to: Option<OneOrMany<String>>,
252}
253/// A standard or profile referenced by `dcterms:conformsTo`.
254#[skip_serializing_none]
255#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
256pub struct ConformsToStandard {
257    /// JSON-LD node identifier.
258    #[serde(rename = "@id")]
259    #[validate(custom(function = "is_url"))]
260    pub id: Option<String>,
261    /// JSON-LD node type.
262    #[serde(rename = "@type")]
263    pub jsonld_type: Option<String>,
264    /// Standard title.
265    pub title: Option<String>,
266    /// Standard identifier.
267    pub identifier: Option<String>,
268}
269/// A collection of operations providing access to data (`dcat:DataService`)
270///
271/// Added in DCAT 2. Sub-class of `dcat:Resource`.
272/// See <https://www.w3.org/TR/vocab-dcat-3/#Class:Data_Service>
273#[skip_serializing_none]
274#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
275pub struct DataService {
276    /// JSON-LD node identifier.
277    #[serde(rename = "@id")]
278    pub id: Option<String>,
279    /// JSON-LD node type.
280    #[serde(rename = "@type")]
281    pub jsonld_type: Option<String>,
282    /// Service title(s) (`dcterms:title`)
283    #[serde(default)]
284    pub title: Option<OneOrMany<String>>,
285    /// Description(s) (`dcterms:description`)
286    #[serde(default)]
287    pub description: Option<OneOrMany<String>>,
288    /// Unique identifier(s) (`dcterms:identifier`)
289    #[serde(default)]
290    pub identifier: Option<OneOrMany<String>>,
291    /// Publication date as ISO 8601 string (`dcterms:issued`)
292    pub issued: Option<String>,
293    /// Last modification date as ISO 8601 string (`dcterms:modified`)
294    pub modified: Option<String>,
295    /// Language code(s) per ISO 639-1 (`dcterms:language`)
296    #[serde(default)]
297    pub language: Option<OneOrMany<String>>,
298    /// Publisher (`dcterms:publisher`)
299    #[validate(nested)]
300    pub publisher: Option<Publisher>,
301    /// Creator(s) (`dcterms:creator`, DCAT 2)
302    #[validate(nested)]
303    pub creator: Option<Vec<Agent>>,
304    /// Contact point IRI(s) for vCard (`dcat:contactPoint`)
305    #[serde(rename = "contactPoint", default)]
306    #[validate(nested)]
307    pub contact_point: Option<OneOrMany<ContactPoint>>,
308    /// Keywords (`dcat:keyword`)
309    #[serde(default)]
310    pub keywords: Option<OneOrMany<String>>,
311    /// Theme/category IRI(s) (`dcat:theme`)
312    #[serde(default)]
313    pub themes: Option<OneOrMany<String>>,
314    /// License document URI (`dcterms:license`)
315    pub license: Option<String>,
316    /// Rights statement URI (`dcterms:rights`)
317    pub rights: Option<String>,
318    /// Access rights statement URI (`dcterms:accessRights`)
319    #[serde(rename = "accessRights")]
320    pub access_rights: Option<String>,
321    /// ODRL policy IRI (`odrl:hasPolicy`)
322    #[serde(rename = "hasPolicy")]
323    pub has_policy: Option<String>,
324    /// Standards conformed to, as IRIs (`dcterms:conformsTo`)
325    #[serde(rename = "conformsTo", default)]
326    #[validate(nested)]
327    pub conforms_to: Option<OneOrMany<ConformsTo>>,
328    /// Landing page URI(s) (`dcat:landingPage`)
329    #[serde(rename = "landingPage", default)]
330    #[validate(custom(function = "is_document_refs_urls"), nested)]
331    pub landing_page: Option<OneOrMany<DocumentRef>>,
332    /// Resource type IRI(s) (`dcterms:type`)
333    #[serde(rename = "type", default)]
334    pub type_: Option<OneOrMany<String>>,
335    /// Version indicator (`dcat:version`, DCAT 3)
336    pub version: Option<String>,
337    /// Version notes (`adms:versionNotes`, DCAT 3)
338    #[serde(rename = "versionNotes")]
339    pub version_notes: Option<String>,
340    /// IRI of the previous version (`dcat:previousVersion`, DCAT 3)
341    #[serde(rename = "previousVersion")]
342    pub previous_version: Option<String>,
343    /// IRI(s) of versioned snapshots (`dcat:hasVersion`, DCAT 3)
344    #[serde(rename = "hasVersion", default)]
345    pub has_version: Option<OneOrMany<String>>,
346    /// IRI of the current version (`dcat:hasCurrentVersion`, DCAT 3)
347    #[serde(rename = "hasCurrentVersion")]
348    pub has_current_version: Option<String>,
349    /// IRI of the resource this one replaces (`dcterms:replaces`, DCAT 3)
350    pub replaces: Option<String>,
351    /// Life-cycle status IRI (`adms:status`, DCAT 3)
352    pub status: Option<String>,
353    /// Qualified relationships to other resources (`dcat:qualifiedRelation`, DCAT 2)
354    #[validate(nested)]
355    #[serde(rename = "qualifiedRelation")]
356    pub qualified_relation: Option<Vec<Relationship>>,
357    /// Root location or primary endpoint IRI(s) (`dcat:endpointURL`)
358    #[serde(rename = "endpointURL")]
359    #[validate(custom(function = "is_one_or_many_urls"))]
360    pub endpoint_url: OneOrMany<String>,
361    /// Endpoint description IRI(s) or documents (`dcat:endpointDescription`)
362    #[serde(rename = "endpointDescription", default)]
363    pub endpoint_description: Option<OneOrMany<String>>,
364    /// Dataset IRI(s) served by this service (`dcat:servesDataset`)
365    #[serde(rename = "servesDataset", default)]
366    pub serves_dataset: Option<OneOrMany<String>>,
367}
368/// A collection of data published or curated by a single agent (`dcat:Dataset`)
369///
370/// Sub-class of `dcat:Resource`. The conceptual dataset, not any particular
371/// serialization. See <https://www.w3.org/TR/vocab-dcat-3/#Class:Dataset>
372#[skip_serializing_none]
373#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
374pub struct Dataset {
375    /// JSON-LD node identifier.
376    #[serde(rename = "@id")]
377    pub id: Option<String>,
378    /// JSON-LD node type.
379    #[serde(rename = "@type")]
380    pub jsonld_type: Option<String>,
381    /// Dataset title(s) (`dcterms:title`)
382    #[serde(default)]
383    pub title: Option<OneOrMany<String>>,
384    /// Description(s) (`dcterms:description`)
385    #[serde(default)]
386    pub description: Option<OneOrMany<String>>,
387    /// Unique identifier(s) (`dcterms:identifier`)
388    #[serde(default)]
389    pub identifier: Option<OneOrMany<String>>,
390    /// Publication date as ISO 8601 string (`dcterms:issued`)
391    pub issued: Option<String>,
392    /// Last modification date as ISO 8601 string (`dcterms:modified`)
393    pub modified: Option<String>,
394    /// Language code(s) per ISO 639-1 (`dcterms:language`)
395    #[serde(default)]
396    pub language: Option<OneOrMany<String>>,
397    /// Publisher (`dcterms:publisher`)
398    #[validate(nested)]
399    pub publisher: Option<Publisher>,
400    /// Creator(s) (`dcterms:creator`, DCAT 2)
401    #[validate(nested)]
402    pub creator: Option<Vec<Agent>>,
403    /// Contact point IRI(s) for vCard (`dcat:contactPoint`)
404    #[serde(rename = "contactPoint", default)]
405    #[validate(nested)]
406    pub contact_point: Option<OneOrMany<ContactPoint>>,
407    /// Keywords (`dcat:keyword`)
408    #[serde(default)]
409    pub keywords: Option<OneOrMany<String>>,
410    /// Theme/category IRI(s) (`dcat:theme`)
411    #[serde(default)]
412    pub themes: Option<OneOrMany<String>>,
413    /// License document URI (`dcterms:license`)
414    pub license: Option<String>,
415    /// Rights statement URI (`dcterms:rights`)
416    pub rights: Option<String>,
417    /// Access rights statement URI (`dcterms:accessRights`)
418    #[serde(rename = "accessRights")]
419    pub access_rights: Option<String>,
420    /// ODRL policy IRI (`odrl:hasPolicy`)
421    #[serde(rename = "hasPolicy")]
422    pub has_policy: Option<String>,
423    /// Standards conformed to, as IRIs (`dcterms:conformsTo`)
424    #[serde(rename = "conformsTo", default)]
425    #[validate(nested)]
426    pub conforms_to: Option<OneOrMany<ConformsTo>>,
427    /// Landing page URI(s) (`dcat:landingPage`)
428    #[serde(rename = "landingPage", default)]
429    #[validate(custom(function = "is_document_refs_urls"), nested)]
430    pub landing_page: Option<OneOrMany<DocumentRef>>,
431    /// Relations to other resources, as IRIs (`dcterms:relation`)
432    #[serde(default)]
433    pub relation: Option<OneOrMany<String>>,
434    /// Resource type IRI(s) (`dcterms:type`)
435    #[serde(rename = "type", default)]
436    pub type_: Option<OneOrMany<String>>,
437    /// Version indicator (`dcat:version`, DCAT 3)
438    pub version: Option<String>,
439    /// Version notes (`adms:versionNotes`, DCAT 3)
440    #[serde(rename = "versionNotes")]
441    pub version_notes: Option<String>,
442    /// IRI of the previous version (`dcat:previousVersion`, DCAT 3)
443    #[serde(rename = "previousVersion")]
444    pub previous_version: Option<String>,
445    /// IRI(s) of versioned snapshots (`dcat:hasVersion`, DCAT 3)
446    #[serde(rename = "hasVersion", default)]
447    pub has_version: Option<OneOrMany<String>>,
448    /// IRI of the current version (`dcat:hasCurrentVersion`, DCAT 3)
449    #[serde(rename = "hasCurrentVersion")]
450    pub has_current_version: Option<String>,
451    /// IRI of the resource this one replaces (`dcterms:replaces`, DCAT 3)
452    pub replaces: Option<String>,
453    /// Life-cycle status IRI (`adms:status`, DCAT 3)
454    pub status: Option<String>,
455    /// Related resources that reference this dataset (`dcterms:isReferencedBy`, DCAT 2)
456    #[serde(rename = "isReferencedBy", default)]
457    pub is_referenced_by: Option<OneOrMany<String>>,
458    /// Parts of this resource, as IRIs (`dcterms:hasPart`, DCAT 3)
459    #[serde(rename = "hasPart", default)]
460    pub has_part: Option<OneOrMany<String>>,
461    /// Qualified relationships to other resources (`dcat:qualifiedRelation`, DCAT 2)
462    #[validate(nested)]
463    #[serde(rename = "qualifiedRelation")]
464    pub qualified_relation: Option<Vec<Relationship>>,
465    /// IRI of the first resource in a series (`dcat:first`, DCAT 3)
466    pub first: Option<String>,
467    /// IRI of the last resource in a series (`dcat:last`, DCAT 3)
468    pub last: Option<String>,
469    /// IRI of the previous resource in a series (`dcat:prev`, DCAT 3)
470    pub previous: Option<String>,
471    /// Available distributions (`dcat:distribution`)
472    #[validate(nested)]
473    pub distribution: Option<Vec<Distribution>>,
474    /// Update frequency IRI (`dcterms:accrualPeriodicity`)
475    pub frequency: Option<String>,
476    /// Dataset series IRI(s) this dataset belongs to (`dcat:inSeries`, DCAT 3)
477    #[serde(rename = "inSeries", default)]
478    pub in_series: Option<OneOrMany<String>>,
479    /// Spatial coverage (`dcterms:spatial`)
480    #[validate(nested)]
481    pub spatial: Option<Vec<Location>>,
482    /// Minimum spatial separation in meters (`dcat:spatialResolutionInMeters`, DCAT 2)
483    #[serde(rename = "spatialResolutionInMeters")]
484    pub spatial_resolution_in_meters: Option<f64>,
485    /// Temporal coverage (`dcterms:temporal`)
486    #[validate(nested)]
487    pub temporal: Option<Vec<PeriodOfTime>>,
488    /// Minimum time period resolvable as ISO 8601 duration (`dcat:temporalResolution`, DCAT 2)
489    #[serde(rename = "temporalResolution")]
490    pub temporal_resolution: Option<String>,
491    /// Activity IRI(s) that generated this dataset (`prov:wasGeneratedBy`, DCAT 2)
492    #[serde(rename = "wasGeneratedBy", default)]
493    pub was_generated_by: Option<OneOrMany<String>>,
494}
495/// A specific representation of a dataset (`dcat:Distribution`)
496///
497/// See <https://www.w3.org/TR/vocab-dcat-3/#Class:Distribution>
498#[skip_serializing_none]
499#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
500pub struct Distribution {
501    /// JSON-LD node identifier.
502    #[serde(rename = "@id")]
503    pub id: Option<String>,
504    /// JSON-LD node type.
505    #[serde(rename = "@type")]
506    pub jsonld_type: Option<String>,
507    /// Distribution title (`dcterms:title`)
508    #[serde(default)]
509    pub title: Option<OneOrMany<String>>,
510    /// Free-text description (`dcterms:description`)
511    #[serde(default)]
512    pub description: Option<OneOrMany<String>>,
513    /// Publication date as ISO 8601 string (`dcterms:issued`)
514    pub issued: Option<String>,
515    /// Last modification date as ISO 8601 string (`dcterms:modified`)
516    pub modified: Option<String>,
517    /// License document URI (`dcterms:license`)
518    pub license: Option<String>,
519    /// Access rights statement URI (`dcterms:accessRights`)
520    #[serde(rename = "accessRights")]
521    pub access_rights: Option<String>,
522    /// Rights statement URI (`dcterms:rights`)
523    pub rights: Option<String>,
524    /// ODRL policy IRI (`odrl:hasPolicy`)
525    #[serde(rename = "hasPolicy")]
526    pub has_policy: Option<String>,
527    /// DCAT-US access restriction statement.
528    #[serde(rename = "accessRestriction")]
529    pub access_restriction: Option<String>,
530    /// DCAT-US use restriction statement.
531    #[serde(rename = "useRestriction")]
532    pub use_restriction: Option<String>,
533    /// URL(s) providing access to this distribution (`dcat:accessURL`)
534    #[serde(rename = "accessURL")]
535    #[validate(custom(function = "is_one_or_many_urls"))]
536    pub access_url: OneOrMany<String>,
537    /// Data service IRI(s) giving access to this distribution (`dcat:accessService`)
538    #[serde(rename = "accessService", default)]
539    pub access_service: Option<OneOrMany<String>>,
540    /// Direct download URL(s) (`dcat:downloadURL`)
541    #[serde(rename = "downloadURL", default)]
542    #[validate(custom(function = "is_one_or_many_urls"))]
543    pub download_url: Option<OneOrMany<String>>,
544    /// Size in bytes (`dcat:byteSize`)
545    #[serde(rename = "byteSize")]
546    pub byte_size: Option<u64>,
547    /// Minimum spatial separation in meters (`dcat:spatialResolutionInMeters`)
548    #[serde(rename = "spatialResolutionInMeters")]
549    pub spatial_resolution_in_meters: Option<f64>,
550    /// Minimum time period resolvable as ISO 8601 duration (`dcat:temporalResolution`)
551    #[serde(rename = "temporalResolution")]
552    pub temporal_resolution: Option<String>,
553    /// Standards the distribution conforms to, as IRIs (`dcterms:conformsTo`)
554    #[serde(rename = "conformsTo", default)]
555    #[validate(nested)]
556    pub conforms_to: Option<OneOrMany<ConformsTo>>,
557    /// IANA media type IRI (`dcat:mediaType`)
558    #[serde(rename = "mediaType")]
559    pub media_type: Option<String>,
560    /// File format IRI or string (`dcterms:format`)
561    pub format: Option<String>,
562    /// Compression format IANA media type IRI (`dcat:compressFormat`, DCAT 2)
563    #[serde(rename = "compressFormat")]
564    pub compress_format: Option<String>,
565    /// Packaging format IANA media type IRI (`dcat:packageFormat`, DCAT 2)
566    #[serde(rename = "packageFormat")]
567    pub package_format: Option<String>,
568    /// Checksum for integrity verification (`spdx:checksum`, DCAT 3)
569    #[validate(nested)]
570    pub checksum: Option<Checksum>,
571}
572/// A document resource used for landing pages and homepages.
573#[skip_serializing_none]
574#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
575pub struct Document {
576    /// JSON-LD node identifier.
577    #[serde(rename = "@id")]
578    #[validate(custom(function = "is_url"))]
579    pub id: Option<String>,
580    /// JSON-LD node type.
581    #[serde(rename = "@type")]
582    pub jsonld_type: Option<String>,
583    /// Document title.
584    pub title: Option<String>,
585    /// URL used to access the document.
586    #[serde(rename = "accessURL")]
587    #[validate(custom(function = "is_url"))]
588    pub access_url: Option<String>,
589}
590/// A vCard contact point (`vcard:Kind`) used by DCAT-US.
591#[skip_serializing_none]
592#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
593pub struct Kind {
594    /// JSON-LD node type.
595    #[serde(rename = "@type")]
596    pub jsonld_type: Option<String>,
597    /// Formatted contact name (`vcard:fn`).
598    #[serde(rename = "fn")]
599    pub fn_: String,
600    /// Contact email as a `mailto:` URI (`vcard:hasEmail`).
601    #[serde(rename = "hasEmail")]
602    pub has_email: String,
603    /// Contact telephone URI or string (`vcard:tel`).
604    pub tel: Option<String>,
605    /// Contact organization name (`vcard:organization-name`).
606    #[serde(rename = "organization-name")]
607    pub organization_name: Option<String>,
608}
609/// A spatial region or named place (`dcterms:Location`)
610///
611/// See <https://www.w3.org/TR/vocab-dcat-3/#Class:Location>
612#[skip_serializing_none]
613#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
614pub struct Location {
615    /// Full geometry as WKT or other literal encoding (`locn:geometry`)
616    pub geometry: Option<String>,
617    /// Geographic bounding box as WKT or other literal (`dcat:bbox`)
618    pub bbox: Option<String>,
619    /// Geographic centroid as WKT or other literal (`dcat:centroid`)
620    pub centroid: Option<String>,
621}
622/// Qualified relationship between resources (`dcat:Relationship`)
623///
624/// Added in DCAT 2. See <https://www.w3.org/TR/vocab-dcat-3/#Class:Relationship>
625#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
626pub struct Relationship {
627    /// IRI of the related resource (`dcterms:relation`)
628    pub relation: String,
629    /// IRI of the role the related resource plays (`dcat:hadRole`)
630    #[serde(rename = "hadRole", alias = "role")]
631    pub had_role: RelationType,
632}
633/// A DCAT-US publisher organization.
634#[skip_serializing_none]
635#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
636pub struct UsOrganization {
637    /// JSON-LD node identifier.
638    #[serde(rename = "@id")]
639    #[validate(custom(function = "is_url"))]
640    pub id: Option<String>,
641    /// JSON-LD node type.
642    #[serde(rename = "@type")]
643    pub jsonld_type: Option<String>,
644    /// Organization name.
645    pub name: String,
646    /// Preferred label.
647    #[serde(rename = "prefLabel")]
648    pub pref_label: Option<String>,
649    /// Alternative label.
650    #[serde(rename = "altLabel")]
651    pub alt_label: Option<String>,
652    /// Parent organization.
653    #[serde(rename = "subOrganizationOf")]
654    #[validate(nested)]
655    pub sub_organization_of: Option<Box<UsOrganization>>,
656}
657impl DocumentRef {
658    /// Returns the URI represented by this document reference, when present.
659    pub fn url(&self) -> Option<&str> {
660        match self {
661            | Self::Uri(value) => Some(value),
662            | Self::Document(document) => document.access_url.as_deref().or(document.id.as_deref()),
663        }
664    }
665}
666#[cfg(feature = "std")]
667impl InputOutput for Dataset {
668    fn read(path: impl Into<PathBuf>) -> ApiResult<Dataset> {
669        let source = path.into();
670        match MimeType::from(source.display().to_string()) {
671            | MimeType::Json => Dataset::read_json(source),
672            | MimeType::Yaml => Dataset::read_yaml(source),
673            | _ => Err(eyre!("Unsupported DCAT data file extension")),
674        }
675    }
676    fn read_json(path: PathBuf) -> ApiResult<Dataset> {
677        read_file(path).and_then(|content| {
678            serde_json::from_str::<OneOrMany<Dataset>>(&content)
679                .map_err(|why| eyre!("Failed to parse JSON DCAT dataset — {why}"))
680                .and_then(|value| match value {
681                    | OneOrMany::One(dataset) => Ok(dataset),
682                    | OneOrMany::Many(datasets) => match datasets.len() {
683                        | 1 => datasets
684                            .into_iter()
685                            .next()
686                            .ok_or_else(|| eyre!("Expected one DCAT dataset but found none")),
687                        | len => Err(eyre!("Expected one DCAT dataset but found {len}")),
688                    },
689                })
690        })
691    }
692    fn read_yaml(path: PathBuf) -> ApiResult<Dataset> {
693        read_file(path).and_then(|content| {
694            serde_norway::from_str::<OneOrMany<Dataset>>(&content)
695                .map_err(|why| eyre!("Failed to parse YAML DCAT dataset — {why}"))
696                .and_then(|value| match value {
697                    | OneOrMany::One(dataset) => Ok(dataset),
698                    | OneOrMany::Many(datasets) => match datasets.len() {
699                        | 1 => datasets
700                            .into_iter()
701                            .next()
702                            .ok_or_else(|| eyre!("Expected one DCAT dataset but found none")),
703                        | len => Err(eyre!("Expected one DCAT dataset but found {len}")),
704                    },
705                })
706        })
707    }
708    fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
709        let output = path.into();
710        match MimeType::from(output.display().to_string()) {
711            | MimeType::Json => self.write_json(output),
712            | MimeType::Yaml => self.write_yaml(output),
713            | _ => Err(eyre!("Unsupported DCAT data file extension for writing")),
714        }
715    }
716    fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
717        let output = path.into().with_extension("json");
718        serde_json::to_string_pretty(self)
719            .map_err(|why| eyre!("Failed to serialize JSON DCAT dataset — {why}"))
720            .and_then(|content| write_file(output, content))
721    }
722    fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
723        let output = path.into().with_extension("yaml");
724        serde_norway::to_string(self)
725            .map_err(|why| eyre!("Failed to serialize YAML DCAT dataset — {why}"))
726            .and_then(|content| write_file(output, content))
727    }
728}
729impl Publisher {
730    /// Returns the publisher name.
731    pub fn name(&self) -> Option<&str> {
732        match self {
733            | Self::Organization(organization) => Some(organization.name.as_str()),
734            | Self::Agent(agent) => agent.name.as_deref(),
735        }
736    }
737}
738impl SchemaBuilder for Dataset {
739    fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError> {
740        let identifier = fields.get_string_vec_opt("identifier").map(OneOrMany::Many);
741        let mut title = None;
742        if let Some(title_str) = fields.get_string_opt("title") {
743            title = Some(OneOrMany::Many(vec![title_str]));
744        }
745        let mut description = None;
746        if let Some(desc_str) = fields.get_string_opt("description") {
747            description = Some(OneOrMany::Many(vec![desc_str]));
748        }
749        let issued = fields.get_date_opt("issued");
750        let language = fields.get_string_vec_opt("language").map(OneOrMany::Many);
751        let keywords = fields.get_string_vec_opt("keywords").map(OneOrMany::Many);
752        let themes = fields.get_string_vec_opt("themes").map(OneOrMany::Many);
753        let version = fields.get_string_opt("version");
754        let mut publisher = None;
755        if let Some(pub_name) = fields.get_string_opt("publisher") {
756            publisher = Some(Publisher::Agent(Agent {
757                name: Some(pub_name),
758                homepage: None,
759                email: None,
760                identifier: None,
761            }));
762        }
763        let mut creator = None;
764        if let Some(creator_names) = fields.get_string_vec_opt("creators") {
765            creator = Some(
766                creator_names
767                    .into_iter()
768                    .map(|name| Agent {
769                        name: Some(name),
770                        homepage: None,
771                        email: None,
772                        identifier: None,
773                    })
774                    .collect(),
775            );
776        }
777        let license = fields.get_iri_opt("license");
778        let landing_page = fields
779            .get_string_opt("landing_page")
780            .or_else(|| fields.get_iri_opt("landing_page"))
781            .map(|value| OneOrMany::Many(vec![DocumentRef::Uri(value)]));
782        let mut spatial = None;
783        if let Some(spatial_str) = fields.get_string_opt("spatial") {
784            spatial = Some(vec![Location {
785                geometry: Some(spatial_str),
786                bbox: None,
787                centroid: None,
788            }]);
789        }
790        Ok(Dataset {
791            id: None,
792            jsonld_type: Some("dcat:Dataset".to_string()),
793            title,
794            description,
795            identifier,
796            issued,
797            modified: None,
798            language,
799            publisher,
800            creator,
801            contact_point: None,
802            keywords,
803            themes,
804            license,
805            rights: None,
806            access_rights: None,
807            has_policy: None,
808            conforms_to: None,
809            landing_page,
810            relation: None,
811            type_: None,
812            version,
813            version_notes: None,
814            previous_version: None,
815            has_version: None,
816            has_current_version: None,
817            replaces: None,
818            status: None,
819            is_referenced_by: None,
820            has_part: None,
821            qualified_relation: None,
822            first: None,
823            last: None,
824            previous: None,
825            distribution: None,
826            frequency: None,
827            in_series: None,
828            spatial,
829            spatial_resolution_in_meters: None,
830            temporal: None,
831            temporal_resolution: None,
832            was_generated_by: None,
833        })
834    }
835}
836impl SchemaExtractor for Dataset {
837    fn extract_fields(&self) -> Fields {
838        let mut fields = Fields::new();
839        if let Some(identifiers) = &self.identifier {
840            if !identifiers.is_empty() {
841                fields.insert("identifier", FieldValue::StringVec(identifiers.as_slice().to_vec()));
842            }
843        }
844        if let Some(titles) = &self.title {
845            if let Some(first) = titles.first() {
846                fields.insert("title", FieldValue::String(first.clone()));
847                if titles.len() > 1 {
848                    let alt_titles: Vec<String> = titles.iter().skip(1).cloned().collect();
849                    fields.insert("alternative-titles", FieldValue::StringVec(alt_titles));
850                }
851            }
852        }
853        if let Some(descriptions) = &self.description {
854            if let Some(first) = descriptions.first() {
855                fields.insert("description", FieldValue::String(first.clone()));
856            }
857        }
858        if let Some(issued) = &self.issued {
859            fields.insert("issued", FieldValue::Date(issued.clone()));
860        }
861        if let Some(keywords) = &self.keywords {
862            fields.insert("keywords", FieldValue::StringVec(keywords.as_slice().to_vec()));
863        }
864        if let Some(themes) = &self.themes {
865            fields.insert("themes", FieldValue::StringVec(themes.as_slice().to_vec()));
866        }
867        if let Some(language) = &self.language {
868            fields.insert("language", FieldValue::StringVec(language.as_slice().to_vec()));
869        }
870        if let Some(version) = &self.version {
871            fields.insert("version", FieldValue::String(version.clone()));
872        }
873        if let Some(publisher) = &self.publisher {
874            if let Some(name) = publisher.name() {
875                fields.insert("publisher", FieldValue::String(name.to_string()));
876            }
877        }
878        if let Some(creators) = &self.creator {
879            let creator_names: Vec<String> = creators.iter().filter_map(|c| c.name.clone()).collect();
880            if !creator_names.is_empty() {
881                fields.insert("creators", FieldValue::StringVec(creator_names));
882            }
883        }
884        if let Some(license) = &self.license {
885            fields.insert("license", FieldValue::IRI(license.clone()));
886        }
887        if let Some(spatial) = &self.spatial {
888            if let Some(first) = spatial.first() {
889                if let Some(geometry) = &first.geometry {
890                    fields.insert("spatial", FieldValue::String(geometry.clone()));
891                }
892            }
893        }
894        fields
895    }
896}
897impl ToProse for Dataset {
898    fn to_prose(&self) -> String {
899        self.title
900            .iter()
901            .flatten()
902            .cloned()
903            .chain(self.description.iter().flatten().cloned())
904            .chain(self.keywords.iter().flatten().cloned())
905            .collect::<Vec<String>>()
906            .join("\n\n")
907    }
908}
909impl TryFrom<&datacite::Record> for Dataset {
910    type Error = CrosswalkError;
911
912    fn try_from(record: &datacite::Record) -> Result<Self, Self::Error> {
913        Dataset::try_from(record.clone())
914    }
915}
916impl TryFrom<datacite::Record> for Dataset {
917    type Error = CrosswalkError;
918
919    fn try_from(record: datacite::Record) -> Result<Self, Self::Error> {
920        let mapping = datacite_to_dcat();
921        crosswalk::convert(&record, &mapping).map(|(dataset, _)| dataset)
922    }
923}