Skip to main content

shifty_parse/
lib.rs

1//! RDF shapes graph -> formalism IR lowering (Layer 2).
2//!
3//! [`parse_turtle`] reads a SHACL shapes graph and lowers all supported Core +
4//! AF vocabulary into the [`shifty_algebra::Schema`] IR, applying every sugar
5//! rule from `docs/01-gap-analysis.md`. Unsupported custom components, JS, and
6//! richer AF constructs produce [`Diagnostic`]s rather than silent wrong
7//! answers.
8
9pub 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
20/// The result of lowering a shapes graph.
21pub struct ParseOutput {
22    pub schema: Schema,
23    pub diagnostics: Vec<Diagnostic>,
24}
25
26/// Load a Turtle shapes graph (for inspecting the raw RDF stage).
27pub 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
35/// Parse and lower a Turtle shapes graph into the algebra IR.
36pub fn parse_turtle(data: &[u8], base: Option<&str>) -> Result<ParseOutput, ParseError> {
37    let loaded = Loaded::from_turtle(data, base)?;
38    let lowered = lower::lower(&loaded);
39    Ok(ParseOutput {
40        schema: lowered.schema,
41        diagnostics: lowered.diagnostics,
42    })
43}
44
45/// Lower an already-loaded graph into the algebra IR.
46pub fn parse_loaded(loaded: &Loaded) -> ParseOutput {
47    let lowered = lower::lower(loaded);
48    ParseOutput {
49        schema: lowered.schema,
50        diagnostics: lowered.diagnostics,
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use shifty_algebra::render::schema_to_text;
58
59    const SHAPES: &str = r#"
60        @prefix sh: <http://www.w3.org/ns/shacl#> .
61        @prefix ex: <http://ex/> .
62        @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
63
64        ex:PersonShape a sh:NodeShape ;
65            sh:targetClass ex:Person ;
66            sh:property [
67                sh:path ex:name ;
68                sh:minCount 1 ;
69                sh:maxCount 1 ;
70                sh:datatype xsd:string ;
71            ] ;
72            sh:property [
73                sh:path [ sh:inversePath ex:child ] ;
74                sh:nodeKind sh:IRI ;
75            ] .
76    "#;
77
78    #[test]
79    fn lowers_person_shape() {
80        let out = parse_turtle(SHAPES.as_bytes(), None).unwrap();
81        assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
82
83        let text = schema_to_text(&out.schema);
84        // a class-target statement was produced
85        assert!(text.contains("rdf:type/rdfs:subClassOf*"), "text:\n{text}");
86        // cardinality on ex:name lowered to an interval count
87        assert!(text.contains("[1..1] <http://ex/name>"), "text:\n{text}");
88        // inverse path rendered
89        assert!(text.contains("^<http://ex/child>"), "text:\n{text}");
90        // datatype facet present
91        assert!(text.contains("datatype(xsd:string)"), "text:\n{text}");
92    }
93
94    #[test]
95    fn lowers_triple_rule() {
96        let ttl = r#"
97            @prefix sh: <http://www.w3.org/ns/shacl#> .
98            @prefix ex: <http://ex/> .
99            ex:S a sh:NodeShape ;
100                sh:targetClass ex:Rectangle ;
101                sh:rule [
102                    a sh:TripleRule ;
103                    sh:subject sh:this ;
104                    sh:predicate ex:area ;
105                    sh:object [ sh:path ex:width ] ;
106                    sh:condition ex:S ;
107                    sh:order 1 ;
108                ] .
109        "#;
110        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
111        assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
112        assert_eq!(out.schema.rules.len(), 1);
113        let r = &out.schema.rules[0];
114        assert_eq!(r.order, Some(1));
115        assert_eq!(r.conditions.len(), 1);
116        use shifty_algebra::{NodeExpr, RuleHead};
117        match &r.head {
118            RuleHead::Triple {
119                subject,
120                predicate,
121                object,
122            } => {
123                assert!(matches!(subject, NodeExpr::This));
124                assert!(matches!(predicate, NodeExpr::Constant(_)));
125                assert!(matches!(object, NodeExpr::Path(_)));
126            }
127            other => panic!("expected TripleRule, got {other:?}"),
128        }
129    }
130
131    #[test]
132    fn lowers_sparql_rule_opaque() {
133        let ttl = r#"
134            @prefix sh: <http://www.w3.org/ns/shacl#> .
135            @prefix ex: <http://ex/> .
136            ex:S a sh:NodeShape ;
137                sh:targetNode ex:x ;
138                sh:rule [ a sh:SPARQLRule ; sh:construct "CONSTRUCT { ?this ex:p ?this } WHERE {}" ] .
139        "#;
140        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
141        assert_eq!(out.schema.rules.len(), 1);
142        assert!(matches!(
143            out.schema.rules[0].head,
144            shifty_algebra::RuleHead::Sparql(_)
145        ));
146    }
147
148    #[test]
149    fn lowers_sparql_constraint() {
150        let ttl = r#"
151            @prefix sh: <http://www.w3.org/ns/shacl#> .
152            @prefix ex: <http://ex/> .
153            ex:S a sh:NodeShape ;
154                sh:targetNode ex:x ;
155                sh:sparql [ sh:select "SELECT $this WHERE {}" ] .
156        "#;
157        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
158        assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
159        let root = out.schema.statements[0].shape;
160        let shifty_algebra::Shape::Annotated { severity, shape } = out.schema.arena.get(root)
161        else {
162            panic!("expected severity annotation");
163        };
164        assert_eq!(severity, &shifty_algebra::Severity::Violation);
165        assert!(matches!(
166            out.schema.arena.get(*shape),
167            shifty_algebra::Shape::Sparql(_)
168        ));
169    }
170
171    #[test]
172    fn lowers_sparql_target() {
173        let ttl = r#"
174            @prefix sh: <http://www.w3.org/ns/shacl#> .
175            @prefix ex: <http://ex/> .
176            ex:S a sh:NodeShape ;
177                sh:target [ sh:select "SELECT ?this WHERE { ?this a ex:Person }" ] .
178        "#;
179        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
180        assert!(out.diagnostics.is_empty(), "diags: {:?}", out.diagnostics);
181        assert!(matches!(
182            out.schema.statements[0].selector,
183            shifty_algebra::Selector::Sparql(_)
184        ));
185    }
186}