shifty_parse/
functions.rs1use 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}