diff --git a/Cargo.lock b/Cargo.lock index c979f8ce..fdea7024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,6 +298,7 @@ dependencies = [ "log", "serde", "serde_json", + "thiserror", "tokio", "tower", "tower-http", diff --git a/dsp-domain/Cargo.toml b/dsp-domain/Cargo.toml index 32ba5ae4..78175223 100644 --- a/dsp-domain/Cargo.toml +++ b/dsp-domain/Cargo.toml @@ -29,6 +29,7 @@ tracing.workspace = true tracing-subscriber.workspace = true tracing-test.workspace = true url.workspace = true +thiserror = "1.0.50" [dev-dependencies] assert_cmd = "2.0.12" diff --git a/dsp-domain/src/error.rs b/dsp-domain/src/error.rs new file mode 100644 index 00000000..b1e26108 --- /dev/null +++ b/dsp-domain/src/error.rs @@ -0,0 +1,14 @@ +use thiserror::Error; + +/// Type alias for `Result` with default error `DspDomainError`. +/// +/// Can be used like `std::result::Result` as well. +pub type Result = std::result::Result; + +/// This error is raised when a domain entity or value fails creation +/// at runtime. +#[derive(Debug, Error)] +pub enum DspDomainError { + #[error("Error creating value object: `{0}`")] + CreateValueObject(String), +} diff --git a/dsp-domain/src/lib.rs b/dsp-domain/src/lib.rs index de5643fd..58d8d189 100644 --- a/dsp-domain/src/lib.rs +++ b/dsp-domain/src/lib.rs @@ -1 +1,2 @@ +pub mod error; pub mod metadata; diff --git a/dsp-domain/src/metadata/value/alternative_name.rs b/dsp-domain/src/metadata/value/alternative_name.rs index 3c48f240..f058935e 100644 --- a/dsp-domain/src/metadata/value/alternative_name.rs +++ b/dsp-domain/src/metadata/value/alternative_name.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use serde::Serialize; use crate::metadata::value::iso_code::IsoCode; +use crate::metadata::value::lang_text_data::LangTextData; #[derive(Debug, Clone, PartialEq, Serialize)] pub struct AlternativeName(pub HashMap); @@ -16,3 +17,9 @@ impl Default for AlternativeName { Self(map) } } + +impl From for AlternativeName { + fn from(value: LangTextData) -> Self { + AlternativeName(value.0) + } +} diff --git a/dsp-domain/src/metadata/value/description.rs b/dsp-domain/src/metadata/value/description.rs index 9900720e..3ec63f64 100644 --- a/dsp-domain/src/metadata/value/description.rs +++ b/dsp-domain/src/metadata/value/description.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use serde::Serialize; use crate::metadata::value::iso_code::IsoCode; +use crate::metadata::value::lang_text_data::LangTextData; /// A set of descriptions in different languages. #[derive(Debug, Clone, PartialEq, Serialize)] @@ -17,3 +18,9 @@ impl Default for Description { Self(map) } } + +impl From for Description { + fn from(value: LangTextData) -> Self { + Description(value.0) + } +} diff --git a/dsp-domain/src/metadata/value/iso_code.rs b/dsp-domain/src/metadata/value/iso_code.rs index 4ba0c54c..216812d4 100644 --- a/dsp-domain/src/metadata/value/iso_code.rs +++ b/dsp-domain/src/metadata/value/iso_code.rs @@ -2,6 +2,8 @@ use std::fmt::{Display, Formatter}; use serde::Serialize; +use crate::error::DspDomainError; + /// Language codes according to ISO 639-1 /// Not an exhaustive list. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize)] @@ -44,7 +46,7 @@ impl Display for IsoCode { } impl TryFrom<&str> for IsoCode { - type Error = String; + type Error = DspDomainError; fn try_from(value: &str) -> Result { match value { @@ -61,9 +63,9 @@ impl TryFrom<&str> for IsoCode { "zh" => Ok(IsoCode::ZH), "ar" => Ok(IsoCode::AR), "fa" => Ok(IsoCode::FA), - _ => { - Err("Creating an IsoCode failed because provided value is not allowed.".to_string()) - } + _ => Err(DspDomainError::CreateValueObject( + "Creating an IsoCode failed because provided value is not allowed.".to_string(), + )), } } } diff --git a/dsp-domain/src/metadata/value/keyword.rs b/dsp-domain/src/metadata/value/keyword.rs index 01ee48dc..d63a4bb3 100644 --- a/dsp-domain/src/metadata/value/keyword.rs +++ b/dsp-domain/src/metadata/value/keyword.rs @@ -3,6 +3,13 @@ use std::collections::HashMap; use serde::Serialize; use crate::metadata::value::iso_code::IsoCode; +use crate::metadata::value::lang_text_data::LangTextData; #[derive(Clone, Debug, Default, PartialEq, Serialize)] pub struct Keyword(pub HashMap); + +impl From for Keyword { + fn from(value: LangTextData) -> Self { + Keyword(value.0) + } +} diff --git a/dsp-domain/src/metadata/value/publication.rs b/dsp-domain/src/metadata/value/publication.rs index 8f0d4cb2..07202d9e 100644 --- a/dsp-domain/src/metadata/value/publication.rs +++ b/dsp-domain/src/metadata/value/publication.rs @@ -6,3 +6,4 @@ use crate::metadata::value::simple_text_data::SimpleTextData; pub enum Publication { SimpleText(SimpleTextData), } + diff --git a/dsp-domain/src/metadata/value/simple_text_data.rs b/dsp-domain/src/metadata/value/simple_text_data.rs index 8b5d4190..9921cc71 100644 --- a/dsp-domain/src/metadata/value/simple_text_data.rs +++ b/dsp-domain/src/metadata/value/simple_text_data.rs @@ -1,4 +1,12 @@ use serde::Serialize; +use crate::metadata::value::publication::Publication; + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct SimpleTextData(pub String); + +impl SimpleTextData { + pub fn into_simple_text(self) -> Publication { + Publication::SimpleText(self) + } +} diff --git a/dsp-meta/src/api/convert/axum/project_metadata.rs b/dsp-meta/src/api/convert/axum/project_metadata.rs index c41d27dd..22a2bd56 100644 --- a/dsp-meta/src/api/convert/axum/project_metadata.rs +++ b/dsp-meta/src/api/convert/axum/project_metadata.rs @@ -4,18 +4,12 @@ use dsp_domain::metadata::entity::project_metadata::ProjectMetadata; use serde::Serialize; #[derive(Debug, Default, Clone, PartialEq, Serialize)] -pub struct OptionalProjectMetadata(pub Option); +pub struct ProjectMetadataDto(pub Option); -impl IntoResponse for ProjectMetadata { - fn into_response(self) -> Response { - (StatusCode::OK, Json(serde_json::to_value(self).unwrap())).into_response() - } -} - -impl IntoResponse for OptionalProjectMetadata { +impl IntoResponse for ProjectMetadataDto { fn into_response(self) -> Response { match self.0 { - Some(result) => result.into_response(), + Some(pm) => (StatusCode::OK, Json(serde_json::to_value(pm).unwrap())).into_response(), None => (StatusCode::NOT_FOUND).into_response(), } } diff --git a/dsp-meta/src/api/convert/hcl/dataset.rs b/dsp-meta/src/api/convert/hcl/dataset.rs index 92c4a64e..556a8b66 100644 --- a/dsp-meta/src/api/convert/hcl/dataset.rs +++ b/dsp-meta/src/api/convert/hcl/dataset.rs @@ -1,23 +1,24 @@ use dsp_domain::metadata::entity::dataset::Dataset; use dsp_domain::metadata::value::Title; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; -impl TryFrom<&hcl::Block> for Dataset { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(dataset_block: &hcl::Block) -> Result { - if dataset_block.identifier.as_str() != "dataset" { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != "dataset" { return Err(DspMetaError::ParseDataset( format!( "Parse error: dataset block needs to be named 'dataset', however got '{}' instead.", - dataset_block.identifier.as_str() + self.0.identifier.as_str() ) - .to_string(), + .to_string(), )); } let title = Title(String::from("TODO: implement title extraction")); - Ok(Self { title }) + Ok(Dataset { title }) } } diff --git a/dsp-meta/src/api/convert/hcl/extracted_project_blocks.rs b/dsp-meta/src/api/convert/hcl/extracted_project_blocks.rs index afb2597a..2b079937 100644 --- a/dsp-meta/src/api/convert/hcl/extracted_project_blocks.rs +++ b/dsp-meta/src/api/convert/hcl/extracted_project_blocks.rs @@ -47,17 +47,14 @@ impl TryFrom> for ExtractedProjectBlocks { for block in blocks { match block.identifier.as_str() { - ALTERNATIVE_NAME_BLOCK => { - // alternative_names.push(AlternativeName::try_from(block)?); - alternative_names.push(HclBlock(block).try_into()?) - } + ALTERNATIVE_NAME_BLOCK => alternative_names.push(HclBlock(block).try_into()?), DESCRIPTION_BLOCK => { if description.is_some() { return Err(DspMetaError::ParseProject( "Only one 'description' block allowed.".to_string(), )); } - description = Some(Description::try_from(block)?) + description = Some(HclBlock(&block).try_into()?) } URL_BLOCK => { if url.is_some() { @@ -65,15 +62,13 @@ impl TryFrom> for ExtractedProjectBlocks { "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)?), - SPACIAL_COVERAGE_BLOCK => spacial_coverages.push(SpacialCoverage::try_from(block)?), - TEMPORAL_COVERAGE_BLOCK => { - temporal_coverages.push(TemporalCoverage::try_from(block)?) + url = Some(HclBlock(&block).try_into()?) } - PUBLICATION_BLOCK => publications.push(Publication::try_from(block)?), + KEYWORD_BLOCK => keywords.push(HclBlock(&block).try_into()?), + DISCIPLINE_BLOCK => disciplines.push(HclBlock(&block).try_into()?), + SPACIAL_COVERAGE_BLOCK => spacial_coverages.push(HclBlock(&block).try_into()?), + TEMPORAL_COVERAGE_BLOCK => temporal_coverages.push(HclBlock(&block).try_into()?), + PUBLICATION_BLOCK => publications.push(HclBlock(&block).try_into()?), _ => { // catch all warn!("Parse error: unknown block '{}'.", block.identifier); diff --git a/dsp-meta/src/api/convert/hcl/hcl_attribute.rs b/dsp-meta/src/api/convert/hcl/hcl_attribute.rs new file mode 100644 index 00000000..ec18163b --- /dev/null +++ b/dsp-meta/src/api/convert/hcl/hcl_attribute.rs @@ -0,0 +1,2 @@ +pub struct HclAttribute<'a>(pub &'a hcl::Attribute); +pub struct HclAttributes<'a>(pub Vec<&'a hcl::Attribute>); diff --git a/dsp-meta/src/api/convert/hcl/hcl_body.rs b/dsp-meta/src/api/convert/hcl/hcl_body.rs new file mode 100644 index 00000000..83f2889c --- /dev/null +++ b/dsp-meta/src/api/convert/hcl/hcl_body.rs @@ -0,0 +1 @@ +pub struct HclBody<'a>(pub &'a hcl::Block); diff --git a/dsp-meta/src/api/convert/hcl/mod.rs b/dsp-meta/src/api/convert/hcl/mod.rs index 157f7493..1f78c7b6 100644 --- a/dsp-meta/src/api/convert/hcl/mod.rs +++ b/dsp-meta/src/api/convert/hcl/mod.rs @@ -2,7 +2,9 @@ mod dataset; mod dsp_meta_error; mod extracted_project_attributes; mod extracted_project_blocks; +pub mod hcl_attribute; +mod hcl_block; +pub mod hcl_body; mod project; pub(crate) mod project_metadata; mod value; -mod hcl_block; diff --git a/dsp-meta/src/api/convert/hcl/project.rs b/dsp-meta/src/api/convert/hcl/project.rs index 1e2691d6..91804e0d 100644 --- a/dsp-meta/src/api/convert/hcl/project.rs +++ b/dsp-meta/src/api/convert/hcl/project.rs @@ -3,17 +3,18 @@ use dsp_domain::metadata::value::url::Url; use crate::api::convert::hcl::extracted_project_attributes::ExtractedProjectAttributes; use crate::api::convert::hcl::extracted_project_blocks::ExtractedProjectBlocks; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; -impl TryFrom<&hcl::Block> for Project { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(project_block: &hcl::Block) -> Result { - if project_block.identifier.as_str() != "project" { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != "project" { return Err(DspMetaError::ParseProject( format!( "Parse error: project block needs to be named 'project', however got '{}' instead.", - project_block.identifier.as_str() + self.0.identifier.as_str() ) .to_string(), )); @@ -23,7 +24,7 @@ impl TryFrom<&hcl::Block> for Project { // created_at, created_by, shortcode, name, teaser_text, how_to_cite, start_date, end_date, // datasets, funders, grants - let attributes: Vec<&hcl::Attribute> = project_block.body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); let extracted_attributes = ExtractedProjectAttributes::try_from(attributes)?; @@ -73,7 +74,7 @@ impl TryFrom<&hcl::Block> for Project { // extract the project blocks // alternative_names, description, url, keywords, disciplines, publications) - let blocks: Vec<&hcl::Block> = project_block.body.blocks().collect(); + let blocks: Vec<&hcl::Block> = self.0.body.blocks().collect(); let extracted_blocks = ExtractedProjectBlocks::try_from(blocks)?; let alternative_names = extracted_blocks.alternative_names; @@ -144,7 +145,7 @@ mod tests { contact_point = "project_organization" } ); - let project = Project::try_from(&input_project_block).unwrap(); + let project: Project = HclBlock(&input_project_block).try_into().unwrap(); assert_eq!(project.created_at, CreatedAt(1630601274523025000)); assert_eq!( project.created_by, diff --git a/dsp-meta/src/api/convert/hcl/project_metadata.rs b/dsp-meta/src/api/convert/hcl/project_metadata.rs index 40e55fbe..0c14ba02 100644 --- a/dsp-meta/src/api/convert/hcl/project_metadata.rs +++ b/dsp-meta/src/api/convert/hcl/project_metadata.rs @@ -3,29 +3,32 @@ use dsp_domain::metadata::entity::project::Project; use dsp_domain::metadata::entity::project_metadata::ProjectMetadata; use dsp_domain::metadata::value::version::Version; +use crate::api::convert::hcl::hcl_attribute::HclAttribute; +use crate::api::convert::hcl::hcl_block::HclBlock; +use crate::api::convert::hcl::hcl_body::HclBody; use crate::error::DspMetaError; -impl TryFrom<&hcl::Body> for ProjectMetadata { +impl<'a> TryInto for HclBody<'a> { type Error = DspMetaError; /// Converts an `hcl::Body` into `ProjectMetadata` by consuming the /// input. This operation can fail. - fn try_from(body: &hcl::Body) -> Result { + fn try_into(self) -> Result { let mut version: Option = None; let mut project: Option = None; let mut datasets: Vec = vec![]; - let attributes: Vec<&hcl::Attribute> = body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); for attribute in attributes { match attribute.key() { - "version" => version = Some(Version::try_from(attribute)?), + "version" => version = Some(HclAttribute(attribute).try_into()?), _ => { continue; } } } - let blocks: Vec<&hcl::Block> = body.blocks().collect(); + let blocks: Vec<&hcl::Block> = self.0.body.blocks().collect(); for block in blocks { match block.identifier() { "project" => { @@ -34,10 +37,10 @@ impl TryFrom<&hcl::Body> for ProjectMetadata { "Only one project block allowed.".to_string(), )); } else { - project = Some(Project::try_from(block)?) + project = Some(HclBlock(block).try_into()?) } } - "dataset" => datasets.push(Dataset::try_from(block)?), + "dataset" => datasets.push(HclBlock(block).try_into()?), _ => { continue; } diff --git a/dsp-meta/src/api/convert/hcl/value/alternative_name.rs b/dsp-meta/src/api/convert/hcl/value/alternative_name.rs index b8080a1a..be0f5558 100644 --- a/dsp-meta/src/api/convert/hcl/value/alternative_name.rs +++ b/dsp-meta/src/api/convert/hcl/value/alternative_name.rs @@ -1,7 +1,10 @@ use dsp_domain::metadata::value::alternative_name::AlternativeName; use dsp_domain::metadata::value::lang_text_data::LangTextData; +use sophia::ns::rdf::language; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; use crate::api::convert::hcl::hcl_block::HclBlock; +use crate::api::convert::hcl::value::lang_text_data; use crate::error::DspMetaError; const ALTERNATIVE_NAME_BLOCK_IDENTIFIER: &str = "alternative_name"; @@ -20,7 +23,11 @@ impl<'a> TryInto for HclBlock<'a> { } let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); - LangTextData::try_from(attributes).map(|lang_text_data| AlternativeName(lang_text_data.0)) + + // FIXME: improve API + let lang_text_data: Result = + HclAttributes(attributes).try_into(); + lang_text_data.map(|l| l.into()) } } diff --git a/dsp-meta/src/api/convert/hcl/value/description.rs b/dsp-meta/src/api/convert/hcl/value/description.rs index 5165fb35..230a1222 100644 --- a/dsp-meta/src/api/convert/hcl/value/description.rs +++ b/dsp-meta/src/api/convert/hcl/value/description.rs @@ -1,35 +1,45 @@ use dsp_domain::metadata::value::description::Description; use dsp_domain::metadata::value::lang_text_data::LangTextData; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const DESCRIPTION_BLOCK_IDENTIFIER: &str = "description"; -impl TryFrom<&hcl::Block> for Description { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != DESCRIPTION_BLOCK_IDENTIFIER { + fn try_into(self) -> Result { + if self.0.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() + self.0.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)) + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); + + // FIXME: improve API + let lang_text_data: Result = + HclAttributes(attributes).try_into(); + lang_text_data.map(|l| l.into()) } } #[cfg(test)] mod tests { use std::collections::HashMap; + use std::convert::Infallible; use dsp_domain::metadata::value::description::*; use dsp_domain::metadata::value::iso_code::IsoCode; + use crate::api::convert::hcl::hcl_block::HclBlock; + use crate::error::DspMetaError; + #[test] fn test_try_from_correct_block() { let block = hcl::block!( @@ -40,7 +50,7 @@ mod tests { } ); - let description = Description::try_from(&block).unwrap(); + let description: Description = HclBlock(&block).try_into().unwrap(); let mut map = HashMap::new(); map.insert(IsoCode::DE, String::from("Die Beschreibung")); @@ -61,7 +71,7 @@ mod tests { } ); - let description = Description::try_from(&block); + let description: Result = HclBlock(&block).try_into(); assert!(description.is_err()); } diff --git a/dsp-meta/src/api/convert/hcl/value/discipline.rs b/dsp-meta/src/api/convert/hcl/value/discipline.rs index ef989d48..ca00950b 100644 --- a/dsp-meta/src/api/convert/hcl/value/discipline.rs +++ b/dsp-meta/src/api/convert/hcl/value/discipline.rs @@ -2,43 +2,45 @@ use dsp_domain::metadata::value::discipline::Discipline; use dsp_domain::metadata::value::lang_text_data::LangTextData; use dsp_domain::metadata::value::ref_data::RefData; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; -impl TryFrom<&hcl::Block> for Discipline { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != "discipline" { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != "discipline" { let msg = format!( "The passed block is not named correctly. Expected 'discipline', however got '{}' instead.", - block.identifier.as_str() + self.0.identifier.as_str() ); return Err(DspMetaError::CreateValueObject(msg)); } - if block.labels.len() != 1 { + if self.0.labels.len() != 1 { return Err(DspMetaError::CreateValueObject("The passed number of block labels is not correct. Expected '1', namely 'reference data type' (e.g., 'skos').".to_string())); } - let reference_data_type = block.labels.first().ok_or_else(|| { + let reference_data_type = self.0.labels.first().ok_or_else(|| { DspMetaError::CreateValueObject( "The passed discipline block is missing the reference data type label.".to_string(), ) })?; - let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); match reference_data_type.as_str() { "skos" => { - let ref_data = RefData::try_from(attributes)?; + let ref_data: RefData = HclAttributes(attributes).try_into()?; Ok(Discipline::Skos(ref_data)) } "snf" => { - let ref_data = RefData::try_from(attributes)?; + let ref_data: RefData = HclAttributes(attributes).try_into()?; Ok(Discipline::Snf(ref_data)) } "text" => { - let text_data = LangTextData::try_from(attributes)?; + let text_data: LangTextData = HclAttributes(attributes).try_into()?; Ok(Discipline::Text(text_data)) } _ => { @@ -68,7 +70,7 @@ mod tests { } ); - let input = Discipline::try_from(&block).unwrap(); + let input: Discipline = HclBlock(&block).try_into().unwrap(); let expected = Discipline::Skos(RefData { ref_id: "https://skos.um.es/unesco6/5501".to_string(), description: "Local history".to_string(), @@ -89,7 +91,7 @@ mod tests { } ); - let input = Discipline::try_from(&block).unwrap(); + let input: Discipline = HclBlock(&block).try_into().unwrap(); let expected = Discipline::Text(LangTextData( vec![ (IsoCode::DE, "Lokalgeschichte".to_string()), diff --git a/dsp-meta/src/api/convert/hcl/value/keyword.rs b/dsp-meta/src/api/convert/hcl/value/keyword.rs index 100c3da9..ebd1f2b5 100644 --- a/dsp-meta/src/api/convert/hcl/value/keyword.rs +++ b/dsp-meta/src/api/convert/hcl/value/keyword.rs @@ -1,25 +1,30 @@ use dsp_domain::metadata::value::keyword::Keyword; use dsp_domain::metadata::value::lang_text_data::LangTextData; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const KEYWORD_BLOCK_IDENTIFIER: &str = "keyword"; -impl TryFrom<&hcl::Block> for Keyword { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != KEYWORD_BLOCK_IDENTIFIER { + fn try_into(self) -> Result { + if self.0.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() + self.0.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)) + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); + // FIXME: improve API + let lang_text_data: Result = + HclAttributes(attributes).try_into(); + lang_text_data.map(|l| l.into()) } } @@ -30,6 +35,9 @@ mod tests { use dsp_domain::metadata::value::iso_code::IsoCode; use dsp_domain::metadata::value::keyword::*; + use crate::api::convert::hcl::hcl_block::HclBlock; + use crate::error::DspMetaError; + #[test] fn test_try_from_correct_block() { let block = hcl::block!( @@ -40,7 +48,7 @@ mod tests { } ); - let keyword = Keyword::try_from(&block).unwrap(); + let keyword: Keyword = HclBlock(&block).try_into().unwrap(); let mut map: HashMap = HashMap::new(); map.insert(IsoCode::DE, String::from("Der keyword")); @@ -61,7 +69,7 @@ mod tests { } ); - let keyword = Keyword::try_from(&block); + let keyword: Result = HclBlock(&block).try_into(); assert!(keyword.is_err()); } diff --git a/dsp-meta/src/api/convert/hcl/value/lang_text_data.rs b/dsp-meta/src/api/convert/hcl/value/lang_text_data.rs index 77a5e1f8..5b05d9dd 100644 --- a/dsp-meta/src/api/convert/hcl/value/lang_text_data.rs +++ b/dsp-meta/src/api/convert/hcl/value/lang_text_data.rs @@ -3,15 +3,16 @@ use std::collections::HashMap; use dsp_domain::metadata::value::iso_code::IsoCode; use dsp_domain::metadata::value::lang_text_data::LangTextData; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; use crate::error::DspMetaError; -impl TryFrom> for LangTextData { +impl<'a> TryInto for HclAttributes<'a> { type Error = DspMetaError; - fn try_from(attributes: Vec<&hcl::Attribute>) -> Result { + fn try_into(self) -> Result { let mut text_data: HashMap = HashMap::new(); - for attribute in attributes { + for attribute in self.0 { let iso_code = IsoCode::try_from(attribute.key())?; let text = match attribute.expr() { hcl::Expression::String(value) => Ok(value.to_owned()), diff --git a/dsp-meta/src/api/convert/hcl/value/publication.rs b/dsp-meta/src/api/convert/hcl/value/publication.rs index e6f80d49..f25ff296 100644 --- a/dsp-meta/src/api/convert/hcl/value/publication.rs +++ b/dsp-meta/src/api/convert/hcl/value/publication.rs @@ -1,25 +1,28 @@ use dsp_domain::metadata::value::publication::Publication; use dsp_domain::metadata::value::simple_text_data::SimpleTextData; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const PUBLICATION_BLOCK_IDENTIFIER: &str = "publication"; -impl TryFrom<&hcl::Block> for Publication { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != PUBLICATION_BLOCK_IDENTIFIER { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != PUBLICATION_BLOCK_IDENTIFIER { let msg = format!( "The passed block is not named correctly. Expected 'publication', however got '{}' instead.", - block.identifier.as_str() + self.0.identifier.as_str() ); return Err(DspMetaError::CreateValueObject(msg)); } - let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); - - SimpleTextData::try_from(attributes).map(Publication::SimpleText) + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); + let simple_text_data: Result = + HclAttributes(attributes).try_into(); + simple_text_data.map(|s| s.into_simple_text()) } } @@ -28,6 +31,9 @@ mod tests { use dsp_domain::metadata::value::publication::*; use dsp_domain::metadata::value::simple_text_data::SimpleTextData; + use crate::api::convert::hcl::hcl_block::HclBlock; + use crate::error::DspMetaError; + #[test] fn test_try_from_correct_block() { let block = hcl::block!( @@ -36,7 +42,7 @@ mod tests { } ); - let publication = Publication::try_from(&block).unwrap(); + let publication: Publication = HclBlock(&block).try_into().unwrap(); match publication { Publication::SimpleText(data) => { @@ -53,7 +59,7 @@ mod tests { } ); - let publication = Publication::try_from(&block); + let publication: Result = HclBlock(&block).try_into(); assert!(publication.is_err()); } diff --git a/dsp-meta/src/api/convert/hcl/value/ref_data.rs b/dsp-meta/src/api/convert/hcl/value/ref_data.rs index 3fff2cb9..f32b32a6 100644 --- a/dsp-meta/src/api/convert/hcl/value/ref_data.rs +++ b/dsp-meta/src/api/convert/hcl/value/ref_data.rs @@ -1,19 +1,20 @@ use dsp_domain::metadata::value::ref_data::RefData; use tracing::warn; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; use crate::error::DspMetaError; /// Reference to a discipline defined in an external reference system (e.g. SNF or SKOS) /// FIXME: Move to the API layer where the service adapter is implemented -impl TryFrom> for RefData { +impl<'a> TryInto for HclAttributes<'a> { type Error = DspMetaError; - fn try_from(attributes: Vec<&hcl::Attribute>) -> Result { + fn try_into(self) -> Result { let mut ref_id: Option = None; let mut description: Option = None; let mut url: Option = None; - for attribute in attributes { + for attribute in self.0 { match attribute.key() { "ref_id" => { if ref_id.is_some() { diff --git a/dsp-meta/src/api/convert/hcl/value/simple_text_data.rs b/dsp-meta/src/api/convert/hcl/value/simple_text_data.rs index 89874db9..53c1c612 100644 --- a/dsp-meta/src/api/convert/hcl/value/simple_text_data.rs +++ b/dsp-meta/src/api/convert/hcl/value/simple_text_data.rs @@ -1,17 +1,18 @@ use dsp_domain::metadata::value::simple_text_data::SimpleTextData; use tracing::warn; +use crate::api::convert::hcl::hcl_attribute::HclAttributes; use crate::error::DspMetaError; const TEXT_ATTRIBUTE_IDENTIFIER: &str = "text"; -impl TryFrom> for SimpleTextData { +impl<'a> TryInto for HclAttributes<'a> { type Error = DspMetaError; - fn try_from(attributes: Vec<&hcl::Attribute>) -> Result { + fn try_into(self) -> Result { let mut text_attribute_value: Option = None; - for attribute in attributes { + for attribute in self.0 { match attribute.key() { TEXT_ATTRIBUTE_IDENTIFIER => { if text_attribute_value.is_some() { @@ -39,14 +40,18 @@ impl TryFrom> for SimpleTextData { #[cfg(test)] mod tests { + use std::convert::Infallible; use dsp_domain::metadata::value::simple_text_data::*; use tracing_test::traced_test; + use crate::api::convert::hcl::hcl_attribute::HclAttributes; + use crate::error::DspMetaError; + #[test] fn test_try_from_attributes() { let attribute = hcl::Attribute::new("text", "some text"); - let text_data = SimpleTextData::try_from(vec![&attribute]).unwrap(); + let text_data: SimpleTextData = HclAttributes(vec![&attribute]).try_into().unwrap(); assert_eq!(text_data, SimpleTextData("some text".to_string())); } @@ -54,7 +59,8 @@ mod tests { #[test] fn test_try_from_attributes_missing_text() { let attribute = hcl::Attribute::new("some_other_attribute", "some text"); - let text_data = SimpleTextData::try_from(vec![&attribute]); + let text_data: Result = + HclAttributes(vec![&attribute]).try_into(); assert!(text_data.is_err()); assert!(logs_contain( "Parse error: unknown attribute 'some_other_attribute'" @@ -65,14 +71,16 @@ mod tests { fn test_try_from_attributes_multiple_text() { let attribute = hcl::Attribute::new("text", "some text"); let attribute2 = hcl::Attribute::new("text", "some text"); - let text_data = SimpleTextData::try_from(vec![&attribute, &attribute2]); + let text_data: Result = + HclAttributes(vec![&attribute, &attribute2]).try_into(); assert!(text_data.is_err()); } #[test] fn test_try_from_attributes_wrong_type() { let attribute = hcl::Attribute::new("text", 1); - let text_data = SimpleTextData::try_from(vec![&attribute]); + let text_data: Result = + HclAttributes(vec![&attribute]).try_into(); assert!(text_data.is_err()); } } diff --git a/dsp-meta/src/api/convert/hcl/value/spatial_coverage.rs b/dsp-meta/src/api/convert/hcl/value/spatial_coverage.rs index b96be982..35c60e79 100644 --- a/dsp-meta/src/api/convert/hcl/value/spatial_coverage.rs +++ b/dsp-meta/src/api/convert/hcl/value/spatial_coverage.rs @@ -1,35 +1,36 @@ use dsp_domain::metadata::value::ref_data::RefData; use dsp_domain::metadata::value::spatial_coverage::SpacialCoverage; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const SPACIAL_COVERAGE: &str = "spacial_coverage"; const GEONAMES: &str = "geonames"; -impl TryFrom<&hcl::Block> for SpacialCoverage { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != SPACIAL_COVERAGE { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != SPACIAL_COVERAGE { let msg = format!( "The passed block is not named correctly. Expected 'spacial_coverage', however got '{}' instead.", - block.identifier.as_str() + self.0.identifier.as_str() ); return Err(DspMetaError::CreateValueObject(msg)); } - if block.labels.len() != 1 { + if self.0.labels.len() != 1 { return Err(DspMetaError::CreateValueObject("The passed number of block labels is not correct. Expected '1', namely 'reference data type' (e.g., 'geonames').".to_string())); } - let reference_data_type = block.labels.first().ok_or_else(|| { + let reference_data_type = self.0.labels.first().ok_or_else(|| { DspMetaError::CreateValueObject( "The passed spacial_coverage block is missing the reference data type label." .to_string(), ) })?; - let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); match reference_data_type.as_str() { GEONAMES => { diff --git a/dsp-meta/src/api/convert/hcl/value/temporal_coverage.rs b/dsp-meta/src/api/convert/hcl/value/temporal_coverage.rs index 63de4390..05a0d77d 100644 --- a/dsp-meta/src/api/convert/hcl/value/temporal_coverage.rs +++ b/dsp-meta/src/api/convert/hcl/value/temporal_coverage.rs @@ -2,6 +2,7 @@ use dsp_domain::metadata::value::lang_text_data::LangTextData; use dsp_domain::metadata::value::ref_data::RefData; use dsp_domain::metadata::value::temporal_coverage::TemporalCoverage; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const TEMPORAL_COVERAGE: &str = "temporal_coverage"; @@ -10,30 +11,30 @@ const PERIODO: &str = "periodo"; const TEXT: &str = "text"; -impl TryFrom<&hcl::Block> for TemporalCoverage { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != TEMPORAL_COVERAGE { + fn try_into(self) -> Result { + if self.0.identifier.as_str() != TEMPORAL_COVERAGE { let msg = format!( "The passed block is not named correctly. Expected 'temporal_coverage', however got '{}' instead.", - block.identifier.as_str() + self.0.identifier.as_str() ); return Err(DspMetaError::CreateValueObject(msg)); } - if block.labels.len() != 1 { + if self.0.labels.len() != 1 { return Err(DspMetaError::CreateValueObject("The passed number of block labels is not correct. Expected '1', namely 'reference data type' (e.g., 'chronontology, periodo').".to_string())); } - let reference_data_type = block.labels.first().ok_or_else(|| { + let reference_data_type = self.0.labels.first().ok_or_else(|| { DspMetaError::CreateValueObject( "The passed spacial_coverage block is missing the reference data type label." .to_string(), ) })?; - let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); match reference_data_type.as_str() { CHRONONTOLOGY => { diff --git a/dsp-meta/src/api/convert/hcl/value/url.rs b/dsp-meta/src/api/convert/hcl/value/url.rs index e9af993c..e90210b6 100644 --- a/dsp-meta/src/api/convert/hcl/value/url.rs +++ b/dsp-meta/src/api/convert/hcl/value/url.rs @@ -1,21 +1,22 @@ use dsp_domain::metadata::value::url::Url; use tracing::warn; +use crate::api::convert::hcl::hcl_block::HclBlock; use crate::error::DspMetaError; const URL_BLOCK_IDENTIFIER: &str = "url"; const HREF_ATTRIBUTE_KEY: &str = "href"; const LABEL_ATTRIBUTE_KEY: &str = "label"; -impl TryFrom<&hcl::Block> for Url { +impl<'a> TryInto for HclBlock<'a> { type Error = DspMetaError; - fn try_from(block: &hcl::Block) -> Result { - if block.identifier.as_str() != URL_BLOCK_IDENTIFIER { + fn try_into(self) -> Result { + if self.0.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() + self.0.identifier.as_str() ); return Err(DspMetaError::CreateValueObject(msg)); } @@ -23,7 +24,7 @@ impl TryFrom<&hcl::Block> for Url { let mut href: Option = None; let mut label: Option = None; - let attributes: Vec<&hcl::Attribute> = block.body.attributes().collect(); + let attributes: Vec<&hcl::Attribute> = self.0.body.attributes().collect(); for attribute in attributes { match attribute.key() { LABEL_ATTRIBUTE_KEY => { diff --git a/dsp-meta/src/api/convert/hcl/value/version.rs b/dsp-meta/src/api/convert/hcl/value/version.rs index 86e6c035..a6316e1a 100644 --- a/dsp-meta/src/api/convert/hcl/value/version.rs +++ b/dsp-meta/src/api/convert/hcl/value/version.rs @@ -1,19 +1,23 @@ use dsp_domain::metadata::value::version::Version; use hcl::Expression; +use crate::api::convert::hcl::hcl_attribute::HclAttribute; +use crate::error::DspMetaError; + /// Given a list of attributes, try to extract the version. -impl TryFrom<&hcl::Attribute> for Version { - type Error = crate::error::DspMetaError; - fn try_from(attribute: &hcl::Attribute) -> Result { +impl<'a> TryInto for HclAttribute<'a> { + type Error = DspMetaError; + + fn try_into(self) -> Result { type Error = crate::error::DspMetaError; - let mut result: Result = Err(Error::ParseVersion( + let mut result: Result = Err(Error::ParseVersion( "Version attribute is not provided.".to_string(), )); - if attribute.key() == "version" { - result = match attribute.expr() { - Expression::Number(value) => Ok(Self(value.as_u64().ok_or_else(|| { + if self.0.key() == "version" { + result = match self.0.expr() { + Expression::Number(value) => Ok(Version(value.as_u64().ok_or_else(|| { Error::ParseVersion("Version needs to be a non-negative number.".to_string()) })?)), _ => Err(Error::ParseVersion( @@ -30,10 +34,12 @@ impl TryFrom<&hcl::Attribute> for Version { mod tests { use dsp_domain::metadata::value::version::*; + use crate::api::convert::hcl::hcl_attribute::HclAttribute; + #[test] fn test_try_from_attributes() { let attribute = hcl::Attribute::new("version", 1u64); - let version = Version::try_from(&attribute).unwrap(); + let version: Version = HclAttribute(&attribute).try_into().unwrap(); assert_eq!(version, Version(1)); } } diff --git a/dsp-meta/src/api/handler/project_metadata_handler.rs b/dsp-meta/src/api/handler/project_metadata_handler.rs index 8ab78a32..0dd25e1f 100644 --- a/dsp-meta/src/api/handler/project_metadata_handler.rs +++ b/dsp-meta/src/api/handler/project_metadata_handler.rs @@ -8,7 +8,8 @@ use dsp_domain::metadata::value::Shortcode; use serde_json::Value; use tracing::trace; -use crate::api::convert::axum::project_metadata::OptionalProjectMetadata; +use crate::api::convert::axum::project_metadata::ProjectMetadataDto; +use crate::api::convert::hcl::hcl_body::HclBody; use crate::api::convert::rdf::project_metadata::ProjectMetadataGraph; use crate::app_state::AppState; use crate::domain::service::project_metadata_api_contract::ProjectMetadataApiContract; @@ -21,13 +22,13 @@ use crate::error::DspMetaError; pub async fn get_project_metadata_by_shortcode( Path(shortcode): Path, State(state): State>, -) -> Result { +) -> Result { trace!("entered get_project_metadata_by_shortcode()"); let _maybe_graph: ProjectMetadataGraph = ProjectMetadata::default().try_into()?; state .project_metadata_service .find_by_id(Shortcode(shortcode)) - .map(OptionalProjectMetadata) + .map(ProjectMetadataDto) } pub async fn get_all_project_metadata(State(state): State>) -> Json { @@ -40,13 +41,16 @@ pub async fn get_all_project_metadata(State(state): State>) -> Jso pub async fn save_project_metadata( State(state): State>, body: String, -) -> Result { +) -> Result { trace!("entered save_project_metadata"); let service = &state.project_metadata_service; let hcl_body = hcl::from_str(body.as_str())?; - let project_metadata = ProjectMetadata::try_from(&hcl_body)?; + let project_metadata: ProjectMetadata = HclBody(&hcl_body).try_into()?; - service.save(project_metadata) + service + .save(project_metadata) + .map(Some) + .map(ProjectMetadataDto) } diff --git a/dsp-meta/src/error.rs b/dsp-meta/src/error.rs index 9cda7192..016671d9 100644 --- a/dsp-meta/src/error.rs +++ b/dsp-meta/src/error.rs @@ -1,9 +1,10 @@ use std::io; +use dsp_domain::error::DspDomainError; use serde::Serialize; use thiserror::Error; -/// Type alias for `Result` with default error `InvalidIri`. +/// Type alias for `Result` with default error `DspMetaError`. /// /// Can be used like `std::result::Result` as well. pub type Result = std::result::Result; @@ -36,3 +37,11 @@ impl From for DspMetaError { DspMetaError::IO(error.to_string()) } } + +impl From for DspMetaError { + fn from(value: DspDomainError) -> Self { + match value { + DspDomainError::CreateValueObject(err) => DspMetaError::CreateValueObject(err), + } + } +}