diff --git a/nemo/src/io/format_builder.rs b/nemo/src/io/format_builder.rs index 3df710229..55ad42e4f 100644 --- a/nemo/src/io/format_builder.rs +++ b/nemo/src/io/format_builder.rs @@ -27,6 +27,7 @@ use crate::{ }, error::{ValidationReport, hint::Hint, info::Info, validation_error::ValidationError}, substitution::Substitution, + translation::directive::FormatContext, }, syntax::import_export::attribute, }; @@ -47,7 +48,11 @@ pub(crate) trait FormatParameter: FromStr + Debug + ToString + IntoEnumIterator + Copy + Eq + Hash { fn required_for(&self, tag: Tag) -> bool; - fn is_value_valid(&self, value: AnyDataValue) -> Result<(), ValidationError>; + fn is_value_valid( + &self, + value: AnyDataValue, + format_context: FormatContext, + ) -> Result<(), ValidationError>; } pub(super) fn value_type_matches( @@ -234,7 +239,11 @@ impl FormatParameter for StandardParameter { false } - fn is_value_valid(&self, value: AnyDataValue) -> Result<(), ValidationError> { + fn is_value_valid( + &self, + value: AnyDataValue, + _format_context: FormatContext, + ) -> Result<(), ValidationError> { value_type_matches(self, &value, self.supported_types())?; match self { @@ -269,6 +278,7 @@ pub(crate) trait FormatBuilder: Debug + Sized + Into { tag: Self::Tag, parameters: &Parameters, direction: Direction, + format_context: FormatContext, ) -> Result; fn expected_arity(&self) -> Option; @@ -357,6 +367,7 @@ impl Parameters { _filter_rules: &[Rule], direction: Direction, report: &mut ValidationReport, + formate_context: FormatContext, ) -> Option { let Ok(format_tag) = B::Tag::from_str(spec.format().name()) else { report.add( @@ -420,7 +431,7 @@ impl Parameters { } }; - if let Err(error) = parameter.is_value_valid(value.clone()) { + if let Err(error) = parameter.is_value_valid(value.clone(), formate_context.clone()) { report.add(value_term, error); has_errors = true; continue; @@ -505,6 +516,7 @@ impl ImportExportBuilder { self.resource.clone() } + #[allow(clippy::too_many_arguments)] fn new_with_tag( predicate: Tag, tag: B::Tag, @@ -513,10 +525,18 @@ impl ImportExportBuilder { filter_rules: &[Rule], direction: Direction, report: &mut ValidationReport, + format_context: FormatContext, ) -> Option { let origin = spec.origin(); - let parameters = - Parameters::::validate(predicate, spec, bindings, filter_rules, direction, report)?; + let parameters = Parameters::::validate( + predicate, + spec, + bindings, + filter_rules, + direction, + report, + format_context.clone(), + )?; let resource_builder = if let Some(value) = parameters.get_optional(StandardParameter::Resource.into()) { @@ -538,7 +558,7 @@ impl ImportExportBuilder { }) .unwrap_or_else(|| CompressionFormat::from_resource_builder(&resource_builder)); - let inner = match B::new(tag, ¶meters, direction) { + let inner = match B::new(tag, ¶meters, direction, format_context.clone()) { Ok(b) => b, Err(error) => { report.add_source(origin.clone(), error); @@ -629,6 +649,7 @@ impl ImportExportBuilder { filter_rules: &[Rule], direction: Direction, report: &mut ValidationReport, + format_context: FormatContext, ) -> Option { let format_tag = spec.format().name().to_owned(); let Ok(tag) = format_tag.parse::() else { @@ -651,6 +672,7 @@ impl ImportExportBuilder { filter_rules, direction, report, + format_context, ), SupportedFormatTag::Rdf(tag) => Self::new_with_tag::( predicate, @@ -660,6 +682,7 @@ impl ImportExportBuilder { filter_rules, direction, report, + format_context, ), SupportedFormatTag::Json(tag) => Self::new_with_tag::( predicate, @@ -669,6 +692,7 @@ impl ImportExportBuilder { filter_rules, direction, report, + format_context, ), SupportedFormatTag::Sparql(tag) => Self::new_with_tag::( predicate, @@ -678,6 +702,7 @@ impl ImportExportBuilder { filter_rules, direction, report, + format_context, ), } } diff --git a/nemo/src/io/formats/dsv.rs b/nemo/src/io/formats/dsv.rs index 7338fd2f2..b0e43ef4a 100644 --- a/nemo/src/io/formats/dsv.rs +++ b/nemo/src/io/formats/dsv.rs @@ -28,6 +28,7 @@ use crate::{ rule_model::{ components::{import_export::Direction, term::value_type::ValueType}, error::validation_error::ValidationError, + translation::directive::FormatContext, }, syntax::import_export::{attribute, file_format}, }; @@ -178,12 +179,16 @@ impl FormatParameter for DsvParameter { } } - fn is_value_valid(&self, value: AnyDataValue) -> Result<(), ValidationError> { + fn is_value_valid( + &self, + value: AnyDataValue, + format_context: FormatContext, + ) -> Result<(), ValidationError> { value_type_matches(self, &value, self.supported_types())?; match self { - DsvParameter::BaseParamType(base) => { - FormatParameter::::is_value_valid(base, value) + DsvParameter::BaseParamType(base_param) => { + FormatParameter::::is_value_valid(base_param, value, format_context) } DsvParameter::Limit => value .to_u64() @@ -238,6 +243,7 @@ impl FormatBuilder for DsvBuilder { tag: Self::Tag, parameters: &Parameters, _direction: Direction, + _format_context: FormatContext, ) -> Result { let value_formats = parameters.get_optional(DsvParameter::Format).map(|value| { DsvValueFormats::try_from(value).expect("value formats have already been validated") diff --git a/nemo/src/io/formats/json.rs b/nemo/src/io/formats/json.rs index 9ba653a40..827d2428c 100644 --- a/nemo/src/io/formats/json.rs +++ b/nemo/src/io/formats/json.rs @@ -14,7 +14,10 @@ use crate::{ AnyImportExportBuilder, FormatBuilder, Parameters, StandardParameter, SupportedFormatTag, format_tag, }, - rule_model::{components::import_export::Direction, error::validation_error::ValidationError}, + rule_model::{ + components::import_export::Direction, error::validation_error::ValidationError, + translation::directive::FormatContext, + }, syntax::import_export::file_format, }; @@ -71,6 +74,7 @@ impl FormatBuilder for JsonHandler { _tag: Self::Tag, _parameters: &Parameters, direction: Direction, + _format_context: FormatContext, ) -> Result { if matches!(direction, Direction::Export) { return Err(ValidationError::UnsupportedJsonExport); diff --git a/nemo/src/io/formats/rdf.rs b/nemo/src/io/formats/rdf.rs index 04d0acf24..97c5bd5a7 100644 --- a/nemo/src/io/formats/rdf.rs +++ b/nemo/src/io/formats/rdf.rs @@ -38,6 +38,7 @@ use crate::{ rule_model::{ components::{import_export::Direction, term::value_type::ValueType}, error::validation_error::ValidationError, + translation::directive::FormatContext, }, syntax::import_export::{attribute, file_format}, }; @@ -187,12 +188,16 @@ impl FormatParameter for RdfParameter { ) } - fn is_value_valid(&self, value: AnyDataValue) -> Result<(), ValidationError> { + fn is_value_valid( + &self, + value: AnyDataValue, + format_context: FormatContext, + ) -> Result<(), ValidationError> { value_type_matches(self, &value, self.supported_types())?; match self { - RdfParameter::BaseParamType(base) => { - FormatParameter::::is_value_valid(base, value) + RdfParameter::BaseParamType(base_param) => { + FormatParameter::::is_value_valid(base_param, value, format_context) } RdfParameter::Limit => value .to_u64() @@ -222,6 +227,7 @@ impl FormatBuilder for RdfHandler { tag: Self::Tag, parameters: &Parameters, _direction: Direction, + _format_context: FormatContext, ) -> Result { let variant = match tag { RdfTag::Rdf => { diff --git a/nemo/src/io/formats/sparql.rs b/nemo/src/io/formats/sparql.rs index 4f85a53a5..e6e939041 100644 --- a/nemo/src/io/formats/sparql.rs +++ b/nemo/src/io/formats/sparql.rs @@ -24,6 +24,7 @@ use crate::{ rule_model::{ components::{import_export::Direction, term::value_type::ValueType}, error::validation_error::ValidationError, + translation::directive::FormatContext, }, syntax::import_export::{ attribute, @@ -67,12 +68,16 @@ impl FormatParameter for SparqlParameter { matches!(tag, SparqlTag::Sparql) && matches!(self, Self::Endpoint) } - fn is_value_valid(&self, value: AnyDataValue) -> Result<(), ValidationError> { + fn is_value_valid( + &self, + value: AnyDataValue, + format_context: FormatContext, + ) -> Result<(), ValidationError> { value_type_matches(self, &value, self.supported_types())?; match self { - SparqlParameter::BaseParamType(base) => { - FormatParameter::::is_value_valid(base, value) + SparqlParameter::BaseParamType(base_param) => { + FormatParameter::::is_value_valid(base_param, value, format_context) } SparqlParameter::Base => Ok(()), SparqlParameter::Format => DsvValueFormats::try_from(value).and(Ok(())).map_err(|_| { @@ -93,13 +98,33 @@ impl FormatParameter for SparqlParameter { } SparqlParameter::Query => { let query = value.to_plain_string_unchecked(); - SparqlParser::new() // TODO(mam): inject prefixes and base here, cf. #647 - .parse_query(query.as_str()) - .and(Ok(())) - .map_err(|e| ValidationError::InvalidSparqlQuery { + + let mut parser = SparqlParser::new(); + + if let Some(base) = format_context.base() { + parser = parser.with_base_iri(base.to_string()).map_err(|e| { + ValidationError::InvalidSparqlQuery { + query: query.clone(), + oxi_error: e.to_string(), + } + })? + } + + for (prefix_name, prefix_iri) in format_context.prefixes() { + parser = parser + .with_prefix(prefix_name, prefix_iri.to_string()) + .map_err(|e| ValidationError::InvalidSparqlQuery { + query: query.clone(), + oxi_error: e.to_string(), + })? + } + + parser.parse_query(query.as_str()).and(Ok(())).map_err(|e| { + ValidationError::InvalidSparqlQuery { query, oxi_error: e.to_string(), - }) + } + }) } } } @@ -144,6 +169,7 @@ impl FormatBuilder for SparqlBuilder { _tag: Self::Tag, parameters: &Parameters, _direction: Direction, + format_context: FormatContext, ) -> Result { let value_formats = parameters .get_optional(SparqlParameter::Format) @@ -164,7 +190,9 @@ impl FormatBuilder for SparqlBuilder { .map(|value| value.to_plain_string_unchecked()) .unwrap_or(QUERY_DEFAULT.to_string()); - let query = SparqlParser::new() // TODO(mam): inject prefixes and base here, cf. #647 + let parser = format_context.into_sparql_parser(); + + let query = parser .parse_query(query.as_str()) .expect("query has already been validated"); @@ -268,10 +296,14 @@ impl ImportHandler for SparqlHandler { #[cfg(test)] mod test { - use crate::parser::{ - ParserState, - ast::{ProgramAST, directive::import::Import}, - input::ParserInput, + + use crate::{ + parser::{ + ParserState, + ast::{ProgramAST, directive::import::Import}, + input::ParserInput, + }, + rule_model::translation::directive::FormatContext, }; use nom::combinator::all_consuming; @@ -282,10 +314,19 @@ mod test { #[test] fn parse_query() { + let mut format_context = FormatContext::default(); + format_context.add_base("http://www.wikidata.org/".to_string()); + // PREFIX wikibase: + format_context.add_prefix( + "wikibase".to_string(), + "http://wikiba.se/ontology#".to_string(), + ); + // PREFIX wdt: + format_context.add_prefix("wdt".to_string(), "prop/direct/".to_string()); + let valid_query = AnyDataValue::new_plain_string(String::from(" - PREFIX wikibase: + PREFIX wikibase: PREFIX bd: - PREFIX wdt: PREFIX wd: PREFIX ps: PREFIX p: @@ -300,9 +341,12 @@ mod test { ); let query_param = SparqlParameter::Query; - let result = query_param.is_value_valid(valid_query); + let result = query_param.is_value_valid(valid_query, format_context); assert!(result.is_ok()); + } + #[test] + fn parse_invalid_query() { // Invalid because no prefixes are specified let invalid_query = AnyDataValue::new_plain_string(String::from(" SELECT ?item ?itemLabel @@ -313,7 +357,8 @@ mod test { } LIMIT 10") ); - let result = query_param.is_value_valid(invalid_query); + let query_param = SparqlParameter::Query; + let result = query_param.is_value_valid(invalid_query, FormatContext::default()); assert!(result.is_err()); } diff --git a/nemo/src/rule_model/components/import_export.rs b/nemo/src/rule_model/components/import_export.rs index 0ba301cde..500afe20f 100644 --- a/nemo/src/rule_model/components/import_export.rs +++ b/nemo/src/rule_model/components/import_export.rs @@ -16,7 +16,7 @@ use crate::{ }, rule_model::{ components::rule::Rule, error::ValidationReport, origin::Origin, - pipeline::id::ProgramComponentId, + pipeline::id::ProgramComponentId, translation::directive::FormatContext, }, syntax::{self, import_export::attribute::QUERY}, }; @@ -78,6 +78,8 @@ pub(crate) struct ImportExportDirective { bindings: Vec, /// Sub-rules for filtered import/export filter_rules: Vec, + /// Base IRI (if any is set) and Hashmap of any Prefixes + format_context: FormatContext, } impl std::fmt::Display for ImportExportDirective { @@ -167,7 +169,12 @@ pub struct ImportDirective(pub(crate) ImportExportDirective); impl ImportDirective { /// Create a new [ImportDirective]. - pub fn new(predicate: Tag, spec: ImportExportSpec, bindings: Vec) -> Self { + pub fn new( + predicate: Tag, + spec: ImportExportSpec, + bindings: Vec, + format_context: FormatContext, + ) -> Self { Self(ImportExportDirective { origin: Origin::default(), id: ProgramComponentId::default(), @@ -175,6 +182,7 @@ impl ImportDirective { spec, bindings, filter_rules: Vec::new(), + format_context, }) } @@ -374,6 +382,7 @@ impl ImportDirective { self.filter_rules(), Direction::Import, report, + self.format_context().clone(), ) } @@ -387,9 +396,15 @@ impl ImportDirective { self.filter_rules(), Direction::Import, &mut report, + self.format_context().clone(), ) } + /// Return the FormatContext + pub fn format_context(&self) -> &FormatContext { + &self.0.format_context + } + /// Return the predicate. pub fn predicate(&self) -> &Tag { &self.0.predicate @@ -565,6 +580,7 @@ impl ExportDirective { spec, bindings, filter_rules: Vec::new(), + format_context: FormatContext::default(), }) } @@ -596,6 +612,7 @@ impl ExportDirective { self.filter_rules(), Direction::Export, report, + self.format_context().clone(), ) } @@ -609,9 +626,15 @@ impl ExportDirective { self.filter_rules(), Direction::Export, &mut report, + self.format_context().clone(), ) } + /// Return the FormatContext + pub fn format_context(&self) -> &FormatContext { + &self.0.format_context + } + /// Return the predicate. pub fn predicate(&self) -> &Tag { &self.0.predicate diff --git a/nemo/src/rule_model/error/translation_error.rs b/nemo/src/rule_model/error/translation_error.rs index f631dba83..4606e1e23 100644 --- a/nemo/src/rule_model/error/translation_error.rs +++ b/nemo/src/rule_model/error/translation_error.rs @@ -126,6 +126,16 @@ pub enum TranslationError { #[assoc(note = "parameter names must have the form `$name'")] #[assoc(code = 130)] ParamDeclarationNotGlobal, + /// Base is not a valid IRI + #[error(r#"base is not a valid IRI"#)] + #[assoc(note = "base must be a valid absolute IRI (RFC 3987)")] + #[assoc(code = 131)] + BaseInvalid, + /// Prefix is not a valid IRI + #[error(r#"prefix is not a valid IRI"#)] + #[assoc(note = "prefix must be a valid IRI either absolute or relative to base (RFC 3987)")] + #[assoc(code = 132)] + PrefixInvalid, /// Unsupported: Declare statements #[error(r#"declare statements are currently unsupported"#)] diff --git a/nemo/src/rule_model/translation/directive.rs b/nemo/src/rule_model/translation/directive.rs index d4a80a702..cca94eec3 100644 --- a/nemo/src/rule_model/translation/directive.rs +++ b/nemo/src/rule_model/translation/directive.rs @@ -1,6 +1,9 @@ //! This module contains functions for translating directive ast nodes. +use core::panic; +use std::collections::{HashMap, hash_map::Entry}; -use std::collections::hash_map::Entry; +use oxiri::{Iri, IriRef}; +use spargebra::SparqlParser; use crate::{ parser::ast::{self}, @@ -96,6 +99,12 @@ fn handle_base<'a>(translation: &mut ASTProgramTranslation, base: &ast::directiv .add_context_source(first_base.clone(), Info::FirstDefinition); } + // check if base is a valid iri + if Iri::parse(base.iri().content()).is_err() { + translation.report.add(base, TranslationError::BaseInvalid); + // TODO add parse error as context + } + translation.base = Some((base.iri().content(), base.origin())); } @@ -127,6 +136,26 @@ fn handle_prefix<'a>( translation: &mut ASTProgramTranslation, prefix: &ast::directive::prefix::Prefix<'a>, ) { + let parse_prefix = || { + let rel_prefix = IriRef::parse(prefix.iri().content())?; + if rel_prefix.is_absolute() { + return Ok::<(), Box>(()); + } else if let Some(base) = &translation.base { + Iri::parse(base.0.to_string()) + .expect("base is not a valid IRI") + .resolve(&rel_prefix)?; + return Ok::<(), Box>(()); + } + Err(Box::new(TranslationError::PrefixInvalid)) + }; + + if parse_prefix().is_err() { + // parsing not successful + translation + .report + .add(prefix.prefix_token(), TranslationError::PrefixInvalid); + } + match translation.prefix_mapping.entry(prefix.prefix()) { Entry::Occupied(entry) => { let (_, prefix_first) = entry.get(); @@ -141,3 +170,73 @@ fn handle_prefix<'a>( } } } + +/// Wrapper for IRI base and prefixes +/// Automatically attempts to resolve all relative prefixes on the base +#[derive(Clone, Debug, Default)] +pub struct FormatContext { + base: Option>, + prefixes: HashMap>, +} + +impl FormatContext { + /// Returns the base IRI if set + pub fn base(&self) -> &Option> { + &self.base + } + + /// returns a Hashmap of all prefixes and their related IRIs + pub fn prefixes(&self) -> &HashMap> { + &self.prefixes + } + + /// adds a base if None was set before + /// panics if a base is already set + pub fn add_base(&mut self, base: String) { + match self.base { + None => self.base = Some(Iri::parse(base).expect("not a valid IRI")), + Some(_) => panic!("attempted to set existing base"), + } + } + + /// adds an absolute prefix + /// if the prefix given is relative attempts to resolve it on the base + /// panics if + /// - the iri is invalid + /// - the prefix cannot be resolved + /// - a relative prefix is given but no base is set + pub fn add_prefix(&mut self, prefix: String, iri: String) { + let rel_prefix = IriRef::parse(iri.clone()).expect("not a valid Iri"); + let abs_prefix = if rel_prefix.is_absolute() { + Iri::parse(iri.clone()).expect("IRI is absolute") + } else { + if let Some(base) = &self.base { + base.resolve(&rel_prefix.clone()) + .expect("relative prefix can be resolved on base") + } else { + unreachable!() + } + }; + + self.prefixes.insert(prefix, abs_prefix); + } + + /// prepares and returns a SparqlParser with the base and prefixes + pub fn into_sparql_parser(self) -> SparqlParser { + let mut parser = SparqlParser::new(); + + if let Some(base) = &self.base { + parser = parser + .with_base_iri(base.to_string()) + .expect("valid base is invalid"); + } + + for (prefix_name, prefix_iri) in &self.prefixes { + parser = parser + .with_prefix(prefix_name, prefix_iri.to_string()) + .expect("valid prefix is invalid"); + } + + parser + } +} diff --git a/nemo/src/rule_model/translation/directive/import_export.rs b/nemo/src/rule_model/translation/directive/import_export.rs index 129ffa902..85123b5d8 100644 --- a/nemo/src/rule_model/translation/directive/import_export.rs +++ b/nemo/src/rule_model/translation/directive/import_export.rs @@ -18,6 +18,7 @@ use crate::{ origin::Origin, translation::{ ASTProgramTranslation, TranslationComponent, complex::infix::InfixOperation, + directive::FormatContext, }, }, }; @@ -119,8 +120,18 @@ impl TranslationComponent for ImportDirective { let bindings = import_export_bindings(translation, import.guards())?; + let mut format_context = FormatContext::default(); + + if let Some(base) = &translation.base { + format_context.add_base(base.0.clone()); + } + + for (prefix, iri) in &translation.prefix_mapping { + format_context.add_prefix(prefix.clone(), iri.0.clone()); + } + Some(Origin::ast( - ImportDirective::new(predicate, spec, bindings), + ImportDirective::new(predicate, spec, bindings, format_context), import, )) }