Skip to main content

acorn/schema/standard/
huwise.rs

1//! HuWise dataset schema models
2//!
3#[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
30/// Collection of HuWise datasets
31pub type Catalog = Vec<Dataset>;
32/// Dublin Core type enumeration
33#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
34pub enum DublinCoreType {
35    /// Dataset entry
36    #[serde(rename = "Dataset")]
37    Dataset,
38}
39/// Custom template metadata block
40#[skip_serializing_none]
41#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
42pub struct CustomTemplate {
43    /// Projects field (custom template)
44    pub projects: Option<String>,
45    /// Source-of-data field
46    #[serde(rename = "source-of-data")]
47    pub source_of_data: Option<String>,
48}
49/// Top-level dataset container from HuWise exports
50#[skip_serializing_none]
51#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
52pub struct Dataset {
53    /// Unique dataset identifier
54    pub dataset_id: String,
55    /// Whether the dataset has attachments
56    pub has_attachments: bool,
57    /// Number of attachments
58    pub attachments_count: u64,
59    /// Whether the dataset has records
60    pub has_records: bool,
61    /// Field definitions or schema details
62    pub fields: Value,
63    /// Structured metadata blocks
64    #[validate(nested)]
65    pub metas: Meta,
66    /// Feature records associated with the dataset
67    pub features: Vec<Value>,
68}
69/// Metadata sections attached to a dataset
70#[skip_serializing_none]
71#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
72pub struct Meta {
73    /// DCAT metadata
74    #[validate(nested)]
75    pub dcat: Option<Dcat>,
76    /// Default metadata
77    #[serde(rename = "default")]
78    #[validate(nested)]
79    pub r#default: Option<DefaultMeta>,
80    /// Dublin Core metadata
81    #[serde(rename = "dublin-core")]
82    #[validate(nested)]
83    pub dublin_core: Option<DublinCore>,
84    /// DCAT-AP metadata
85    #[validate(nested)]
86    pub dcat_ap: Option<DcatAp>,
87    /// Custom template metadata
88    #[serde(rename = "custom-template")]
89    #[validate(nested)]
90    pub custom_template: Option<CustomTemplate>,
91    /// DataCite metadata
92    #[validate(nested)]
93    pub datacite: Option<Datacite>,
94}
95/// DCAT metadata block
96#[skip_serializing_none]
97#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
98#[serde(rename_all = "snake_case")]
99pub struct Dcat {
100    /// Creation timestamp
101    pub created: Option<String>,
102    /// Issued timestamp
103    pub issued: Option<String>,
104    /// Creator name
105    pub creator: Option<String>,
106    /// Contributor name
107    pub contributor: Option<String>,
108    /// Contact name
109    pub contact_name: Option<String>,
110    /// Contact email
111    #[validate(email)]
112    pub contact_email: Option<String>,
113    /// Accrual periodicity
114    #[serde(rename = "accrualperiodicity")]
115    pub accrual_periodicity: Option<String>,
116    /// Spatial coverage
117    pub spatial: Option<String>,
118    /// Temporal coverage
119    pub temporal: Option<String>,
120    /// Granularity description
121    pub granularity: Option<String>,
122    /// Data quality notes
123    #[serde(rename = "dataquality")]
124    pub data_quality: Option<String>,
125    /// Publisher type
126    pub publisher_type: Option<String>,
127    /// Conformance reference
128    pub conforms_to: Option<String>,
129    /// Temporal coverage start
130    pub temporal_coverage_start: Option<String>,
131    /// Temporal coverage end
132    pub temporal_coverage_end: Option<String>,
133    /// Access rights
134    #[serde(rename = "accessRights")]
135    pub access_rights: Option<String>,
136    /// Relation reference
137    pub relation: Option<String>,
138}
139/// Default metadata block
140#[skip_serializing_none]
141#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
142#[serde(rename_all = "snake_case")]
143pub struct DefaultMeta {
144    /// Dataset title
145    pub title: Option<String>,
146    /// Dataset description (often HTML)
147    pub description: Option<String>,
148    /// Theme labels
149    pub theme: Option<Vec<String>>,
150    /// Keyword labels
151    pub keyword: Option<Vec<String>>,
152    /// License identifier
153    pub license: Option<String>,
154    /// License URL
155    #[validate(url)]
156    pub license_url: Option<String>,
157    /// Language code
158    pub language: Option<String>,
159    /// Metadata language codes
160    pub metadata_languages: Option<Vec<String>>,
161    /// Timezone string
162    pub timezone: Option<String>,
163    /// Last modified timestamp
164    pub modified: Option<String>,
165    /// Update metadata on metadata changes
166    pub modified_updates_on_metadata_change: Option<bool>,
167    /// Update metadata on data changes
168    pub modified_updates_on_data_change: Option<bool>,
169    /// Data processed timestamp
170    pub data_processed: Option<String>,
171    /// Metadata processed timestamp
172    pub metadata_processed: Option<String>,
173    /// Geographic reference
174    pub geographic_reference: Option<String>,
175    /// Whether the geographic reference is automatic
176    pub geographic_reference_auto: Option<bool>,
177    /// Territorial reference
178    pub territory: Option<String>,
179    /// Geometry type labels
180    pub geometry_types: Option<Vec<String>>,
181    /// Bounding box definition
182    pub bbox: Option<String>,
183    /// Publisher name
184    pub publisher: Option<String>,
185    /// Reference links
186    pub references: Option<String>,
187    /// Record count
188    pub records_count: Option<u64>,
189    /// Attribution text
190    pub attributions: Option<String>,
191    /// Source domain identifier
192    pub source_domain: Option<String>,
193    /// Source domain title
194    pub source_domain_title: Option<String>,
195    /// Source domain address
196    pub source_domain_address: Option<String>,
197    /// Source dataset identifier
198    pub source_dataset: Option<String>,
199    /// Shared catalog identifier
200    pub shared_catalog: Option<String>,
201    /// Whether the dataset is federated
202    pub federated: Option<bool>,
203    /// Parent domain identifier
204    pub parent_domain: Option<String>,
205    /// Update frequency
206    pub update_frequency: Option<String>,
207}
208/// Dublin Core metadata block
209#[skip_serializing_none]
210#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
211pub struct DublinCore {
212    /// Title of the dataset
213    pub title: Option<String>,
214    /// Alternative title
215    pub alternative: Option<String>,
216    /// Subject keywords
217    pub subject: Option<Vec<String>>,
218    /// Description text (often HTML)
219    pub description: Option<String>,
220    /// Abstract summary
221    #[serde(rename = "abstract")]
222    pub abstract_text: Option<String>,
223    /// Table of content or outline
224    #[serde(rename = "tableOfContent")]
225    pub table_of_content: Option<String>,
226    /// Dublin Core type
227    #[serde(rename = "type")]
228    pub kind: Option<DublinCoreType>,
229    /// Language code
230    pub language: Option<String>,
231    /// Coverage notes
232    pub coverage: Option<String>,
233    /// Spatial coverage
234    pub spatial: Option<String>,
235    /// Temporal start
236    #[serde(rename = "temporal_start")]
237    pub temporal_start: Option<String>,
238    /// Temporal end
239    #[serde(rename = "temporal_end")]
240    pub temporal_end: Option<String>,
241    /// Relation reference
242    pub relation: Option<String>,
243    /// Source reference
244    pub source: Option<String>,
245    /// Replaces reference
246    pub replaces: Option<String>,
247    /// References value
248    pub references: Option<String>,
249    /// Requirements value
250    pub requires: Option<String>,
251    /// Conforms-to reference
252    #[serde(rename = "conformsTo")]
253    pub conforms_to: Option<String>,
254    /// Has-format reference
255    #[serde(rename = "hasFormat")]
256    pub has_format: Option<String>,
257    /// Has-part reference
258    #[serde(rename = "hasPart")]
259    pub has_part: Option<String>,
260    /// Has-version reference
261    #[serde(rename = "hasVersion")]
262    pub has_version: Option<String>,
263    /// Is-format-of reference
264    #[serde(rename = "isFormatOf")]
265    pub is_format_of: Option<String>,
266    /// Is-part-of reference
267    #[serde(rename = "isPartOf")]
268    pub is_part_of: Option<String>,
269    /// Is-version-of reference
270    #[serde(rename = "isVersionOf")]
271    pub is_version_of: Option<String>,
272    /// Is-referenced-by reference
273    #[serde(rename = "isReferencedBy")]
274    pub is_referenced_by: Option<String>,
275    /// Is-replaced-by reference
276    #[serde(rename = "isReplacedBy")]
277    pub is_replaced_by: Option<String>,
278    /// Is-required-by reference
279    #[serde(rename = "isRequiredBy")]
280    pub is_required_by: Option<String>,
281    /// Contributor names
282    pub contributor: Option<Vec<String>>,
283    /// Creator name
284    pub creator: Option<String>,
285    /// Publisher name
286    pub publisher: Option<String>,
287    /// Rights statement
288    pub rights: Option<String>,
289    /// Access rights
290    #[serde(rename = "accessRights")]
291    pub access_rights: Option<String>,
292    /// License identifier
293    pub license: Option<String>,
294    /// Date start
295    #[serde(rename = "date_start")]
296    pub date_start: Option<String>,
297    /// Date end
298    #[serde(rename = "date_end")]
299    pub date_end: Option<String>,
300    /// Availability start date
301    #[serde(rename = "available_start")]
302    pub available_start: Option<String>,
303    /// Availability end date
304    #[serde(rename = "available_end")]
305    pub available_end: Option<String>,
306    /// Created date
307    pub created: Option<String>,
308    /// Accepted date
309    #[serde(rename = "dateAccepted")]
310    pub date_accepted: Option<String>,
311    /// Copyrighted date
312    #[serde(rename = "dateCopyrighted")]
313    pub date_copyrighted: Option<String>,
314    /// Submitted date
315    #[serde(rename = "dateSubmitted")]
316    pub date_submitted: Option<String>,
317    /// Issued date
318    pub issued: Option<String>,
319    /// Modified date
320    pub modified: Option<String>,
321    /// Valid start date
322    #[serde(rename = "valid_start")]
323    pub valid_start: Option<String>,
324    /// Valid end date
325    #[serde(rename = "valid_end")]
326    pub valid_end: Option<String>,
327    /// Format identifier
328    pub format: Option<String>,
329    /// Extent string
330    pub extent: Option<String>,
331    /// Medium string
332    pub medium: Option<String>,
333    /// Identifier value
334    pub identifier: Option<String>,
335    /// Bibliographic citation
336    #[serde(rename = "bibliographicCitation")]
337    pub bibliographic_citation: Option<String>,
338    /// Rights holder
339    #[serde(rename = "rightsHolder")]
340    pub rights_holder: Option<String>,
341    /// Accrual method
342    #[serde(rename = "accrualMethod")]
343    pub accrual_method: Option<String>,
344    /// Accrual periodicity
345    #[serde(rename = "accrualPeriodicity")]
346    pub accrual_periodicity: Option<String>,
347    /// Accrual policy
348    #[serde(rename = "accrualPolicy")]
349    pub accrual_policy: Option<String>,
350    /// Audience
351    pub audience: Option<String>,
352    /// Education level
353    #[serde(rename = "educationLevel")]
354    pub education_level: Option<String>,
355    /// Instructional method
356    #[serde(rename = "instructionalMethod")]
357    pub instructional_method: Option<String>,
358    /// Mediator
359    pub mediator: Option<String>,
360    /// Provenance description
361    pub provenance: Option<String>,
362}
363/// DCAT-AP metadata block
364#[skip_serializing_none]
365#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
366#[serde(rename_all = "snake_case")]
367pub struct DcatAp {
368    /// Title
369    pub title: Option<String>,
370    /// Description
371    pub description: Option<String>,
372    /// Theme label
373    pub theme: Option<String>,
374    /// Keyword labels
375    pub keyword: Option<Vec<String>>,
376    /// Contact name
377    pub contact_name: Option<String>,
378    /// Contact email
379    #[validate(email)]
380    pub contact_email: Option<String>,
381    /// Publisher name
382    pub publisher_name: Option<String>,
383    /// Publisher type
384    pub publisher_type: Option<String>,
385    /// Spatial bounding box
386    pub spatial_bbox: Option<String>,
387    /// Spatial centroid
388    pub spatial_centroid: Option<String>,
389    /// Temporal start date
390    #[serde(rename = "temporal_startDate")]
391    pub temporal_start_date: Option<String>,
392    /// Temporal end date
393    #[serde(rename = "temporal_endDate")]
394    pub temporal_end_date: Option<String>,
395    /// Accrual periodicity
396    #[serde(rename = "accrualPeriodicity")]
397    pub accrual_periodicity: Option<String>,
398}
399/// DataCite metadata block
400#[skip_serializing_none]
401#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
402#[serde(rename_all = "kebab-case")]
403pub struct Datacite {
404    /// Identifier value
405    pub identifier: Option<String>,
406    /// Title value
407    pub title: Option<String>,
408    /// Alternative title
409    pub alternative_title: Option<String>,
410    /// Publisher name
411    pub publisher: Option<String>,
412    /// Creator value
413    pub creator: Option<Value>,
414    /// Publication year
415    pub publication_year: Option<String>,
416    /// Subject keywords
417    pub subject: Option<Vec<String>>,
418    /// Contributor value
419    pub contributor: Option<Value>,
420    /// Date value
421    pub date: Option<Value>,
422    /// Language value
423    pub language: Option<String>,
424    /// Resource type
425    pub resource_type: Option<Value>,
426    /// Alternate identifier
427    pub alternate_identifier: Option<String>,
428    /// Related identifier
429    #[serde(rename = "relatedidentifier")]
430    pub related_identifier: Option<Value>,
431    /// Size string
432    pub size: Option<String>,
433    /// Format string
434    pub format: Option<String>,
435    /// Version string
436    pub version: Option<String>,
437    /// Rights statement
438    pub rights: Option<Value>,
439    /// Description text
440    pub description: Option<String>,
441    /// Geolocation value
442    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}