1pub mod diagnostics;
10pub mod functions;
11pub mod graph;
12pub mod lower;
13pub mod path;
14pub mod sparql_path;
15pub mod vocab;
16
17pub use diagnostics::{DiagLevel, Diagnostic, ParseError};
18pub use functions::collect_functions;
19pub use graph::{Loaded, RdfFormat};
20pub use sparql_path::parse_property_path;
21
22use shifty_algebra::Schema;
23
24pub struct ParseOutput {
26 pub schema: Schema,
27 pub diagnostics: Vec<Diagnostic>,
28}
29
30impl ParseOutput {
31 pub fn require_valid(&self) -> Result<(), ParseError> {
36 let errors: Vec<_> = self
37 .diagnostics
38 .iter()
39 .filter(|diagnostic| diagnostic.level == DiagLevel::Error)
40 .collect();
41 if errors.is_empty() {
42 return Ok(());
43 }
44 Err(ParseError(format!(
45 "invalid shapes graph: {}",
46 errors
47 .iter()
48 .map(ToString::to_string)
49 .collect::<Vec<_>>()
50 .join("; ")
51 )))
52 }
53}
54
55pub fn load_turtle(data: &[u8], base: Option<&str>) -> Result<Loaded, ParseError> {
57 Loaded::from_turtle(data, base)
58}
59
60pub fn load_ntriples(data: &[u8]) -> Result<Loaded, ParseError> {
61 Loaded::from_ntriples(data)
62}
63
64pub fn load_rdf_auto(
67 data: &[u8],
68 content_type: Option<&str>,
69 source: Option<&str>,
70 base: Option<&str>,
71) -> Result<Loaded, ParseError> {
72 Loaded::from_rdf_auto(data, content_type, source, base)
73}
74
75pub fn load_rdf_auto_with_format(
77 data: &[u8],
78 content_type: Option<&str>,
79 source: Option<&str>,
80 base: Option<&str>,
81) -> Result<(Loaded, RdfFormat), ParseError> {
82 Loaded::from_rdf_auto_reporting(data, content_type, source, base)
83}
84
85pub fn parse_turtle(data: &[u8], base: Option<&str>) -> Result<ParseOutput, ParseError> {
87 let loaded = Loaded::from_turtle(data, base)?;
88 let lowered = lower::lower(&loaded);
89 Ok(ParseOutput {
90 schema: lowered.schema,
91 diagnostics: lowered.diagnostics,
92 })
93}
94
95pub fn parse_loaded(loaded: &Loaded) -> ParseOutput {
97 let lowered = lower::lower(loaded);
98 ParseOutput {
99 schema: lowered.schema,
100 diagnostics: lowered.diagnostics,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use shifty_algebra::render::schema_to_text;
108
109 const SHAPES: &str = r#"
110 @prefix sh: <http://www.w3.org/ns/shacl#> .
111 @prefix ex: <http://ex/> .
112 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
113
114 ex:PersonShape a sh:NodeShape ;
115 sh:targetClass ex:Person ;
116 sh:property [
117 sh:path ex:name ;
118 sh:minCount 1 ;
119 sh:maxCount 1 ;
120 sh:datatype xsd:string ;
121 ] ;
122 sh:property [
123 sh:path [ sh:inversePath ex:child ] ;
124 sh:nodeKind sh:IRI ;
125 ] .
126 "#;
127
128 #[test]
129 fn lowers_person_shape() {
130 let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
131 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
132
133 let text = schema_to_text(&out.schema);
134 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
136 assert!(text.contains("[1..1] ex:name"), "text:\n{text}");
139 assert!(text.contains("^ex:child"), "text:\n{text}");
141 assert!(text.contains("datatype(xsd:string)"), "text:\n{text}");
143 }
144
145 #[test]
146 fn invalid_lowering_diagnostics_are_fatal() {
147 let shapes = br#"
148 @prefix sh: <http://www.w3.org/ns/shacl#> .
149 @prefix ex: <http://ex/> .
150 ex:S a sh:NodeShape ;
151 sh:sparql [
152 sh:select "SELECT $this WHERE { $this missing:p ?value }"
153 ] .
154 "#;
155 let out = parse_turtle(shapes, None).unwrap();
156 let error = out.require_valid().unwrap_err();
157 assert!(error.to_string().contains("invalid SPARQL query"));
158 }
159
160 #[test]
161 fn sources_provenance_named_and_blank_shapes() {
162 let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
163 let named_id = out
164 .schema
165 .names
166 .iter()
167 .find(|(_, names)| names.iter().any(|name| name == "http://ex/PersonShape"))
168 .map(|(id, _)| *id)
169 .expect("PersonShape should be named");
170 assert_eq!(
171 out.schema.sources.get(&named_id),
172 Some(&shifty_algebra::Term::NamedNode(
173 shifty_algebra::NamedNode::new("http://ex/PersonShape").unwrap()
174 ))
175 );
176
177 let blank_sources = out
180 .schema
181 .sources
182 .values()
183 .filter(|t| matches!(t, shifty_algebra::Term::BlankNode(_)))
184 .count();
185 assert_eq!(blank_sources, 2, "sources: {:?}", out.schema.sources);
186
187 let out2 = parse_turtle(SHAPES.as_bytes(), None).unwrap();
189 assert_eq!(out.schema.sources.len(), out2.schema.sources.len());
190 }
191
192 #[test]
193 fn auto_loads_rdf_xml_shapes() {
194 let rdfxml = br#"<?xml version="1.0"?>
195 <rdf:RDF
196 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
197 xmlns:sh="http://www.w3.org/ns/shacl#"
198 xmlns:ex="http://ex/">
199 <sh:NodeShape rdf:about="http://ex/S">
200 <sh:targetClass rdf:resource="http://ex/Thing"/>
201 </sh:NodeShape>
202 </rdf:RDF>"#;
203 let loaded = load_rdf_auto(
204 rdfxml,
205 Some("application/rdf+xml"),
206 Some("https://example.test/shapes.rdf"),
207 Some("https://example.test/shapes.rdf"),
208 )
209 .unwrap();
210 let out = parse_loaded(&loaded);
211 let text = schema_to_text(&out.schema);
212 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
213 }
214
215 #[test]
216 fn lowers_triple_rule() {
217 let ttl = r#"
218 @prefix sh: <http://www.w3.org/ns/shacl#> .
219 @prefix ex: <http://ex/> .
220 ex:S a sh:NodeShape ;
221 sh:targetClass ex:Rectangle ;
222 sh:rule [
223 a sh:TripleRule ;
224 sh:subject sh:this ;
225 sh:predicate ex:area ;
226 sh:object [ sh:path ex:width ] ;
227 sh:condition ex:S ;
228 sh:order 1 ;
229 ] .
230 "#;
231 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
232 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
233 assert_eq!(out.schema.rules.len(), 1);
234 let r = &out.schema.rules[0];
235 assert_eq!(r.order, Some(1));
236 assert_eq!(r.conditions.len(), 1);
237 use shifty_algebra::{NodeExpr, RuleHead};
238 match &r.head {
239 RuleHead::Triple {
240 subject,
241 predicate,
242 object,
243 } => {
244 assert!(matches!(subject, NodeExpr::This));
245 assert!(matches!(predicate, NodeExpr::Constant(_)));
246 assert!(matches!(object, NodeExpr::Path(_)));
247 }
248 other => panic!("expected TripleRule, got {other:?}"),
249 }
250 }
251
252 #[test]
253 fn lowers_sparql_rule_opaque() {
254 let ttl = r#"
255 @prefix sh: <http://www.w3.org/ns/shacl#> .
256 @prefix ex: <http://ex/> .
257 ex:S a sh:NodeShape ;
258 sh:targetNode ex:x ;
259 sh:rule [ a sh:SPARQLRule ; sh:construct "CONSTRUCT { ?this ex:p ?this } WHERE {}" ] .
260 "#;
261 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
262 assert_eq!(out.schema.rules.len(), 1);
263 assert!(matches!(
264 out.schema.rules[0].head,
265 shifty_algebra::RuleHead::Sparql(_)
266 ));
267 }
268
269 #[test]
270 fn lowers_sparql_constraint() {
271 let ttl = r#"
272 @prefix sh: <http://www.w3.org/ns/shacl#> .
273 @prefix ex: <http://ex/> .
274 ex:S a sh:NodeShape ;
275 sh:targetNode ex:x ;
276 sh:sparql [ sh:select "SELECT $this WHERE {}" ] .
277 "#;
278 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
279 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
280 let root = out.schema.statements[0].shape;
281 let shifty_algebra::Shape::Annotated {
282 severity, shape, ..
283 } = out.schema.arena.get(root)
284 else {
285 panic!("expected severity annotation");
286 };
287 assert_eq!(severity, &shifty_algebra::Severity::Violation);
288 assert!(matches!(
289 out.schema.arena.get(*shape),
290 shifty_algebra::Shape::Sparql(_)
291 ));
292 }
293
294 #[test]
295 fn lowers_sparql_target() {
296 let ttl = r#"
297 @prefix sh: <http://www.w3.org/ns/shacl#> .
298 @prefix ex: <http://ex/> .
299 ex:S a sh:NodeShape ;
300 sh:target [ sh:select "SELECT ?this WHERE { ?this a ex:Person }" ] .
301 "#;
302 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
303 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
304 assert!(matches!(
305 out.schema.statements[0].selector,
306 shifty_algebra::Selector::Sparql(_)
307 ));
308 }
309}