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
28pub fn load_turtle(data: &[u8], base: Option<&str>) -> Result<Loaded, ParseError> {
30 Loaded::from_turtle(data, base)
31}
32
33pub fn load_ntriples(data: &[u8]) -> Result<Loaded, ParseError> {
34 Loaded::from_ntriples(data)
35}
36
37pub fn load_rdf_auto(
40 data: &[u8],
41 content_type: Option<&str>,
42 source: Option<&str>,
43 base: Option<&str>,
44) -> Result<Loaded, ParseError> {
45 Loaded::from_rdf_auto(data, content_type, source, base)
46}
47
48pub fn parse_turtle(data: &[u8], base: Option<&str>) -> Result<ParseOutput, ParseError> {
50 let loaded = Loaded::from_turtle(data, base)?;
51 let lowered = lower::lower(&loaded);
52 Ok(ParseOutput {
53 schema: lowered.schema,
54 diagnostics: lowered.diagnostics,
55 })
56}
57
58pub fn parse_loaded(loaded: &Loaded) -> ParseOutput {
60 let lowered = lower::lower(loaded);
61 ParseOutput {
62 schema: lowered.schema,
63 diagnostics: lowered.diagnostics,
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use shifty_algebra::render::schema_to_text;
71
72 const SHAPES: &str = r#"
73 @prefix sh: <http://www.w3.org/ns/shacl#> .
74 @prefix ex: <http://ex/> .
75 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
76
77 ex:PersonShape a sh:NodeShape ;
78 sh:targetClass ex:Person ;
79 sh:property [
80 sh:path ex:name ;
81 sh:minCount 1 ;
82 sh:maxCount 1 ;
83 sh:datatype xsd:string ;
84 ] ;
85 sh:property [
86 sh:path [ sh:inversePath ex:child ] ;
87 sh:nodeKind sh:IRI ;
88 ] .
89 "#;
90
91 #[test]
92 fn lowers_person_shape() {
93 let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
94 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
95
96 let text = schema_to_text(&out.schema);
97 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
99 assert!(text.contains("[1..1] <http://ex/name>"), "text:\n{text}");
101 assert!(text.contains("^<http://ex/child>"), "text:\n{text}");
103 assert!(text.contains("datatype(xsd:string)"), "text:\n{text}");
105 }
106
107 #[test]
108 fn sources_provenance_named_and_blank_shapes() {
109 let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
110 let named_id = out
111 .schema
112 .names
113 .iter()
114 .find(|(_, names)| names.iter().any(|name| name == "http://ex/PersonShape"))
115 .map(|(id, _)| *id)
116 .expect("PersonShape should be named");
117 assert_eq!(
118 out.schema.sources.get(&named_id),
119 Some(&shifty_algebra::Term::NamedNode(
120 shifty_algebra::NamedNode::new("http://ex/PersonShape").unwrap()
121 ))
122 );
123
124 let blank_sources = out
127 .schema
128 .sources
129 .values()
130 .filter(|t| matches!(t, shifty_algebra::Term::BlankNode(_)))
131 .count();
132 assert_eq!(blank_sources, 2, "sources: {:?}", out.schema.sources);
133
134 let out2 = parse_turtle(SHAPES.as_bytes(), None).unwrap();
136 assert_eq!(out.schema.sources.len(), out2.schema.sources.len());
137 }
138
139 #[test]
140 fn auto_loads_rdf_xml_shapes() {
141 let rdfxml = br#"<?xml version="1.0"?>
142 <rdf:RDF
143 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
144 xmlns:sh="http://www.w3.org/ns/shacl#"
145 xmlns:ex="http://ex/">
146 <sh:NodeShape rdf:about="http://ex/S">
147 <sh:targetClass rdf:resource="http://ex/Thing"/>
148 </sh:NodeShape>
149 </rdf:RDF>"#;
150 let loaded = load_rdf_auto(
151 rdfxml,
152 Some("application/rdf+xml"),
153 Some("https://example.test/shapes.rdf"),
154 Some("https://example.test/shapes.rdf"),
155 )
156 .unwrap();
157 let out = parse_loaded(&loaded);
158 let text = schema_to_text(&out.schema);
159 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
160 }
161
162 #[test]
163 fn lowers_triple_rule() {
164 let ttl = r#"
165 @prefix sh: <http://www.w3.org/ns/shacl#> .
166 @prefix ex: <http://ex/> .
167 ex:S a sh:NodeShape ;
168 sh:targetClass ex:Rectangle ;
169 sh:rule [
170 a sh:TripleRule ;
171 sh:subject sh:this ;
172 sh:predicate ex:area ;
173 sh:object [ sh:path ex:width ] ;
174 sh:condition ex:S ;
175 sh:order 1 ;
176 ] .
177 "#;
178 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
179 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
180 assert_eq!(out.schema.rules.len(), 1);
181 let r = &out.schema.rules[0];
182 assert_eq!(r.order, Some(1));
183 assert_eq!(r.conditions.len(), 1);
184 use shifty_algebra::{NodeExpr, RuleHead};
185 match &r.head {
186 RuleHead::Triple {
187 subject,
188 predicate,
189 object,
190 } => {
191 assert!(matches!(subject, NodeExpr::This));
192 assert!(matches!(predicate, NodeExpr::Constant(_)));
193 assert!(matches!(object, NodeExpr::Path(_)));
194 }
195 other => panic!("expected TripleRule, got {other:?}"),
196 }
197 }
198
199 #[test]
200 fn lowers_sparql_rule_opaque() {
201 let ttl = r#"
202 @prefix sh: <http://www.w3.org/ns/shacl#> .
203 @prefix ex: <http://ex/> .
204 ex:S a sh:NodeShape ;
205 sh:targetNode ex:x ;
206 sh:rule [ a sh:SPARQLRule ; sh:construct "CONSTRUCT { ?this ex:p ?this } WHERE {}" ] .
207 "#;
208 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
209 assert_eq!(out.schema.rules.len(), 1);
210 assert!(matches!(
211 out.schema.rules[0].head,
212 shifty_algebra::RuleHead::Sparql(_)
213 ));
214 }
215
216 #[test]
217 fn lowers_sparql_constraint() {
218 let ttl = r#"
219 @prefix sh: <http://www.w3.org/ns/shacl#> .
220 @prefix ex: <http://ex/> .
221 ex:S a sh:NodeShape ;
222 sh:targetNode ex:x ;
223 sh:sparql [ sh:select "SELECT $this WHERE {}" ] .
224 "#;
225 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
226 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
227 let root = out.schema.statements[0].shape;
228 let shifty_algebra::Shape::Annotated {
229 severity, shape, ..
230 } = out.schema.arena.get(root)
231 else {
232 panic!("expected severity annotation");
233 };
234 assert_eq!(severity, &shifty_algebra::Severity::Violation);
235 assert!(matches!(
236 out.schema.arena.get(*shape),
237 shifty_algebra::Shape::Sparql(_)
238 ));
239 }
240
241 #[test]
242 fn lowers_sparql_target() {
243 let ttl = r#"
244 @prefix sh: <http://www.w3.org/ns/shacl#> .
245 @prefix ex: <http://ex/> .
246 ex:S a sh:NodeShape ;
247 sh:target [ sh:select "SELECT ?this WHERE { ?this a ex:Person }" ] .
248 "#;
249 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
250 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
251 assert!(matches!(
252 out.schema.statements[0].selector,
253 shifty_algebra::Selector::Sparql(_)
254 ));
255 }
256}