use crate::ast;
use crate::ir::{schema_ir::SchemaIR, shape_label::ShapeLabel};
use crate::node::Node;
use crate::{ShapeExprLabel, ShapeLabelIdx};
use rudof_iri::IriS;
use std::path::Path;
use std::sync::Arc;
use thiserror::Error;
pub struct ExternalResolveCtx<'a> {
pub node: &'a Node,
pub shape_idx: ShapeLabelIdx,
pub shape_label: Option<&'a ShapeLabel>,
pub schema: &'a SchemaIR,
}
#[derive(Debug, Clone)]
pub enum ExternalResolution {
Conformant { rationale: String },
NonConformant { rationale: String },
Abstain,
}
#[derive(Debug, Clone)]
pub enum DispatchOutcome {
Conformant { resolver: String, rationale: String },
NonConformant { resolver: String, rationale: String },
Abstain,
}
pub trait ExternalShapeResolver: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn rewrite_ast(&self, schema: ast::Schema) -> ast::Schema {
schema
}
fn resolve(&self, _ctx: &ExternalResolveCtx<'_>) -> ExternalResolution {
ExternalResolution::Abstain
}
}
#[derive(Debug, Clone)]
pub struct ExternalShapeResolverRegistry {
resolvers: Vec<Arc<dyn ExternalShapeResolver>>,
}
impl Default for ExternalShapeResolverRegistry {
fn default() -> Self {
Self {
resolvers: vec![Arc::new(RejectAllExternalResolver)],
}
}
}
impl ExternalShapeResolverRegistry {
pub fn empty() -> Self {
Self { resolvers: vec![] }
}
pub fn with_resolver<R: ExternalShapeResolver + 'static>(mut self, r: R) -> Self {
self.resolvers.insert(0, Arc::new(r));
self
}
pub fn with_resolver_arc(mut self, r: Arc<dyn ExternalShapeResolver>) -> Self {
self.resolvers.insert(0, r);
self
}
pub fn resolvers(&self) -> &[Arc<dyn ExternalShapeResolver>] {
&self.resolvers
}
pub fn rewrite_ast(&self, mut schema: ast::Schema) -> ast::Schema {
for r in &self.resolvers {
schema = r.rewrite_ast(schema);
}
schema
}
pub fn dispatch(&self, ctx: &ExternalResolveCtx<'_>) -> DispatchOutcome {
for r in &self.resolvers {
match r.resolve(ctx) {
ExternalResolution::Abstain => continue,
ExternalResolution::Conformant { rationale } => {
return DispatchOutcome::Conformant {
resolver: r.name().to_string(),
rationale,
};
},
ExternalResolution::NonConformant { rationale } => {
return DispatchOutcome::NonConformant {
resolver: r.name().to_string(),
rationale,
};
},
}
}
DispatchOutcome::Abstain
}
}
#[derive(Debug, Clone, Default)]
pub struct RejectAllExternalResolver;
impl ExternalShapeResolver for RejectAllExternalResolver {
fn name(&self) -> &str {
"reject-all"
}
fn resolve(&self, _ctx: &ExternalResolveCtx<'_>) -> ExternalResolution {
ExternalResolution::NonConformant {
rationale: "EXTERNAL shape rejected: no resolver supplied a definition".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct SchemaExternalResolver {
name: String,
externs: ast::Schema,
}
impl SchemaExternalResolver {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ExternalResolverError> {
let path = path.as_ref();
let source_iri: IriS =
path.try_into()
.map_err(|e: rudof_iri::error::IriSError| ExternalResolverError::PathToIri {
path: path.to_path_buf(),
error: e.to_string(),
})?;
let externs =
crate::ShExParser::parse_buf(path, Some(source_iri)).map_err(|e| ExternalResolverError::Parse {
path: path.to_path_buf(),
error: e.to_string(),
})?;
Ok(Self {
name: format!("schema:{}", path.display()),
externs,
})
}
pub fn from_schema(name: impl Into<String>, externs: ast::Schema) -> Self {
Self {
name: name.into(),
externs,
}
}
pub fn externs(&self) -> &ast::Schema {
&self.externs
}
}
impl ExternalShapeResolver for SchemaExternalResolver {
fn name(&self) -> &str {
&self.name
}
fn rewrite_ast(&self, mut schema: ast::Schema) -> ast::Schema {
let lookups = match self.externs.shapes() {
Some(s) => s,
None => return schema,
};
if let Some(decls) = schema.shapes_mut() {
for decl in decls.iter_mut() {
if matches!(decl.shape_expr, ast::ShapeExpr::External)
&& let Some(matching) = lookup_decl(&lookups, &decl.id)
{
decl.shape_expr = matching.shape_expr.clone();
}
}
}
schema
}
}
fn lookup_decl<'a>(decls: &'a [ast::ShapeDecl], label: &ShapeExprLabel) -> Option<&'a ast::ShapeDecl> {
decls.iter().find(|d| &d.id == label)
}
#[derive(Debug, Error)]
pub enum ExternalResolverError {
#[error("Could not convert path {path:?} into IRI: {error}")]
PathToIri { path: std::path::PathBuf, error: String },
#[error("Could not parse external shapes file {path:?}: {error}")]
Parse { path: std::path::PathBuf, error: String },
#[error("Unknown external resolver kind '{kind}'. Available kinds: {}", available.join(", "))]
UnknownKind { kind: String, available: Vec<String> },
#[error("External resolver kind '{kind}' requires an argument: {expected}")]
MissingArg { kind: String, expected: String },
#[error("External resolver kind '{kind}' does not accept any argument")]
ForbiddenArg { kind: String },
}
#[derive(Debug, Clone)]
pub struct ExternalResolverInfo {
pub name: &'static str,
pub description: &'static str,
pub spec_syntax: &'static str,
}
pub fn available_external_resolvers() -> Vec<ExternalResolverInfo> {
vec![
ExternalResolverInfo {
name: "reject-all",
description: "Reject any EXTERNAL shape that no earlier resolver claimed",
spec_syntax: "reject-all",
},
ExternalResolverInfo {
name: "schema",
description: "Substitute EXTERNAL shape declarations using definitions from a ShEx file",
spec_syntax: "schema:<path>",
},
]
}
pub fn resolver_from_spec(spec: &str) -> Result<Arc<dyn ExternalShapeResolver>, ExternalResolverError> {
let (kind, arg) = match spec.split_once(':') {
Some((k, a)) => (k.trim(), Some(a.trim())),
None => (spec.trim(), None),
};
match (kind, arg) {
("reject-all", None) => Ok(Arc::new(RejectAllExternalResolver)),
("reject-all", Some(_)) => Err(ExternalResolverError::ForbiddenArg {
kind: "reject-all".to_string(),
}),
("schema", Some(path)) if !path.is_empty() => Ok(Arc::new(SchemaExternalResolver::from_path(path)?)),
("schema", _) => Err(ExternalResolverError::MissingArg {
kind: "schema".to_string(),
expected: "path to a ShEx file".to_string(),
}),
(other, _) => Err(ExternalResolverError::UnknownKind {
kind: other.to_string(),
available: available_external_resolvers()
.into_iter()
.map(|i| i.name.to_string())
.collect(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Schema, ShapeDecl, ShapeExpr};
use crate::ir::actions::semantic_actions_registry::SemanticActionsRegistry;
fn label(iri: &str) -> ShapeExprLabel {
ShapeExprLabel::iri_unchecked(iri)
}
fn schema_with(decls: Vec<ShapeDecl>) -> Schema {
Schema::default().with_shapes(Some(decls))
}
fn dummy_ctx<'a>(node: &'a Node, schema: &'a SchemaIR) -> ExternalResolveCtx<'a> {
ExternalResolveCtx {
node,
shape_idx: ShapeLabelIdx::default(),
shape_label: None,
schema,
}
}
#[test]
fn default_registry_rejects() {
let reg = ExternalShapeResolverRegistry::default();
let names: Vec<_> = reg.resolvers().iter().map(|r| r.name().to_string()).collect();
assert_eq!(names, vec!["reject-all".to_string()]);
let node = Node::iri(IriS::new_unchecked("http://example/n"));
let schema = SchemaIR::new(SemanticActionsRegistry::default());
let ctx = dummy_ctx(&node, &schema);
assert!(matches!(reg.dispatch(&ctx), DispatchOutcome::NonConformant { .. }));
}
#[test]
fn empty_registry_abstains() {
let reg = ExternalShapeResolverRegistry::empty();
let node = Node::iri(IriS::new_unchecked("http://example/n"));
let schema = SchemaIR::new(SemanticActionsRegistry::default());
let ctx = dummy_ctx(&node, &schema);
assert!(matches!(reg.dispatch(&ctx), DispatchOutcome::Abstain));
}
#[test]
fn schema_resolver_substitutes_matching_label() {
let sext = label("http://a.example/Sext");
let externs_shape = ShapeExpr::empty_shape();
let externs = schema_with(vec![ShapeDecl::new(sext.clone(), externs_shape.clone(), false)]);
let main = schema_with(vec![ShapeDecl::new(sext.clone(), ShapeExpr::External, false)]);
let resolver = SchemaExternalResolver::from_schema("test", externs);
let rewritten = resolver.rewrite_ast(main);
let decls = rewritten.shapes().expect("shapes present");
assert_eq!(decls.len(), 1);
assert!(!matches!(decls[0].shape_expr, ShapeExpr::External));
assert_eq!(decls[0].shape_expr, externs_shape);
}
#[test]
fn schema_resolver_leaves_unknown_labels_external() {
let known = label("http://a.example/Known");
let unknown = label("http://a.example/Unknown");
let externs = schema_with(vec![ShapeDecl::new(known, ShapeExpr::empty_shape(), false)]);
let main = schema_with(vec![ShapeDecl::new(unknown, ShapeExpr::External, false)]);
let resolver = SchemaExternalResolver::from_schema("test", externs);
let rewritten = resolver.rewrite_ast(main);
let decls = rewritten.shapes().unwrap();
assert!(matches!(decls[0].shape_expr, ShapeExpr::External));
}
#[test]
fn spec_reject_all() {
let r = resolver_from_spec("reject-all").expect("parses");
assert_eq!(r.name(), "reject-all");
}
#[test]
fn spec_reject_all_with_arg_is_rejected() {
let err = resolver_from_spec("reject-all:foo").expect_err("forbidden arg");
assert!(matches!(err, ExternalResolverError::ForbiddenArg { .. }));
}
#[test]
fn spec_schema_missing_arg() {
let err = resolver_from_spec("schema").expect_err("needs arg");
assert!(matches!(err, ExternalResolverError::MissingArg { .. }));
let err = resolver_from_spec("schema:").expect_err("needs non-empty arg");
assert!(matches!(err, ExternalResolverError::MissingArg { .. }));
}
#[test]
fn spec_unknown_kind_reports_available() {
let err = resolver_from_spec("bogus").expect_err("unknown kind");
match err {
ExternalResolverError::UnknownKind { kind, available } => {
assert_eq!(kind, "bogus");
assert!(available.contains(&"reject-all".to_string()));
assert!(available.contains(&"schema".to_string()));
},
other => panic!("expected UnknownKind, got {other:?}"),
}
}
#[test]
fn available_lists_two_built_ins() {
let infos = available_external_resolvers();
let names: Vec<_> = infos.iter().map(|i| i.name).collect();
assert_eq!(names, vec!["reject-all", "schema"]);
}
#[test]
fn registry_runs_user_resolvers_before_default() {
let sext = label("http://a.example/Sext");
let externs = schema_with(vec![ShapeDecl::new(sext.clone(), ShapeExpr::empty_shape(), false)]);
let resolver = SchemaExternalResolver::from_schema("test", externs);
let reg = ExternalShapeResolverRegistry::default().with_resolver(resolver);
let names: Vec<_> = reg.resolvers().iter().map(|r| r.name().to_string()).collect();
assert_eq!(names, vec!["test".to_string(), "reject-all".to_string()]);
let main = schema_with(vec![ShapeDecl::new(sext, ShapeExpr::External, false)]);
let rewritten = reg.rewrite_ast(main);
assert!(!matches!(
rewritten.shapes().unwrap()[0].shape_expr,
ShapeExpr::External
));
}
}