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 auto_loads_rdf_xml_shapes() {
109 let rdfxml = br#"<?xml version="1.0"?>
110 <rdf:RDF
111 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
112 xmlns:sh="http://www.w3.org/ns/shacl#"
113 xmlns:ex="http://ex/">
114 <sh:NodeShape rdf:about="http://ex/S">
115 <sh:targetClass rdf:resource="http://ex/Thing"/>
116 </sh:NodeShape>
117 </rdf:RDF>"#;
118 let loaded = load_rdf_auto(
119 rdfxml,
120 Some("application/rdf+xml"),
121 Some("https://example.test/shapes.rdf"),
122 Some("https://example.test/shapes.rdf"),
123 )
124 .unwrap();
125 let out = parse_loaded(&loaded);
126 let text = schema_to_text(&out.schema);
127 assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
128 }
129
130 #[test]
131 fn lowers_triple_rule() {
132 let ttl = r#"
133 @prefix sh: <http://www.w3.org/ns/shacl#> .
134 @prefix ex: <http://ex/> .
135 ex:S a sh:NodeShape ;
136 sh:targetClass ex:Rectangle ;
137 sh:rule [
138 a sh:TripleRule ;
139 sh:subject sh:this ;
140 sh:predicate ex:area ;
141 sh:object [ sh:path ex:width ] ;
142 sh:condition ex:S ;
143 sh:order 1 ;
144 ] .
145 "#;
146 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
147 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
148 assert_eq!(out.schema.rules.len(), 1);
149 let r = &out.schema.rules[0];
150 assert_eq!(r.order, Some(1));
151 assert_eq!(r.conditions.len(), 1);
152 use shifty_algebra::{NodeExpr, RuleHead};
153 match &r.head {
154 RuleHead::Triple {
155 subject,
156 predicate,
157 object,
158 } => {
159 assert!(matches!(subject, NodeExpr::This));
160 assert!(matches!(predicate, NodeExpr::Constant(_)));
161 assert!(matches!(object, NodeExpr::Path(_)));
162 }
163 other => panic!("expected TripleRule, got {other:?}"),
164 }
165 }
166
167 #[test]
168 fn lowers_sparql_rule_opaque() {
169 let ttl = r#"
170 @prefix sh: <http://www.w3.org/ns/shacl#> .
171 @prefix ex: <http://ex/> .
172 ex:S a sh:NodeShape ;
173 sh:targetNode ex:x ;
174 sh:rule [ a sh:SPARQLRule ; sh:construct "CONSTRUCT { ?this ex:p ?this } WHERE {}" ] .
175 "#;
176 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
177 assert_eq!(out.schema.rules.len(), 1);
178 assert!(matches!(
179 out.schema.rules[0].head,
180 shifty_algebra::RuleHead::Sparql(_)
181 ));
182 }
183
184 #[test]
185 fn lowers_sparql_constraint() {
186 let ttl = r#"
187 @prefix sh: <http://www.w3.org/ns/shacl#> .
188 @prefix ex: <http://ex/> .
189 ex:S a sh:NodeShape ;
190 sh:targetNode ex:x ;
191 sh:sparql [ sh:select "SELECT $this WHERE {}" ] .
192 "#;
193 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
194 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
195 let root = out.schema.statements[0].shape;
196 let shifty_algebra::Shape::Annotated {
197 severity, shape, ..
198 } = out.schema.arena.get(root)
199 else {
200 panic!("expected severity annotation");
201 };
202 assert_eq!(severity, &shifty_algebra::Severity::Violation);
203 assert!(matches!(
204 out.schema.arena.get(*shape),
205 shifty_algebra::Shape::Sparql(_)
206 ));
207 }
208
209 #[test]
210 fn lowers_sparql_target() {
211 let ttl = r#"
212 @prefix sh: <http://www.w3.org/ns/shacl#> .
213 @prefix ex: <http://ex/> .
214 ex:S a sh:NodeShape ;
215 sh:target [ sh:select "SELECT ?this WHERE { ?this a ex:Person }" ] .
216 "#;
217 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
218 assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
219 assert!(matches!(
220 out.schema.statements[0].selector,
221 shifty_algebra::Selector::Sparql(_)
222 ));
223 }
224}