1pub mod diagnostics;
10pub mod graph;
11pub mod lower;
12pub mod path;
13pub mod vocab;
14
15pub use diagnostics::{DiagLevel, Diagnostic, ParseError};
16pub use graph::{Loaded, RdfFormat};
17
18use shifty_algebra::Schema;
19
20pub struct ParseOutput {
22 pub schema: Schema,
23 pub diagnostics: Vec<Diagnostic>,
24}
25
26pub fn load_turtle(data: &[u8], base: Option<&str>) -> Result<Loaded, ParseError> {
28 Loaded::from_turtle(data, base)
29}
30
31pub fn load_ntriples(data: &[u8]) -> Result<Loaded, ParseError> {
32 Loaded::from_ntriples(data)
33}
34
35pub fn load_rdf_auto(
38 data: &[u8],
39 content_type: Option<&str>,
40 source: Option<&str>,
41 base: Option<&str>,
42) -> Result<Loaded, ParseError> {
43 Loaded::from_rdf_auto(data, content_type, source, base)
44}
45
46pub fn parse_turtle(data: &[u8], base: Option<&str>) -> Result<ParseOutput, ParseError> {
48 let loaded = Loaded::from_turtle(data, base)?;
49 let lowered = lower::lower(&loaded);
50 Ok(ParseOutput {
51 schema: lowered.schema,
52 diagnostics: lowered.diagnostics,
53 })
54}
55
56pub fn parse_loaded(loaded: &Loaded) -> ParseOutput {
58 let lowered = lower::lower(loaded);
59 ParseOutput {
60 schema: lowered.schema,
61 diagnostics: lowered.diagnostics,
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use shifty_algebra::render::schema_to_text;
69
70 const SHAPES: &str = r#"
71 @prefix sh: <http://www.w3.org/ns/shacl#> .
72 @prefix ex: <http://ex/> .
73 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
74
75 ex:PersonShape a sh:NodeShape ;
76 sh:targetClass ex:Person ;
77 sh:property [
78 sh:path ex:name ;
79 sh:minCount 1 ;
80 sh:maxCount 1 ;
81 sh:datatype xsd:string ;
82 ] ;
83 sh:property [
84 sh:path [ sh:inversePath ex:child ] ;
85 sh:nodeKind sh:IRI ;
86 ] .
87 "#;
88
89 #[test]
90 fn lowers_person_shape() {
91 let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
92 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
93
94 let text = schema_to_text(&out.schema);
95 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
97 assert!(text.contains("[1..1] <http://ex/name>"), "text:\n{text}");
99 assert!(text.contains("^<http://ex/child>"), "text:\n{text}");
101 assert!(text.contains("datatype(xsd:string)"), "text:\n{text}");
103 }
104
105 #[test]
106 fn auto_loads_rdf_xml_shapes() {
107 let rdfxml = br#"<?xml version="1.0"?>
108 <rdf:RDF
109 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
110 xmlns:sh="http://www.w3.org/ns/shacl#"
111 xmlns:ex="http://ex/">
112 <sh:NodeShape rdf:about="http://ex/S">
113 <sh:targetClass rdf:resource="http://ex/Thing"/>
114 </sh:NodeShape>
115 </rdf:RDF>"#;
116 let loaded = load_rdf_auto(
117 rdfxml,
118 Some("application/rdf+xml"),
119 Some("https://example.test/shapes.rdf"),
120 Some("https://example.test/shapes.rdf"),
121 )
122 .unwrap();
123 let out = parse_loaded(&loaded);
124 let text = schema_to_text(&out.schema);
125 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
126 }
127
128 #[test]
129 fn lowers_triple_rule() {
130 let ttl = r#"
131 @prefix sh: <http://www.w3.org/ns/shacl#> .
132 @prefix ex: <http://ex/> .
133 ex:S a sh:NodeShape ;
134 sh:targetClass ex:Rectangle ;
135 sh:rule [
136 a sh:TripleRule ;
137 sh:subject sh:this ;
138 sh:predicate ex:area ;
139 sh:object [ sh:path ex:width ] ;
140 sh:condition ex:S ;
141 sh:order 1 ;
142 ] .
143 "#;
144 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
145 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
146 assert_eq!(out.schema.rules.len(), 1);
147 let r = &out.schema.rules[0];
148 assert_eq!(r.order, Some(1));
149 assert_eq!(r.conditions.len(), 1);
150 use shifty_algebra::{NodeExpr, RuleHead};
151 match &r.head {
152 RuleHead::Triple {
153 subject,
154 predicate,
155 object,
156 } => {
157 assert!(matches!(subject, NodeExpr::This));
158 assert!(matches!(predicate, NodeExpr::Constant(_)));
159 assert!(matches!(object, NodeExpr::Path(_)));
160 }
161 other => panic!("expected TripleRule, got {other:?}"),
162 }
163 }
164
165 #[test]
166 fn lowers_sparql_rule_opaque() {
167 let ttl = r#"
168 @prefix sh: <http://www.w3.org/ns/shacl#> .
169 @prefix ex: <http://ex/> .
170 ex:S a sh:NodeShape ;
171 sh:targetNode ex:x ;
172 sh:rule [ a sh:SPARQLRule ; sh:construct "CONSTRUCT { ?this ex:p ?this } WHERE {}" ] .
173 "#;
174 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
175 assert_eq!(out.schema.rules.len(), 1);
176 assert!(matches!(
177 out.schema.rules[0].head,
178 shifty_algebra::RuleHead::Sparql(_)
179 ));
180 }
181
182 #[test]
183 fn lowers_sparql_constraint() {
184 let ttl = r#"
185 @prefix sh: <http://www.w3.org/ns/shacl#> .
186 @prefix ex: <http://ex/> .
187 ex:S a sh:NodeShape ;
188 sh:targetNode ex:x ;
189 sh:sparql [ sh:select "SELECT $this WHERE {}" ] .
190 "#;
191 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
192 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
193 let root = out.schema.statements[0].shape;
194 let shifty_algebra::Shape::Annotated {
195 severity, shape, ..
196 } = out.schema.arena.get(root)
197 else {
198 panic!("expected severity annotation");
199 };
200 assert_eq!(severity, &shifty_algebra::Severity::Violation);
201 assert!(matches!(
202 out.schema.arena.get(*shape),
203 shifty_algebra::Shape::Sparql(_)
204 ));
205 }
206
207 #[test]
208 fn lowers_sparql_target() {
209 let ttl = r#"
210 @prefix sh: <http://www.w3.org/ns/shacl#> .
211 @prefix ex: <http://ex/> .
212 ex:S a sh:NodeShape ;
213 sh:target [ sh:select "SELECT ?this WHERE { ?this a ex:Person }" ] .
214 "#;
215 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
216 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
217 assert!(matches!(
218 out.schema.statements[0].selector,
219 shifty_algebra::Selector::Sparql(_)
220 ));
221 }
222}