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