shacl 0.3.1

A SHACL validator for RDF data, implemented in Rust.
Documentation
use crate::ast::ASTComponent;
use rudof_rdf::rdf_core::parser::rdf_node_parser::constructors::{
    ObjectsPropertyParser, SingleBoolPropertyParser, SingleIntegerPropertyParser,
};
use rudof_rdf::rdf_core::parser::rdf_node_parser::{ParserExt, RDFNodeParse};
use rudof_rdf::rdf_core::term::Object;
use rudof_rdf::rdf_core::term::literal::ConcreteLiteral;
use rudof_rdf::rdf_core::vocabs::ShaclVocab;
use rudof_rdf::rdf_core::{FocusRDF, RDFError, SHACLPath};
use std::collections::HashSet;
use std::marker::PhantomData;

/// This parser gets the siblings of a focus node
/// Siblings are the other qualified value shapes that share the same parent(s)
/// The defnition in the spec is: https://www.w3.org/TR/shacl12-core/#dfn-sibling-shapes
/// "Let Q be a shape in shapes graph G that declares a qualified cardinality constraint
/// (by having values for sh:qualifiedValueShape and at least one of sh:qualifiedMinCount
/// or sh:qualifiedMaxCount).
/// Let ps be the set of shapes in G that have Q as a value of sh:property.
/// If Q has true as a value for sh:qualifiedValueShapesDisjoint then the set of sibling
/// shapes for Q is defined as the set of all values of the SPARQL property path
/// sh:property/sh:qualifiedValueShape for any shape in ps minus the value of
/// sh:qualifiedValueShape of Q itself. The set of sibling shapes is empty otherwise."
struct QualifiedValueShapeSiblings<RDF: FocusRDF> {
    _marker: PhantomData<RDF>,
    property_qualified_value_shape_path: SHACLPath,
}

impl<RDF: FocusRDF> RDFNodeParse<RDF> for QualifiedValueShapeSiblings<RDF> {
    type Output = Vec<Object>;

    fn parse_focused(&self, rdf: &mut RDF) -> Result<Self::Output, RDFError> {
        match rdf.get_focus() {
            None => Err(RDFError::NoFocusNodeError),
            Some(focus) => {
                let mut siblings = Vec::new();
                let maybe_disjoint = rdf.object_for(focus, &ShaclVocab::sh_qualified_value_shapes_disjoint().into())?;
                if let Some(disjoint) = maybe_disjoint {
                    match disjoint {
                        Object::Literal(ConcreteLiteral::BooleanLiteral(true)) => {
                            let qvs = rdf.objects_for(focus, &ShaclVocab::sh_qualified_value_shape().into())?;
                            if !qvs.is_empty() {
                                let ps = rdf.subjects_for(&ShaclVocab::sh_property().into(), focus)?;
                                for property_parent in ps {
                                    let candidate_siblings = rdf.objects_for_shacl_path(
                                        &property_parent,
                                        &self.property_qualified_value_shape_path,
                                    )?;
                                    for sibling in candidate_siblings {
                                        if !qvs.contains(&sibling) {
                                            let sibling_node = RDF::term_as_object(&sibling)?;
                                            siblings.push(sibling_node);
                                        }
                                    }
                                }
                            }
                        },
                        Object::Literal(ConcreteLiteral::BooleanLiteral(false)) => {},
                        _ => { /* TODO - Raise error */ },
                    }
                }
                Ok(siblings)
            },
        }
    }
}

pub(crate) fn qualified_value_shape<RDF: FocusRDF>() -> impl RDFNodeParse<RDF, Output = Vec<ASTComponent>> {
    ObjectsPropertyParser::new(ShaclVocab::sh_qualified_value_shape())
        .then(|qvs| parse_qualified_value_shape::<RDF>(qvs.into_iter().collect()))
}

fn qualified_value_shape_disjoint_parser<RDF: FocusRDF>() -> impl RDFNodeParse<RDF, Output = Option<bool>> {
    SingleBoolPropertyParser::new(ShaclVocab::sh_qualified_value_shapes_disjoint()).optional()
}

fn qualified_min_count_parser<RDF: FocusRDF>() -> impl RDFNodeParse<RDF, Output = Option<isize>> {
    SingleIntegerPropertyParser::new(ShaclVocab::sh_qualified_min_count()).optional()
}

fn qualified_max_count_parser<RDF: FocusRDF>() -> impl RDFNodeParse<RDF, Output = Option<isize>> {
    SingleIntegerPropertyParser::new(ShaclVocab::sh_qualified_max_count()).optional()
}

fn qualified_value_shape_siblings<RDF: FocusRDF>() -> QualifiedValueShapeSiblings<RDF> {
    QualifiedValueShapeSiblings {
        _marker: PhantomData,
        property_qualified_value_shape_path: SHACLPath::sequence(vec![
            SHACLPath::iri(ShaclVocab::sh_property()),
            SHACLPath::iri(ShaclVocab::sh_qualified_value_shape()),
        ]),
    }
}

fn parse_qualified_value_shape<RDF: FocusRDF>(
    qvs: HashSet<Object>,
) -> impl RDFNodeParse<RDF, Output = Vec<ASTComponent>> {
    qualified_value_shape_disjoint_parser()
        .and(qualified_min_count_parser())
        .and(qualified_max_count_parser())
        .and(qualified_value_shape_siblings())
        .flat_map(move |(((maybe_disjoint, maybe_mins), maybe_maxs), siblings)| {
            Ok(build_qualified_shape(
                qvs.clone(),
                maybe_disjoint,
                maybe_mins,
                maybe_maxs,
                siblings,
            ))
        })
}

fn build_qualified_shape(
    terms: HashSet<Object>,
    disjoint: Option<bool>,
    q_min_count: Option<isize>,
    q_max_count: Option<isize>,
    siblings: Vec<Object>,
) -> Vec<ASTComponent> {
    let mut result = Vec::new();
    for term in terms {
        let shape = ASTComponent::QualifiedValueShape {
            shape: term,
            q_min_count,
            q_max_count,
            disjoint,
            siblings: siblings.clone(),
        };
        result.push(shape);
    }
    result
}