1#[cfg(feature = "std")]
4use crate::error::ApiResult;
5#[cfg(feature = "std")]
6use crate::io::{read_file, write_file, InputOutput};
7use crate::prelude::*;
8use crate::schema::standard::crosswalk::{self, mapping::datacite_to_huwise, CrosswalkError, FieldValue, Fields, SchemaBuilder, SchemaExtractor};
9use crate::schema::standard::datacite;
10#[cfg(feature = "std")]
11use crate::util::MimeType;
12use crate::util::ToProse;
13#[cfg(feature = "std")]
14use crate::PathBuf;
15#[cfg(feature = "std")]
16use ammonia::clean;
17#[cfg(feature = "std")]
18use color_eyre::eyre::eyre;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use serde_with::skip_serializing_none;
23use validator::Validate;
24
25#[cfg(not(feature = "std"))]
26fn clean(value: &str) -> String {
27 value.to_string()
28}
29
30pub type Catalog = Vec<Dataset>;
32#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
34pub enum DublinCoreType {
35 #[serde(rename = "Dataset")]
37 Dataset,
38}
39#[skip_serializing_none]
41#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
42pub struct CustomTemplate {
43 pub projects: Option<String>,
45 #[serde(rename = "source-of-data")]
47 pub source_of_data: Option<String>,
48}
49#[skip_serializing_none]
51#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
52pub struct Dataset {
53 pub dataset_id: String,
55 pub has_attachments: bool,
57 pub attachments_count: u64,
59 pub has_records: bool,
61 pub fields: Value,
63 #[validate(nested)]
65 pub metas: Meta,
66 pub features: Vec<Value>,
68}
69#[skip_serializing_none]
71#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
72pub struct Meta {
73 #[validate(nested)]
75 pub dcat: Option<Dcat>,
76 #[serde(rename = "default")]
78 #[validate(nested)]
79 pub r#default: Option<DefaultMeta>,
80 #[serde(rename = "dublin-core")]
82 #[validate(nested)]
83 pub dublin_core: Option<DublinCore>,
84 #[validate(nested)]
86 pub dcat_ap: Option<DcatAp>,
87 #[serde(rename = "custom-template")]
89 #[validate(nested)]
90 pub custom_template: Option<CustomTemplate>,
91 #[validate(nested)]
93 pub datacite: Option<Datacite>,
94}
95#[skip_serializing_none]
97#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
98#[serde(rename_all = "snake_case")]
99pub struct Dcat {
100 pub created: Option<String>,
102 pub issued: Option<String>,
104 pub creator: Option<String>,
106 pub contributor: Option<String>,
108 pub contact_name: Option<String>,
110 #[validate(email)]
112 pub contact_email: Option<String>,
113 #[serde(rename = "accrualperiodicity")]
115 pub accrual_periodicity: Option<String>,
116 pub spatial: Option<String>,
118 pub temporal: Option<String>,
120 pub granularity: Option<String>,
122 #[serde(rename = "dataquality")]
124 pub data_quality: Option<String>,
125 pub publisher_type: Option<String>,
127 pub conforms_to: Option<String>,
129 pub temporal_coverage_start: Option<String>,
131 pub temporal_coverage_end: Option<String>,
133 #[serde(rename = "accessRights")]
135 pub access_rights: Option<String>,
136 pub relation: Option<String>,
138}
139#[skip_serializing_none]
141#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
142#[serde(rename_all = "snake_case")]
143pub struct DefaultMeta {
144 pub title: Option<String>,
146 pub description: Option<String>,
148 pub theme: Option<Vec<String>>,
150 pub keyword: Option<Vec<String>>,
152 pub license: Option<String>,
154 #[validate(url)]
156 pub license_url: Option<String>,
157 pub language: Option<String>,
159 pub metadata_languages: Option<Vec<String>>,
161 pub timezone: Option<String>,
163 pub modified: Option<String>,
165 pub modified_updates_on_metadata_change: Option<bool>,
167 pub modified_updates_on_data_change: Option<bool>,
169 pub data_processed: Option<String>,
171 pub metadata_processed: Option<String>,
173 pub geographic_reference: Option<String>,
175 pub geographic_reference_auto: Option<bool>,
177 pub territory: Option<String>,
179 pub geometry_types: Option<Vec<String>>,
181 pub bbox: Option<String>,
183 pub publisher: Option<String>,
185 pub references: Option<String>,
187 pub records_count: Option<u64>,
189 pub attributions: Option<String>,
191 pub source_domain: Option<String>,
193 pub source_domain_title: Option<String>,
195 pub source_domain_address: Option<String>,
197 pub source_dataset: Option<String>,
199 pub shared_catalog: Option<String>,
201 pub federated: Option<bool>,
203 pub parent_domain: Option<String>,
205 pub update_frequency: Option<String>,
207}
208#[skip_serializing_none]
210#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
211pub struct DublinCore {
212 pub title: Option<String>,
214 pub alternative: Option<String>,
216 pub subject: Option<Vec<String>>,
218 pub description: Option<String>,
220 #[serde(rename = "abstract")]
222 pub abstract_text: Option<String>,
223 #[serde(rename = "tableOfContent")]
225 pub table_of_content: Option<String>,
226 #[serde(rename = "type")]
228 pub kind: Option<DublinCoreType>,
229 pub language: Option<String>,
231 pub coverage: Option<String>,
233 pub spatial: Option<String>,
235 #[serde(rename = "temporal_start")]
237 pub temporal_start: Option<String>,
238 #[serde(rename = "temporal_end")]
240 pub temporal_end: Option<String>,
241 pub relation: Option<String>,
243 pub source: Option<String>,
245 pub replaces: Option<String>,
247 pub references: Option<String>,
249 pub requires: Option<String>,
251 #[serde(rename = "conformsTo")]
253 pub conforms_to: Option<String>,
254 #[serde(rename = "hasFormat")]
256 pub has_format: Option<String>,
257 #[serde(rename = "hasPart")]
259 pub has_part: Option<String>,
260 #[serde(rename = "hasVersion")]
262 pub has_version: Option<String>,
263 #[serde(rename = "isFormatOf")]
265 pub is_format_of: Option<String>,
266 #[serde(rename = "isPartOf")]
268 pub is_part_of: Option<String>,
269 #[serde(rename = "isVersionOf")]
271 pub is_version_of: Option<String>,
272 #[serde(rename = "isReferencedBy")]
274 pub is_referenced_by: Option<String>,
275 #[serde(rename = "isReplacedBy")]
277 pub is_replaced_by: Option<String>,
278 #[serde(rename = "isRequiredBy")]
280 pub is_required_by: Option<String>,
281 pub contributor: Option<Vec<String>>,
283 pub creator: Option<String>,
285 pub publisher: Option<String>,
287 pub rights: Option<String>,
289 #[serde(rename = "accessRights")]
291 pub access_rights: Option<String>,
292 pub license: Option<String>,
294 #[serde(rename = "date_start")]
296 pub date_start: Option<String>,
297 #[serde(rename = "date_end")]
299 pub date_end: Option<String>,
300 #[serde(rename = "available_start")]
302 pub available_start: Option<String>,
303 #[serde(rename = "available_end")]
305 pub available_end: Option<String>,
306 pub created: Option<String>,
308 #[serde(rename = "dateAccepted")]
310 pub date_accepted: Option<String>,
311 #[serde(rename = "dateCopyrighted")]
313 pub date_copyrighted: Option<String>,
314 #[serde(rename = "dateSubmitted")]
316 pub date_submitted: Option<String>,
317 pub issued: Option<String>,
319 pub modified: Option<String>,
321 #[serde(rename = "valid_start")]
323 pub valid_start: Option<String>,
324 #[serde(rename = "valid_end")]
326 pub valid_end: Option<String>,
327 pub format: Option<String>,
329 pub extent: Option<String>,
331 pub medium: Option<String>,
333 pub identifier: Option<String>,
335 #[serde(rename = "bibliographicCitation")]
337 pub bibliographic_citation: Option<String>,
338 #[serde(rename = "rightsHolder")]
340 pub rights_holder: Option<String>,
341 #[serde(rename = "accrualMethod")]
343 pub accrual_method: Option<String>,
344 #[serde(rename = "accrualPeriodicity")]
346 pub accrual_periodicity: Option<String>,
347 #[serde(rename = "accrualPolicy")]
349 pub accrual_policy: Option<String>,
350 pub audience: Option<String>,
352 #[serde(rename = "educationLevel")]
354 pub education_level: Option<String>,
355 #[serde(rename = "instructionalMethod")]
357 pub instructional_method: Option<String>,
358 pub mediator: Option<String>,
360 pub provenance: Option<String>,
362}
363#[skip_serializing_none]
365#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
366#[serde(rename_all = "snake_case")]
367pub struct DcatAp {
368 pub title: Option<String>,
370 pub description: Option<String>,
372 pub theme: Option<String>,
374 pub keyword: Option<Vec<String>>,
376 pub contact_name: Option<String>,
378 #[validate(email)]
380 pub contact_email: Option<String>,
381 pub publisher_name: Option<String>,
383 pub publisher_type: Option<String>,
385 pub spatial_bbox: Option<String>,
387 pub spatial_centroid: Option<String>,
389 #[serde(rename = "temporal_startDate")]
391 pub temporal_start_date: Option<String>,
392 #[serde(rename = "temporal_endDate")]
394 pub temporal_end_date: Option<String>,
395 #[serde(rename = "accrualPeriodicity")]
397 pub accrual_periodicity: Option<String>,
398}
399#[skip_serializing_none]
401#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
402#[serde(rename_all = "kebab-case")]
403pub struct Datacite {
404 pub identifier: Option<String>,
406 pub title: Option<String>,
408 pub alternative_title: Option<String>,
410 pub publisher: Option<String>,
412 pub creator: Option<Value>,
414 pub publication_year: Option<String>,
416 pub subject: Option<Vec<String>>,
418 pub contributor: Option<Value>,
420 pub date: Option<Value>,
422 pub language: Option<String>,
424 pub resource_type: Option<Value>,
426 pub alternate_identifier: Option<String>,
428 #[serde(rename = "relatedidentifier")]
430 pub related_identifier: Option<Value>,
431 pub size: Option<String>,
433 pub format: Option<String>,
435 pub version: Option<String>,
437 pub rights: Option<Value>,
439 pub description: Option<String>,
441 pub geolocation: Option<String>,
443}
444impl TryFrom<datacite::Record> for Dataset {
445 type Error = CrosswalkError;
446
447 fn try_from(record: datacite::Record) -> Result<Self, Self::Error> {
448 let mapping = datacite_to_huwise();
449 crosswalk::convert(&record, &mapping).map(|(dataset, _)| dataset)
450 }
451}
452impl TryFrom<&datacite::Record> for Dataset {
453 type Error = CrosswalkError;
454
455 fn try_from(record: &datacite::Record) -> Result<Self, Self::Error> {
456 Dataset::try_from(record.clone())
457 }
458}
459impl SchemaBuilder for Dataset {
460 fn build_from_fields(fields: &Fields) -> Result<Self, CrosswalkError> {
461 let dataset_id = fields.get_string("identifier")?;
462 let datacite_meta = Some(build_datacite_block(fields));
463 let meta = Meta {
464 datacite: datacite_meta,
465 r#default: None,
466 dublin_core: None,
467 dcat: None,
468 dcat_ap: None,
469 custom_template: None,
470 };
471 Ok(Dataset {
472 dataset_id,
473 has_attachments: false,
474 attachments_count: 0,
475 has_records: false,
476 fields: Value::Array(vec![]),
477 metas: meta,
478 features: vec![],
479 })
480 }
481}
482fn build_datacite_block(fields: &Fields) -> Datacite {
483 let title = fields.get_string_opt("title");
484 let identifier = fields.get_string_opt("identifier");
485 let creator = fields
486 .get_string_vec_opt("creators")
487 .map(|names| Value::Array(names.into_iter().map(Value::String).collect()));
488 let publication_year = fields.get_number_opt("publication-year").map(|y| (y as i32).to_string());
489 let description = fields.get_string_opt("description");
490 let subject = fields.get_string_vec_opt("subjects");
491 let language = fields.get_string_opt("language");
492 let publisher = fields.get_string_opt("publisher");
493 let resource_type = fields.get_string_opt("resource-type").map(Value::String);
494 let rights = fields.get_string_opt("license").map(Value::String);
495 let version = fields.get_string_opt("version");
496 Datacite {
497 identifier,
498 title,
499 alternative_title: None,
500 publisher,
501 creator,
502 publication_year,
503 date: None,
504 subject,
505 contributor: None,
506 description,
507 language,
508 resource_type,
509 alternate_identifier: None,
510 related_identifier: None,
511 size: None,
512 format: None,
513 version,
514 rights,
515 geolocation: None,
516 }
517}
518impl SchemaExtractor for Dataset {
519 fn extract_fields(&self) -> Fields {
520 fn extract_json_string_array(value: &Value) -> Option<Vec<String>> {
521 match value {
522 | Value::Array(arr) => {
523 let strings: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
524 if !strings.is_empty() {
525 Some(strings)
526 } else {
527 None
528 }
529 }
530 | Value::String(s) => Some(vec![s.clone()]),
531 | _ => None,
532 }
533 }
534 let mut fields = Fields::new();
535 fields.insert("identifier", FieldValue::String(self.dataset_id.clone()));
536 if let Some(meta) = &self.metas.datacite {
537 if let Some(value) = &meta.title {
538 fields.insert("title", FieldValue::String(value.clone()));
539 }
540 if let Some(value) = &meta.identifier {
541 fields.insert("doi", FieldValue::String(value.clone()));
542 }
543 if let Some(value) = &meta.creator {
544 if let Some(creators) = extract_json_string_array(value) {
545 if !creators.is_empty() {
546 fields.insert("creators", FieldValue::StringVec(creators));
547 }
548 }
549 }
550 if let Some(value) = &meta.publication_year {
551 if let Ok(year) = value.parse::<f64>() {
552 fields.insert("publication-year", FieldValue::Number(year));
553 }
554 }
555 if let Some(value) = &meta.description {
556 fields.insert("description", FieldValue::String(value.clone()));
557 }
558 if let Some(value) = &meta.subject {
559 if !value.is_empty() {
560 fields.insert("subjects", FieldValue::StringVec(value.clone()));
561 }
562 }
563 if let Some(value) = &meta.language {
564 fields.insert("language", FieldValue::String(value.clone()));
565 }
566 if let Some(value) = &meta.publisher {
567 fields.insert("publisher", FieldValue::String(value.clone()));
568 }
569 if let Some(value) = &meta.resource_type {
570 if let Some(s) = value.as_str() {
571 fields.insert("resource-type", FieldValue::String(s.to_string()));
572 }
573 }
574 if let Some(value) = &meta.rights {
575 if let Some(s) = value.as_str() {
576 fields.insert("license", FieldValue::String(s.to_string()));
577 }
578 }
579 if let Some(value) = &meta.version {
580 fields.insert("version", FieldValue::String(value.clone()));
581 }
582 } else if let Some(meta) = &self.metas.r#default {
583 if let Some(title) = &meta.title {
584 fields.insert("title", FieldValue::String(title.clone()));
585 }
586 if let Some(value) = &meta.description {
587 let sanitized = clean(value);
588 fields.insert("description", FieldValue::String(sanitized));
589 }
590 if let Some(value) = &meta.keyword {
591 if !value.is_empty() {
592 fields.insert("subjects", FieldValue::StringVec(value.clone()));
593 }
594 }
595 if let Some(value) = &meta.language {
596 fields.insert("language", FieldValue::String(value.clone()));
597 }
598 if let Some(value) = &meta.publisher {
599 fields.insert("publisher", FieldValue::String(value.clone()));
600 }
601 if let Some(value) = &meta.license {
602 fields.insert("license", FieldValue::String(value.clone()));
603 }
604 if let Some(value) = &meta.modified {
605 fields.insert("updated", FieldValue::Date(value.clone()));
606 }
607 } else if let Some(meta) = &self.metas.dublin_core {
608 if let Some(value) = &meta.title {
609 fields.insert("title", FieldValue::String(value.clone()));
610 }
611 if let Some(value) = &meta.abstract_text {
612 let sanitized = clean(value);
613 fields.insert("description", FieldValue::String(sanitized));
614 }
615 if let Some(value) = &meta.creator {
616 fields.insert("creators", FieldValue::String(value.clone()));
617 }
618 if let Some(value) = &meta.subject {
619 if !value.is_empty() {
620 fields.insert("subjects", FieldValue::StringVec(value.clone()));
621 }
622 }
623 if let Some(value) = &meta.language {
624 fields.insert("language", FieldValue::String(value.clone()));
625 }
626 if let Some(value) = &meta.publisher {
627 fields.insert("publisher", FieldValue::String(value.clone()));
628 }
629 if let Some(value) = &meta.issued {
630 fields.insert("publication-year", FieldValue::Date(value.clone()));
631 }
632 if let Some(value) = &meta.license {
633 fields.insert("license", FieldValue::String(value.clone()));
634 }
635 } else if let Some(meta) = &self.metas.dcat {
636 if let Some(value) = &meta.creator {
637 fields.insert("creators", FieldValue::String(value.clone()));
638 }
639 if let Some(value) = &meta.issued {
640 fields.insert("publication-year", FieldValue::Date(value.clone()));
641 }
642 if let Some(value) = &meta.spatial {
643 fields.insert("spatial", FieldValue::String(value.clone()));
644 }
645 } else if let Some(meta) = &self.metas.dcat_ap {
646 if let Some(value) = &meta.title {
647 fields.insert("title", FieldValue::String(value.clone()));
648 }
649 if let Some(value) = &meta.description {
650 let sanitized = clean(value);
651 fields.insert("description", FieldValue::String(sanitized));
652 }
653 if let Some(value) = &meta.keyword {
654 if !value.is_empty() {
655 fields.insert("subjects", FieldValue::StringVec(value.clone()));
656 }
657 }
658 if let Some(value) = &meta.publisher_name {
659 fields.insert("publisher", FieldValue::String(value.clone()));
660 }
661 }
662 fields
663 }
664}
665impl ToProse for Dataset {
666 fn to_prose(&self) -> String {
667 self.metas
668 .datacite
669 .iter()
670 .flat_map(|meta| meta.title.iter().cloned())
671 .chain(self.metas.datacite.iter().flat_map(|meta| meta.description.iter().cloned()))
672 .chain(self.metas.datacite.iter().flat_map(|meta| meta.subject.iter().flatten().cloned()))
673 .chain(self.metas.r#default.iter().flat_map(|meta| meta.title.iter().cloned()))
674 .chain(
675 self.metas
676 .r#default
677 .iter()
678 .flat_map(|meta| meta.description.iter().map(|value| clean(value))),
679 )
680 .chain(self.metas.r#default.iter().flat_map(|meta| meta.keyword.iter().flatten().cloned()))
681 .chain(self.metas.dublin_core.iter().flat_map(|meta| meta.title.iter().cloned()))
682 .chain(
683 self.metas
684 .dublin_core
685 .iter()
686 .flat_map(|meta| meta.description.iter().map(|value| clean(value))),
687 )
688 .chain(
689 self.metas
690 .dublin_core
691 .iter()
692 .flat_map(|meta| meta.abstract_text.iter().map(|value| clean(value))),
693 )
694 .chain(self.metas.dublin_core.iter().flat_map(|meta| meta.subject.iter().flatten().cloned()))
695 .chain(self.metas.dcat_ap.iter().flat_map(|meta| meta.title.iter().cloned()))
696 .chain(
697 self.metas
698 .dcat_ap
699 .iter()
700 .flat_map(|meta| meta.description.iter().map(|value| clean(value))),
701 )
702 .chain(self.metas.dcat_ap.iter().flat_map(|meta| meta.keyword.iter().flatten().cloned()))
703 .collect::<Vec<String>>()
704 .join("\n\n")
705 }
706}
707#[cfg(feature = "std")]
708impl InputOutput for Dataset {
709 fn read(path: impl Into<PathBuf>) -> ApiResult<Dataset> {
710 let source = path.into();
711 match MimeType::from(source.display().to_string()) {
712 | MimeType::Json => Dataset::read_json(source),
713 | MimeType::Yaml => Dataset::read_yaml(source),
714 | _ => Err(eyre!("Unsupported HuWise data file extension")),
715 }
716 }
717 fn read_json(path: PathBuf) -> ApiResult<Dataset> {
718 #[derive(Deserialize)]
719 #[serde(untagged)]
720 enum JsonInput {
721 One(Box<Dataset>),
722 Many(Vec<Dataset>),
723 }
724
725 read_file(path).and_then(|content| {
726 serde_json::from_str::<JsonInput>(&content)
727 .map_err(|why| eyre!("Failed to parse JSON HuWise dataset — {why}"))
728 .and_then(|value| match value {
729 | JsonInput::One(dataset) => Ok(*dataset),
730 | JsonInput::Many(datasets) => match datasets.len() {
731 | 1 => datasets
732 .into_iter()
733 .next()
734 .ok_or_else(|| eyre!("Expected one HuWise dataset but found none")),
735 | len => Err(eyre!("Expected one HuWise dataset but found {len}")),
736 },
737 })
738 })
739 }
740 fn read_yaml(path: PathBuf) -> ApiResult<Dataset> {
741 #[derive(Deserialize)]
742 #[serde(untagged)]
743 enum YamlInput {
744 One(Box<Dataset>),
745 Many(Vec<Dataset>),
746 }
747
748 read_file(path).and_then(|content| {
749 serde_norway::from_str::<YamlInput>(&content)
750 .map_err(|why| eyre!("Failed to parse YAML HuWise dataset — {why}"))
751 .and_then(|value| match value {
752 | YamlInput::One(dataset) => Ok(*dataset),
753 | YamlInput::Many(datasets) => match datasets.len() {
754 | 1 => datasets
755 .into_iter()
756 .next()
757 .ok_or_else(|| eyre!("Expected one HuWise dataset but found none")),
758 | len => Err(eyre!("Expected one HuWise dataset but found {len}")),
759 },
760 })
761 })
762 }
763 fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
764 let output = path.into();
765 match MimeType::from(output.display().to_string()) {
766 | MimeType::Json => self.write_json(output),
767 | MimeType::Yaml => self.write_yaml(output),
768 | _ => Err(eyre!("Unsupported HuWise data file extension for writing")),
769 }
770 }
771 fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
772 let output = path.into().with_extension("json");
773 serde_json::to_string_pretty(self)
774 .map_err(|why| eyre!("Failed to serialize JSON HuWise dataset — {why}"))
775 .and_then(|content| write_file(output, content))
776 }
777 fn write_yaml(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
778 let output = path.into().with_extension("yaml");
779 serde_norway::to_string(self)
780 .map_err(|why| eyre!("Failed to serialize YAML HuWise dataset — {why}"))
781 .and_then(|content| write_file(output, content))
782 }
783}