diff --git a/data/dokubib.hcl b/data/dokubib.hcl index 9f4b8b47..4fb8bbc6 100644 --- a/data/dokubib.hcl +++ b/data/dokubib.hcl @@ -12,8 +12,9 @@ project { en = "Bibliothek St. Moritz Dokumentation is the local history archive of the community of St. Moritz, Switzerland. It’s collection contains publications, manuscripts and audiovisual documents of the touristic development of St. Moritz" } - url "https://data.dasch.swiss/dokubib/" { - text = "Project Website" + url { + href = "https://data.dasch.swiss/dokubib/" + label = "Project Website" } how_to_cite = "Dokumentationsbibliothek St. Moritz" @@ -130,8 +131,9 @@ organization "biblio_stmoritz" { email = "doku@biblio-stmoritz.ch" name = "Dokumentationsbibliothek St. Moritz" - url "https://www.biblio-stmoritz.ch" { - text = "www.biblio-stmoritz.ch" + url { + href = "https://www.biblio-stmoritz.ch" + label = "www.biblio-stmoritz.ch" } address { diff --git a/data/hdm.hcl b/data/hdm.hcl index d959298d..f29f3c88 100644 --- a/data/hdm.hcl +++ b/data/hdm.hcl @@ -12,8 +12,9 @@ project { en = "The database documents the events that took place in the Hôtel de Musique in Bern between 1766 and 1905. The repertoire was constituted by different kinds of spectacles like theatre plays, operas, ballets, concerts, dance parties, acrobatic performances, conferences or magicians. The list reconstructs the lifely and colourful theatre culture of Bern in the 19th Century." } - url "https://admin.dasch.swiss/project/081C" { - text = "Discover Project Data" + url { + href = "https://admin.dasch.swiss/project/081C" + label = "Discover Project Data" } how_to_cite = "HdM-Bern" @@ -143,8 +144,9 @@ organization "musik_unibe" { name = "Institut für Musikwissenschaft der Universität Bern" email = "urchueguia@musik.unibe.ch" - url "https://www.musik.unibe.ch" { - text = "https://www.musik.unibe.ch" + url { + href = "https://www.musik.unibe.ch" + description = "https://www.musik.unibe.ch" } address { diff --git a/data/incunabula.hcl b/data/incunabula.hcl index 7ed894b6..46938f3e 100644 --- a/data/incunabula.hcl +++ b/data/incunabula.hcl @@ -16,8 +16,9 @@ project { de = "Eine kunstwissenschaftliche Monographie der reich bebilderten Frühdrucke in Basel - dem wichtigsten Zentrum des frühen Buchdrucks auf dem Gebiet der heutigen Schweiz - wird im vorliegenden Projekt erstmals seit über einem Jahrhundert ins Auge gefasst. Im Zentrum stehen 18 Werke aus vier verschiedenen Offizinen, welche insgesamt über 1000 Holzschnitte enthalten, die bedeutendsten überlieferten Basler Bilderfolgen des Spätmittelalters nach den massiven Zerstörungen im Zuge der Reformation. Bei den Texten handelt es sich fast ausschliesslich um deutsche und lateinische Kompilationen religiösen, didaktischen Inhalts, darunter viele zeitgenössische und in Basel entstandene, neben Übersetzungen des 15. Jahrhunderts und vollständig überarbeiteten Ausgaben bereits verbreiteter Werke. Äusserst erfolgreiche Bücher wie das Narrenschiff oder der Heilsspiegel stehen neben kaum bekannten wie den seelsorgerischen Schriften des Kartäusers Ludwig Moser und des Franziskaners Johannes Meder. Die Analyse eines umfassenden Corpus bebilderter Frühdrucke fehlt in der neueren Forschung, welche sich bezüglich der Basler Produktion vorwiegend der Untersuchung der Produzentenkreise gewidmet, die Bilder dagegen - mit Ausnahme des Narrenschiffs - wenig beachtet hat. Sehr heterogen ist auch die Erforschung der Texte, von denen ein grosser Teil unediert geblieben ist, von anderen wiederum existieren ausführlich kommentierte Faksimileausgaben. Die bisherige Bild-Text-Forschung hat sich auf das Narrenschiff fokussiert.Neben der Quellenanalyse der Bilder und Texte strebt das Projekt eine umfassende Untersuchung der Bild-Text-Bezüge unter Berücksichtigung rezeptionsästethischer Fragestellungen an. Gefragt wird nach der Funktion der Bilder für die spätmittelalterlichen visuellen und auditiven Rezipienten. Dabei wird davon ausgegangen, dass es sich bei unseren Frühdrucken ausnahmslos um kalkulierte Bild-Text-Kompilationen handelt, welche einen reflektierten und kreativen Umgang ihrer Produzenten mit den drucktechnischen Möglichkeiten des neuen Mediums voraussetzen, wie z.B. mit der Möglichkeit der vielfältigen Kontextualisierung von Bildern. Die Analyse der Bild- und Textquellen liefert eine umfassende Fallstudie zum Medienwechsel zwischen Handschrift und Frühdruck, diejenige der Bild-Text-Bezüge im Spannungsfeld zwischen mündlicher Tradierung und schriftlicher Fixierung religiöser Didaxe eine Fallstudie zur spätmittelalterlichen Rezeptionsforschung.Methodisch knüpft das Projekt an rezeptionsästhetische Konzepte und Studien der jüngeren literaturwissenschaftlichen Forschung an, welche mit ikonographischen Analysen kombiniert werden. Durch diese Erweiterung textzentrierter methodischer Ansätze treten die Texte in Bezug auf die Bilder nicht mehr als übergeordnete, unabhängige Einheiten in den Blick, sondern die Konstruktion des Werksinnes erscheint im Wechselspiel der Medien. Der traditionelle Begriff der Buchillustration wird dadurch grundlegend revidiert.Neben mehreren Aufsätzen und einer Tagung sind eine Monographie in Buchform geplant, welche sich an die Fachwelt sowie an eine interessierte Öffentlichkeit wendet, ausserdem eine Internet-Publikation der Bild- und Textquellen im Rahmen der Zusammenarbeit mit der Basler Univeristätsbibliothek." } - url "https://admin.dasch.swiss/project/3ABR_2i8QYGSIDvmP9mlEw" { - text = "Discover Project Data" + url { + href = "https://admin.dasch.swiss/project/3ABR_2i8QYGSIDvmP9mlEw" + label = "Discover Project Data" } how_to_cite = "Incunabula" @@ -207,8 +208,9 @@ grant { // reference to person or organization (1-n) funders = ["snf"] - url "https://data.snf.ch/grants/grant/120378" { - text = "https://data.snf.ch/grants/grant/120378" + url { + href = "https://data.snf.ch/grants/grant/120378" + description = "https://data.snf.ch/grants/grant/120378" } } diff --git a/src/dsp_meta/domain/convert/project.rs b/src/dsp_meta/domain/convert/project.rs index 284e67a0..2abde85a 100644 --- a/src/dsp_meta/domain/convert/project.rs +++ b/src/dsp_meta/domain/convert/project.rs @@ -1,14 +1,16 @@ use hcl::Expression; use tracing::warn; +use crate::domain::value::alternative_name::AlternativeName; +use crate::domain::value::description::Description; use crate::domain::value::discipline::Discipline; -use crate::domain::value::iso_code::IsoCode; +use crate::domain::value::keyword::Keyword; use crate::domain::value::publication::Publication; use crate::domain::value::spatial_coverage::SpacialCoverage; use crate::domain::value::temporal_coverage::TemporalCoverage; +use crate::domain::value::url::Url; use crate::domain::value::{ - AlternativeName, ContactPoint, CreatedAt, CreatedBy, Description, EndDate, HowToCite, Keyword, - LangString, Name, Shortcode, StartDate, TeaserText, URL, + ContactPoint, CreatedAt, CreatedBy, EndDate, HowToCite, Name, Shortcode, StartDate, TeaserText, }; use crate::errors::DspMetaError; @@ -145,7 +147,7 @@ const PUBLICATION_BLOCK: &str = "publication"; pub struct ExtractedProjectBlocks { pub alternative_names: Vec, pub description: Option, - pub url: Option, + pub url: Option, pub keywords: Vec, pub disciplines: Vec, pub spacial_coverages: Vec, @@ -159,7 +161,7 @@ impl TryFrom> for ExtractedProjectBlocks { fn try_from(blocks: Vec<&hcl::Block>) -> Result { let mut alternative_names: Vec = vec![]; let mut description: Option = None; - let mut url: Option = None; + let mut url: Option = None; let mut keywords: Vec = vec![]; let mut disciplines: Vec = vec![]; let mut spacial_coverages: Vec = vec![]; @@ -172,22 +174,20 @@ impl TryFrom> for ExtractedProjectBlocks { alternative_names.push(AlternativeName::try_from(block)?); } DESCRIPTION_BLOCK => { - description = if description.is_none() { - Ok(Some(Description::try_from(block)?)) - } else { - Err(DspMetaError::ParseProject( + if description.is_some() { + return Err(DspMetaError::ParseProject( "Only one 'description' block allowed.".to_string(), - )) - }? + )); + } + description = Some(Description::try_from(block)?) } URL_BLOCK => { - url = if url.is_none() { - Ok(Some(URL::try_from(block)?)) - } else { - Err(DspMetaError::ParseProject( + if url.is_some() { + return Err(DspMetaError::ParseProject( "Only one 'url' block allowed.".to_string(), - )) - }? + )); + } + url = Some(Url::try_from(block)?) } KEYWORD_BLOCK => keywords.push(Keyword::try_from(block)?), DISCIPLINE_BLOCK => disciplines.push(Discipline::try_from(block)?), @@ -215,141 +215,12 @@ impl TryFrom> for ExtractedProjectBlocks { } } -impl TryFrom<&hcl::Block> for AlternativeName { - type Error = DspMetaError; - - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() == "alternative_name" { - let mut values: Vec = vec![]; - let attrs: Vec<&hcl::Attribute> = block.body.attributes().collect(); - for attr in attrs { - values.push(LangString::try_from(attr)?) - } - Ok(AlternativeName::from(values)) - } else { - let msg = format!( - "The passed block is not named correctly. Expected 'alternative_name', however got '{}' instead.", - block.identifier.as_str() - ); - Err(DspMetaError::CreateValueObject(msg)) - } - } -} - -impl TryFrom<&hcl::Block> for Description { - type Error = DspMetaError; - - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != "description" { - let msg = format!( - "The passed block is not named correctly. Expected 'description', however got '{}' instead.", - block.identifier.as_str() - ); - return Err(DspMetaError::CreateValueObject(msg)); - } - - let mut descriptions: Vec = vec![]; - let attrs: Vec<&hcl::Attribute> = block.body.attributes().collect(); - for attr in attrs { - descriptions.push(LangString::try_from(attr)?) - } - Ok(Description::from(descriptions)) - } -} - -impl TryFrom<&hcl::Block> for URL { - type Error = DspMetaError; - - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != "url" { - let msg = format!( - "The passed block is not named correctly. Expected 'url', however got '{}' instead.", - block.identifier.as_str() - ); - return Err(DspMetaError::CreateValueObject(msg)); - } - - let url_value = block - .labels - .get(0) - .ok_or_else(|| { - DspMetaError::CreateValueObject( - "The passed url block is missing the label containing the url.".to_string(), - ) - })? - .as_str(); - - let text_value_expr = block - .body - .attributes() - .next() - .ok_or_else(|| { - DspMetaError::CreateValueObject( - "The passed url block is missing the text attribute.".to_string(), - ) - })? - .expr(); - - let text_value = match text_value_expr { - Expression::String(value) => Ok(value.to_owned()), - _ => Err(DspMetaError::CreateValueObject( - "The passed url block text attribute is not of String type.".to_string(), - )), - }?; - - Ok(URL { - value: url::Url::try_from(url_value).map_err(|_| { - DspMetaError::CreateValueObject("The passed url is not a valid url.".to_string()) - })?, - description: text_value, - }) - } -} - -impl TryFrom<&hcl::Block> for Keyword { - type Error = DspMetaError; - - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != "keyword" { - let msg = format!( - "The passed block is not named correctly. Expected 'keyword', however got '{}' instead.", - block.identifier.as_str() - ); - return Err(DspMetaError::CreateValueObject(msg)); - } - - let mut values: Vec = vec![]; - let attrs: Vec<&hcl::Attribute> = block.body.attributes().collect(); - for attr in attrs { - values.push(LangString::try_from(attr)?) - } - Ok(Keyword::from(values)) - } -} - -impl TryFrom<&hcl::Attribute> for LangString { - type Error = DspMetaError; - - fn try_from(attr: &hcl::Attribute) -> Result { - match attr.expr() { - Expression::String(value) => Ok(LangString { - iso_code: IsoCode::try_from(attr.key.as_str())?, - string: value.to_owned(), - }), - _ => Err(DspMetaError::ParseProject( - "Parse error: name needs to be a string.".to_string(), - )), - } - } -} - #[cfg(test)] mod tests { use hcl::{block, Identifier, Number}; use tracing_test::traced_test; use super::*; - use crate::domain::value::AlternativeName; #[test] fn extract_created_at() { @@ -396,23 +267,6 @@ mod tests { let blocks = vec![&input1, &input2]; let result = ExtractedProjectBlocks::try_from(blocks).unwrap(); assert_eq!(result.alternative_names.len(), 2); - - let l1 = LangString { - iso_code: IsoCode::DE, - string: "name1_de".to_owned(), - }; - let l2 = LangString { - iso_code: IsoCode::EN, - string: "name1_en".to_owned(), - }; - let l3 = LangString { - iso_code: IsoCode::FR, - string: "name1_fr".to_owned(), - }; - assert_eq!( - result.alternative_names[0], - AlternativeName::from(vec![l1, l2, l3]) - ); } #[test] @@ -427,43 +281,59 @@ mod tests { let blocks = vec![&input]; let result = ExtractedProjectBlocks::try_from(blocks).unwrap(); assert!(result.description.is_some()); + } - let l1 = LangString { - iso_code: IsoCode::DE, - string: "descr_de".to_owned(), - }; - let l2 = LangString { - iso_code: IsoCode::EN, - string: "descr_en".to_owned(), - }; - let l3 = LangString { - iso_code: IsoCode::FR, - string: "descr_fr".to_owned(), - }; - assert_eq!( - result.description.unwrap(), - Description::from(vec![l1, l2, l3]) + #[test] + fn error_on_multiple_description_blocks() { + let input1 = block!( + description { + de = "descr_de" + en = "descr_en" + fr = "descr_fr" + } ); + let input2 = block!( + description { + de = "descr_de" + en = "descr_en" + fr = "descr_fr" + } + ); + let blocks = vec![&input1, &input2]; + let result = ExtractedProjectBlocks::try_from(blocks); + assert!(result.is_err()); } #[test] - fn extract_url() { + fn extract_single_url() { let input = block!( - url "https://data.dasch.swiss/dokubib/" { - text = "Project Website" + url { + href = "https://data.dasch.swiss/dokubib/" + label = "Project Website" } ); let blocks = vec![&input]; let result = ExtractedProjectBlocks::try_from(blocks).unwrap(); - - let expected = URL::new( - "https://data.dasch.swiss/dokubib/".to_string(), - "Project Website".to_string(), - ) - .unwrap(); - assert!(result.url.is_some()); - assert_eq!(result.url.unwrap(), expected); + } + + #[test] + fn error_on_multiple_url_blocks() { + let input1 = block!( + url { + href = "https://data.dasch.swiss/dokubib/" + label = "Project Website" + } + ); + let input2 = block!( + url { + href = "https://data.dasch.swiss/dokubib/" + label = "Project Website" + } + ); + let blocks = vec![&input1, &input2]; + let result = ExtractedProjectBlocks::try_from(blocks); + assert!(result.is_err()); } #[test] @@ -486,21 +356,6 @@ mod tests { let blocks = vec![&input1, &input2]; let result = ExtractedProjectBlocks::try_from(blocks).unwrap(); assert_eq!(result.keywords.len(), 2); - - let l1 = LangString { - iso_code: IsoCode::DE, - string: "keyword1_de".to_owned(), - }; - let l2 = LangString { - iso_code: IsoCode::EN, - string: "keyword1_en".to_owned(), - }; - let l3 = LangString { - iso_code: IsoCode::FR, - string: "keyword1_fr".to_owned(), - }; - - assert_eq!(result.keywords[0], Keyword::from(vec![l1, l2, l3])); } #[test] diff --git a/src/dsp_meta/domain/entity/project.rs b/src/dsp_meta/domain/entity/project.rs index 721421b7..0f5ffeb7 100644 --- a/src/dsp_meta/domain/entity/project.rs +++ b/src/dsp_meta/domain/entity/project.rs @@ -1,9 +1,12 @@ use crate::domain::convert::project::{ExtractedProjectAttributes, ExtractedProjectBlocks}; +use crate::domain::value::alternative_name::AlternativeName; +use crate::domain::value::description::Description; use crate::domain::value::discipline::Discipline; +use crate::domain::value::keyword::Keyword; use crate::domain::value::publication::Publication; +use crate::domain::value::url::Url; use crate::domain::value::{ - AlternativeName, ContactPoint, CreatedAt, CreatedBy, Description, EndDate, HowToCite, Keyword, - Name, Shortcode, StartDate, TeaserText, URL, + ContactPoint, CreatedAt, CreatedBy, EndDate, HowToCite, Name, Shortcode, StartDate, TeaserText, }; use crate::errors::DspMetaError; @@ -16,7 +19,7 @@ pub struct Project { pub alternative_names: Vec, pub teaser_text: TeaserText, pub description: Description, - pub url: URL, + pub url: Url, pub how_to_cite: HowToCite, pub start_date: StartDate, pub end_date: Option, @@ -103,7 +106,7 @@ impl TryFrom<&hcl::Block> for Project { "Parse error: project needs to have a description.".to_string(), ) })?; - let url = URL::default(); + let url = Url::default(); let keywords = vec![]; let disciplines = vec![]; let publications = vec![]; diff --git a/src/dsp_meta/domain/value/alternative_name.rs b/src/dsp_meta/domain/value/alternative_name.rs new file mode 100644 index 00000000..bd1c89b4 --- /dev/null +++ b/src/dsp_meta/domain/value/alternative_name.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +use crate::domain::value::iso_code::IsoCode; +use crate::domain::value::lang_text_data::LangTextData; +use crate::errors::DspMetaError; + +const ALTERNATIVE_NAME_BLOCK_IDENTIFIER: &str = "alternative_name"; + +#[derive(Debug, Clone, PartialEq)] +pub struct AlternativeName(HashMap); + +impl Default for AlternativeName { + fn default() -> Self { + let mut map: HashMap = HashMap::new(); + map.insert(IsoCode::DE, String::from("Der Default AlternativeName.")); + map.insert(IsoCode::EN, String::from("The default AlternativeName.")); + map.insert(IsoCode::FR, String::from("Le default AlternativeName.")); + Self(map) + } +} + +impl TryFrom<&hcl::Block> for AlternativeName { + type Error = DspMetaError; + + fn try_from(block: &hcl::Block) -> Result { + if block.identifier.as_str() != ALTERNATIVE_NAME_BLOCK_IDENTIFIER { + let msg = format!( + "The passed block is not named correctly. Expected '{}', however got '{}' instead.", + ALTERNATIVE_NAME_BLOCK_IDENTIFIER, + block.identifier.as_str() + ); + return Err(DspMetaError::CreateValueObject(msg)); + } + + let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + LangTextData::try_from(attributes).map(|lang_text_data| AlternativeName(lang_text_data.0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_try_from_correct_block() { + let block = hcl::block!( + alternative_name { + de = "Der alternative Name" + en = "The alternative name" + fr = "Le alternative name" + } + ); + + let alternative_name = AlternativeName::try_from(&block).unwrap(); + + let mut map: HashMap = HashMap::new(); + map.insert(IsoCode::DE, String::from("Der alternative Name")); + map.insert(IsoCode::EN, String::from("The alternative name")); + map.insert(IsoCode::FR, String::from("Le alternative name")); + let expected = AlternativeName(map); + + assert_eq!(alternative_name, expected); + } + + #[test] + fn test_try_from_incorrect_block() { + let block = hcl::block!( + alternative_name_other { + de = "Der alternative Name" + en = "The alternative name" + fr = "Le alternative name" + } + ); + + let alternative_name = AlternativeName::try_from(&block); + + assert!(alternative_name.is_err()); + } +} diff --git a/src/dsp_meta/domain/value/description.rs b/src/dsp_meta/domain/value/description.rs new file mode 100644 index 00000000..2e172e92 --- /dev/null +++ b/src/dsp_meta/domain/value/description.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use crate::domain::value::iso_code::IsoCode; +use crate::domain::value::lang_text_data::LangTextData; +use crate::errors::DspMetaError; + +const DESCRIPTION_BLOCK_IDENTIFIER: &str = "description"; + +/// A set of descriptions in different languages. +#[derive(Debug, Clone, PartialEq)] +pub struct Description(HashMap); + +impl Default for Description { + fn default() -> Self { + let mut map: HashMap = HashMap::new(); + map.insert(IsoCode::DE, String::from("Die Default-Beschreibung.")); + map.insert(IsoCode::EN, String::from("The default description.")); + map.insert(IsoCode::FR, String::from("Le standard description.")); + Self(map) + } +} + +impl TryFrom<&hcl::Block> for Description { + type Error = DspMetaError; + + fn try_from(block: &hcl::Block) -> Result { + if block.identifier.as_str() != DESCRIPTION_BLOCK_IDENTIFIER { + let msg = format!( + "The passed block is not named correctly. Expected '{}', however got '{}' instead.", + DESCRIPTION_BLOCK_IDENTIFIER, + block.identifier.as_str() + ); + return Err(DspMetaError::CreateValueObject(msg)); + } + + let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + LangTextData::try_from(attributes).map(|lang_text_data| Description(lang_text_data.0)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_try_from_correct_block() { + let block = hcl::block!( + description { + de = "Die Beschreibung" + en = "The description" + fr = "La description" + } + ); + + let description = Description::try_from(&block).unwrap(); + + let mut map = HashMap::new(); + map.insert(IsoCode::DE, String::from("Die Beschreibung")); + map.insert(IsoCode::EN, String::from("The description")); + map.insert(IsoCode::FR, String::from("La description")); + let expected = Description(map); + + assert_eq!(description, expected); + } + + #[test] + fn test_try_from_incorrect_block() { + let block = hcl::block!( + description_other { + de = "Die Beschreibung" + en = "The description" + fr = "La description" + } + ); + + let description = Description::try_from(&block); + + assert!(description.is_err()); + } +} diff --git a/src/dsp_meta/domain/value/keyword.rs b/src/dsp_meta/domain/value/keyword.rs new file mode 100644 index 00000000..190260e7 --- /dev/null +++ b/src/dsp_meta/domain/value/keyword.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use crate::domain::value::iso_code::IsoCode; +use crate::domain::value::lang_text_data::LangTextData; +use crate::errors::DspMetaError; + +const KEYWORD_BLOCK_IDENTIFIER: &str = "keyword"; + +#[derive(Debug, Default, PartialEq)] +pub struct Keyword(HashMap); + +impl TryFrom<&hcl::Block> for Keyword { + type Error = DspMetaError; + + fn try_from(block: &hcl::Block) -> Result { + if block.identifier.as_str() != KEYWORD_BLOCK_IDENTIFIER { + let msg = format!( + "The passed block is not named correctly. Expected '{}', however got '{}' instead.", + KEYWORD_BLOCK_IDENTIFIER, + block.identifier.as_str() + ); + return Err(DspMetaError::CreateValueObject(msg)); + } + + let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + LangTextData::try_from(attributes).map(|lang_text_data| Keyword(lang_text_data.0)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_try_from_correct_block() { + let block = hcl::block!( + keyword { + de = "Der keyword" + en = "The keyword" + fr = "Le keyword" + } + ); + + let keyword = Keyword::try_from(&block).unwrap(); + + let mut map: HashMap = HashMap::new(); + map.insert(IsoCode::DE, String::from("Der keyword")); + map.insert(IsoCode::EN, String::from("The keyword")); + map.insert(IsoCode::FR, String::from("Le keyword")); + let expected = Keyword(map); + + assert_eq!(keyword, expected); + } + + #[test] + fn test_try_from_incorrect_block() { + let block = hcl::block!( + keyword_other { + de = "Der keyword" + en = "The keyword" + fr = "Le keyword" + } + ); + + let keyword = Keyword::try_from(&block); + + assert!(keyword.is_err()); + } +} diff --git a/src/dsp_meta/domain/value/lang_text_data.rs b/src/dsp_meta/domain/value/lang_text_data.rs index 5f4aad97..71e6e826 100644 --- a/src/dsp_meta/domain/value/lang_text_data.rs +++ b/src/dsp_meta/domain/value/lang_text_data.rs @@ -3,11 +3,11 @@ use std::collections::HashMap; use crate::domain::value::iso_code::IsoCode; use crate::errors::DspMetaError; +/// Represents multiple strings in different languages. #[derive(Debug, PartialEq)] pub struct LangTextData(pub HashMap); -/// Try to create the text description of the discipline -/// FIXME: Move to the API layer where the service adapter is implemented +/// FIXME: Move to the API layer where the service adapter will be implemented impl TryFrom> for LangTextData { type Error = DspMetaError; @@ -19,8 +19,7 @@ impl TryFrom> for LangTextData { let text = match attribute.expr() { hcl::Expression::String(value) => Ok(value.to_owned()), _ => Err(DspMetaError::CreateValueObject( - "The passed discipline block description attribute is not of String type." - .to_string(), + "The attribute value is not of String type.".to_string(), )), }?; diff --git a/src/dsp_meta/domain/value/mod.rs b/src/dsp_meta/domain/value/mod.rs index b52a4428..51863c5b 100644 --- a/src/dsp_meta/domain/value/mod.rs +++ b/src/dsp_meta/domain/value/mod.rs @@ -1,17 +1,15 @@ -use std::collections::HashMap; - -use iso_code::IsoCode; - -use crate::errors::DspMetaError; - +pub(crate) mod alternative_name; +pub(crate) mod description; pub(crate) mod discipline; pub(crate) mod iso_code; +pub(crate) mod keyword; mod lang_text_data; pub(crate) mod publication; mod ref_data; mod simple_text_data; pub(crate) mod spatial_coverage; pub(crate) mod temporal_coverage; +pub(crate) mod url; pub(crate) mod version; #[derive(Debug, Default, Clone, PartialEq)] @@ -29,151 +27,12 @@ pub struct Shortcode(pub String); #[derive(Debug, Default, Clone, PartialEq)] pub struct Name(pub String); -/// A HashSet of an alternative name in different languages. -/// The HashSet is used to ensure that there are no duplicate values in regards -/// to the language code, as LangString only compares the iso_code. -/// TODO: check if this is the correct data structure -#[derive(Debug, Clone, PartialEq)] -pub struct AlternativeName(HashMap); - -impl Default for AlternativeName { - fn default() -> Self { - Self::from(vec![ - LangString { - iso_code: IsoCode::DE, - string: String::from("Der Default AlternativeName."), - }, - LangString { - iso_code: IsoCode::EN, - string: String::from("The default AlternativeName."), - }, - LangString { - iso_code: IsoCode::FR, - string: String::from("Le default AlternativeName."), - }, - ]) - } -} - -impl From> for AlternativeName { - fn from(names: Vec) -> Self { - let mut map = HashMap::new(); - for name in names { - map.insert(name.iso_code, name.string); - } - Self(map) - } -} - -/// Represents a string in a specific language. -/// Equality of two language specific strings is ONLY based iso_code. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct LangString { - pub iso_code: IsoCode, - pub string: String, -} - -impl Default for LangString { - fn default() -> Self { - Self { - iso_code: IsoCode::DE, - string: String::from("Der Default LangString."), - } - } -} - #[derive(Debug, Default, Clone, PartialEq)] pub struct TeaserText(pub String); -/// A set of descriptions in different languages. -#[derive(Debug, Clone, PartialEq)] -pub struct Description(HashMap); - -impl Default for Description { - fn default() -> Self { - Self::from(vec![ - LangString { - iso_code: IsoCode::DE, - string: String::from("Die Default-Beschreibung."), - }, - LangString { - iso_code: IsoCode::EN, - string: String::from("The default description."), - }, - LangString { - iso_code: IsoCode::FR, - string: String::from("Le standard description."), - }, - ]) - } -} - -impl From> for Description { - fn from(values: Vec) -> Self { - let mut map = HashMap::new(); - for value in values { - map.insert(value.iso_code, value.string); - } - Self(map) - } -} - -/// Represents an HCL attribute which consists of an attribute key and a value expression. -/// -/// In HCL syntax this is represented as: -/// -/// ```hcl -/// url "value" { -/// text = "text" -/// } -/// ``` -/// -/// Use [`Attribute::new`] to construct an [`Attribute`] from a value that is convertible to this -/// crate's [`Expression`] type. -#[derive(Debug, PartialEq, Eq)] -pub struct URL { - pub value: url::Url, - pub description: String, -} - -impl URL { - pub fn new(value: String, description: String) -> Result { - let maybe_url = url::Url::try_from(value.as_str()); - match maybe_url { - Ok(value) => Ok(URL { value, description }), - Err(_) => Err(DspMetaError::CreateValueObject( - "Creating an UrlValue failed because provided value is not a valid URL." - .to_string(), - )), - } - } -} - -impl Default for URL { - fn default() -> Self { - URL { - value: url::Url::try_from("https://default.xyz").unwrap(), - description: "Default URL description".to_string(), - } - } -} - #[derive(Debug, Default, Clone, PartialEq)] pub struct HowToCite(pub String); -#[derive(Debug, Default, PartialEq)] -pub struct Keyword(HashMap); - -impl From> for Keyword { - fn from(values: Vec) -> Self { - let mut map = HashMap::new(); - for value in values { - map.insert(value.iso_code, value.string); - } - Self(map) - } -} - #[derive(Debug, Default, Clone, PartialEq)] pub struct StartDate(pub String); diff --git a/src/dsp_meta/domain/value/url.rs b/src/dsp_meta/domain/value/url.rs new file mode 100644 index 00000000..3354c205 --- /dev/null +++ b/src/dsp_meta/domain/value/url.rs @@ -0,0 +1,157 @@ +use tracing::warn; + +use crate::errors::DspMetaError; + +const URL_BLOCK_IDENTIFIER: &str = "url"; +const HREF_ATTRIBUTE_KEY: &str = "href"; +const LABEL_ATTRIBUTE_KEY: &str = "label"; + +/// Represents an HCL attribute which consists of an attribute key and a value expression. +/// +/// In HCL syntax this is represented as: +/// +/// ```hcl +/// url { +/// href = "https://www.google.com" +/// label = "text describing the link" +/// } +/// ``` +/// +/// Use [`Attribute::new`] to construct an [`Attribute`] from a value that is convertible to this +/// crate's [`Expression`] type. +#[derive(Debug, PartialEq, Eq)] +pub struct Url { + pub href: url::Url, + pub label: String, +} + +impl Url { + pub fn new(url_string: String, label: String) -> Result { + let maybe_url = url::Url::try_from(url_string.as_str()); + match maybe_url { + Ok(href) => Ok(Url { href, label }), + Err(_) => Err(DspMetaError::CreateValueObject( + "Creating an UrlValue failed because provided value is not a valid URL." + .to_string(), + )), + } + } +} + +impl Default for Url { + fn default() -> Self { + Url { + href: url::Url::try_from("https://default.xyz").unwrap(), + label: "Default URL description".to_string(), + } + } +} + +impl TryFrom<&hcl::Block> for Url { + type Error = DspMetaError; + + fn try_from(block: &hcl::Block) -> Result { + if block.identifier.as_str() != URL_BLOCK_IDENTIFIER { + let msg = format!( + "The passed block is not named correctly. Expected '{}', however got '{}' instead.", + URL_BLOCK_IDENTIFIER, + block.identifier.as_str() + ); + return Err(DspMetaError::CreateValueObject(msg)); + } + + let mut href: Option = None; + let mut label: Option = None; + + let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + for attribute in attributes { + match attribute.key() { + LABEL_ATTRIBUTE_KEY => { + if label.is_some() { + return Err(DspMetaError::CreateValueObject( + "The passed discipline block contains multiple description attributes." + .to_string(), + )); + } + label = match attribute.expr() { + hcl::Expression::String(value) => Ok(Some(value.to_owned())), + _ => Err(DspMetaError::CreateValueObject( + "The passed discipline block description attribute is not of String type.".to_string(), + )), + }?; + } + HREF_ATTRIBUTE_KEY => { + if href.is_some() { + return Err(DspMetaError::CreateValueObject( + "Multiple href attributes not allowed.".to_string(), + )); + } + href = match attribute.expr() { + hcl::Expression::String(value) => { + Ok(Some(url::Url::parse(value).map_err(|_| { + DspMetaError::CreateValueObject( + "The passed discipline block url attribute is not a valid url." + .to_string(), + ) + })?)) + } + _ => Err(DspMetaError::CreateValueObject( + "The value for the href attribute is not of String type.".to_string(), + )), + }?; + } + _ => { + warn!("Parse error: unknown attribute '{}'.", attribute.key()); + } + } + } + + Ok(Url { + href: href.ok_or(DspMetaError::CreateValueObject( + "The required href attribute is missing.".to_string(), + ))?, + label: label.ok_or(DspMetaError::CreateValueObject( + "The required label attribute is missing.".to_string(), + ))?, + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_try_from_correct_block() { + let block = hcl::block!( + url { + href = "https://www.google.com" + label = "Google" + } + ); + + let url = Url::try_from(&block).unwrap(); + + let expected = Url { + href: url::Url::try_from("https://www.google.com").unwrap(), + label: "Google".to_string(), + }; + + assert_eq!(url, expected); + } + + #[test] + fn test_try_from_incorrect_block() { + let block = hcl::block!( + url_other { + href = "https://www.google.com" + label = "Google" + } + ); + + let url = Url::try_from(&block); + + assert!(url.is_err()); + } +}