Skip to main content

shifty_parse/
functions.rs

1//! Discover and canonicalize document-owned SHACL function definitions.
2
3use crate::graph::Loaded;
4use crate::lower::canonical_sparql_query;
5use crate::vocab;
6use oxrdf::{NamedOrBlankNode, Term};
7use shifty_algebra::FunctionDef;
8use spargebra::Query;
9use spargebra::SparqlParser;
10use spargebra::algebra::GraphPattern;
11
12pub fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
13    let mut out = Vec::new();
14    for func in shapes
15        .graph
16        .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
17        .map(|subject| subject.into_owned())
18        .collect::<Vec<_>>()
19    {
20        let NamedOrBlankNode::NamedNode(iri) = &func else {
21            continue;
22        };
23        let raw = match shapes
24            .object(&func, vocab::SH_SELECT)
25            .or_else(|| shapes.object(&func, vocab::SH_ASK))
26        {
27            Some(Term::Literal(query)) => query.value().to_string(),
28            _ => continue,
29        };
30        let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
31            continue;
32        };
33        out.push(FunctionDef {
34            iri: iri.clone(),
35            params: function_param_names(shapes, &func),
36            reads_graph: query_reads_graph(&query),
37            query,
38        });
39    }
40    out
41}
42
43fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
44    let mut params: Vec<(i64, String)> = shapes
45        .objects(func, vocab::SH_PARAMETER)
46        .iter()
47        .filter_map(|parameter| {
48            let node = match parameter {
49                Term::NamedNode(node) => NamedOrBlankNode::NamedNode(node.clone()),
50                Term::BlankNode(node) => NamedOrBlankNode::BlankNode(node.clone()),
51                Term::Literal(_) => return None,
52            };
53            let order = match shapes.object(&node, vocab::SH_ORDER) {
54                Some(Term::Literal(value)) => value.value().parse::<i64>().unwrap_or(0),
55                _ => 0,
56            };
57            let name = match shapes.object(&node, vocab::SH_NAME) {
58                Some(Term::Literal(value)) => value.value().to_string(),
59                _ => match shapes.object(&node, vocab::SH_PATH) {
60                    Some(Term::NamedNode(path)) => local_name(path.as_str()).to_string(),
61                    _ => return None,
62                },
63            };
64            Some((order, name))
65        })
66        .collect();
67    params.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
68    params.into_iter().map(|(_, name)| name).collect()
69}
70
71fn local_name(iri: &str) -> &str {
72    iri.rsplit(['#', '/']).next().unwrap_or(iri)
73}
74
75fn query_reads_graph(query: &str) -> bool {
76    let Ok(parsed) = SparqlParser::new().parse_query(query) else {
77        return true;
78    };
79    fn walk(pattern: &GraphPattern) -> bool {
80        use GraphPattern::*;
81        match pattern {
82            Bgp { patterns } => !patterns.is_empty(),
83            Path { .. } | Graph { .. } | Service { .. } => true,
84            Join { left, right }
85            | Union { left, right }
86            | Minus { left, right }
87            | Lateral { left, right } => walk(left) || walk(right),
88            LeftJoin { left, right, .. } => walk(left) || walk(right),
89            Filter { inner, .. }
90            | Extend { inner, .. }
91            | Project { inner, .. }
92            | Distinct { inner }
93            | Reduced { inner }
94            | Slice { inner, .. }
95            | OrderBy { inner, .. }
96            | Group { inner, .. } => walk(inner),
97            _ => false,
98        }
99    }
100    match &parsed {
101        Query::Select { pattern, .. } | Query::Ask { pattern, .. } => walk(pattern),
102        _ => true,
103    }
104}