Skip to main content

rete_core/
shacl.rs

1//! SHACL Core validation over canonical Rete term triples.
2//!
3//! This module targets the stable W3C SHACL Core recommendation. It deliberately
4//! stays independent of the `.rete` byte layout: callers provide a [`DataGraph`]
5//! made from an opened [`crate::Rete`] or from raw triples, and a [`ShaclShapes`]
6//! graph parsed from Turtle.
7
8use std::collections::{BTreeSet, HashSet};
9
10use thiserror::Error;
11
12use crate::{Rete, TermTriple};
13
14const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
15const RDF_FIRST: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#first>";
16const RDF_REST: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#rest>";
17const RDF_NIL: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#nil>";
18const RDFS_CLASS: &str = "<http://www.w3.org/2000/01/rdf-schema#Class>";
19const RDFS_SUBCLASS_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
20const OWL_CLASS: &str = "<http://www.w3.org/2002/07/owl#Class>";
21const XSD_STRING: &str = "<http://www.w3.org/2001/XMLSchema#string>";
22const RDF_LANG_STRING: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#langString>";
23
24const SH: &str = "http://www.w3.org/ns/shacl#";
25
26macro_rules! sh {
27    ($local:literal) => {
28        concat!("<http://www.w3.org/ns/shacl#", $local, ">")
29    };
30}
31
32type Triple = (String, String, String);
33
34#[derive(Debug, Error)]
35#[non_exhaustive]
36pub enum ShaclError {
37    #[error("failed to parse SHACL shapes Turtle: {0}")]
38    Parse(String),
39    #[error("malformed RDF list at {0}")]
40    MalformedList(String),
41}
42
43/// A read-only view of the data graph for SHACL validation. The validator only
44/// ever asks **targeted** questions — a focus node's values, the subjects of a
45/// predicate, the instances of a class — so this surface is small enough to back
46/// two ways: an in-memory triple set ([`DataGraph`], eager) or a `.rete` file's
47/// index directly ([`ReteGraph`]), which routes each lookup as a range read so a
48/// *remote* validation faults only the tiles holding the shapes' targets, not the
49/// whole graph. The class/instance helpers are derived from the primitives, so a
50/// backend only implements the six lookups.
51pub trait GraphView {
52    /// Objects of `(subject, predicate, ?)`.
53    fn objects(&self, subject: &str, predicate: &str) -> Vec<String>;
54    /// Subjects of `(?, predicate, object)`.
55    fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String>;
56    /// Distinct subjects of `(?, predicate, ?)`.
57    fn subjects_of(&self, predicate: &str) -> Vec<String>;
58    /// Distinct objects of `(?, predicate, ?)`.
59    fn objects_of(&self, predicate: &str) -> Vec<String>;
60    /// Distinct predicates of `(subject, ?, ?)`.
61    fn predicates_for_subject(&self, subject: &str) -> Vec<String>;
62    /// Every node (subject or object). The one inherently **non-targeted** lookup
63    /// — a remote validation that reaches it reads the whole graph (only a general
64    /// inverse path or a target-less shape does).
65    fn all_nodes(&self) -> Vec<String>;
66
67    /// Is `child` a reflexive/transitive `rdfs:subClassOf` of `parent`?
68    fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
69        if child == parent {
70            return true;
71        }
72        let mut seen = HashSet::new();
73        let mut stack = vec![child.to_string()];
74        while let Some(c) = stack.pop() {
75            if !seen.insert(c.clone()) {
76                continue;
77            }
78            for sup in self.objects(&c, RDFS_SUBCLASS_OF) {
79                if sup == parent {
80                    return true;
81                }
82                stack.push(sup);
83            }
84        }
85        false
86    }
87
88    /// The (transitive) subclasses of `parent`.
89    fn subclasses_of(&self, parent: &str) -> BTreeSet<String> {
90        self.subjects_of(RDFS_SUBCLASS_OF)
91            .into_iter()
92            .filter(|s| self.is_subclass_of(s, parent))
93            .collect()
94    }
95
96    /// Instances of `class` (direct, or via a subclass).
97    fn instances_of(&self, class: &str) -> Vec<String> {
98        let mut classes = self.subclasses_of(class);
99        classes.insert(class.to_string());
100        let mut out = Vec::new();
101        for c in &classes {
102            out.extend(self.subjects_with(RDF_TYPE, c));
103        }
104        unique(out)
105    }
106
107    /// Is `node` an instance of `class` (direct, or via a subclass)?
108    fn is_instance_of(&self, node: &str, class: &str) -> bool {
109        self.objects(node, RDF_TYPE)
110            .iter()
111            .any(|c| self.is_subclass_of(c, class))
112    }
113}
114
115/// A validation data graph held fully in memory as a sorted triple vector. Backs
116/// the shapes graph and the **eager** data path; the lazy data path uses
117/// [`ReteGraph`].
118#[derive(Debug, Clone, Default)]
119pub struct DataGraph {
120    triples: Vec<Triple>,
121}
122
123impl DataGraph {
124    pub fn from_triples(triples: Vec<TermTriple>) -> Self {
125        let mut triples = triples;
126        triples.sort();
127        triples.dedup();
128        Self { triples }
129    }
130
131    pub fn from_rete(rete: &Rete, graph: Option<&str>) -> Self {
132        Self::from_triples(rete.dump(graph))
133    }
134
135    fn has(&self, s: &str, p: &str, o: &str) -> bool {
136        self.triples
137            .iter()
138            .any(|(ts, tp, to)| ts == s && tp == p && to == o)
139    }
140}
141
142impl GraphView for DataGraph {
143    fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
144        self.triples
145            .iter()
146            .filter(|(s, p, _)| s == subject && p == predicate)
147            .map(|(_, _, o)| o.clone())
148            .collect()
149    }
150
151    fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String> {
152        unique(
153            self.triples
154                .iter()
155                .filter(|(_, p, o)| p == predicate && o == object)
156                .map(|(s, _, _)| s.clone())
157                .collect(),
158        )
159    }
160
161    fn subjects_of(&self, predicate: &str) -> Vec<String> {
162        unique(
163            self.triples
164                .iter()
165                .filter(|(_, p, _)| p == predicate)
166                .map(|(s, _, _)| s.clone())
167                .collect(),
168        )
169    }
170
171    fn objects_of(&self, predicate: &str) -> Vec<String> {
172        unique(
173            self.triples
174                .iter()
175                .filter(|(_, p, _)| p == predicate)
176                .map(|(_, _, o)| o.clone())
177                .collect(),
178        )
179    }
180
181    fn predicates_for_subject(&self, subject: &str) -> Vec<String> {
182        unique(
183            self.triples
184                .iter()
185                .filter(|(s, _, _)| s == subject)
186                .map(|(_, p, _)| p.clone())
187                .collect(),
188        )
189    }
190
191    fn all_nodes(&self) -> Vec<String> {
192        let mut out = Vec::new();
193        for (s, _, o) in &self.triples {
194            out.push(s.clone());
195            out.push(o.clone());
196        }
197        unique(out)
198    }
199}
200
201/// A SHACL data-graph view backed directly by a `.rete` file's index: every
202/// lookup is a routed pattern query, so over a lazy
203/// ([`Rete::open_ranged_lazy`](crate::Rete::open_ranged_lazy)) open a validation
204/// faults only the tiles holding the shapes' target nodes — not the whole graph.
205/// Views the **default** graph (named-graph validation uses the eager
206/// [`DataGraph`]).
207pub struct ReteGraph<'a> {
208    rete: &'a Rete,
209}
210
211impl<'a> ReteGraph<'a> {
212    pub fn new(rete: &'a Rete) -> Self {
213        Self { rete }
214    }
215}
216
217impl GraphView for ReteGraph<'_> {
218    fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
219        self.rete
220            .query(Some(subject), Some(predicate), None)
221            .into_iter()
222            .map(|(_, _, o)| o)
223            .collect()
224    }
225
226    fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String> {
227        self.rete
228            .query(None, Some(predicate), Some(object))
229            .into_iter()
230            .map(|(s, _, _)| s)
231            .collect()
232    }
233
234    fn subjects_of(&self, predicate: &str) -> Vec<String> {
235        unique(
236            self.rete
237                .query(None, Some(predicate), None)
238                .into_iter()
239                .map(|(s, _, _)| s)
240                .collect(),
241        )
242    }
243
244    fn objects_of(&self, predicate: &str) -> Vec<String> {
245        unique(
246            self.rete
247                .query(None, Some(predicate), None)
248                .into_iter()
249                .map(|(_, _, o)| o)
250                .collect(),
251        )
252    }
253
254    fn predicates_for_subject(&self, subject: &str) -> Vec<String> {
255        unique(
256            self.rete
257                .query(Some(subject), None, None)
258                .into_iter()
259                .map(|(_, p, _)| p)
260                .collect(),
261        )
262    }
263
264    fn all_nodes(&self) -> Vec<String> {
265        let mut out = Vec::new();
266        for (s, _, o) in self.rete.query(None, None, None) {
267            out.push(s);
268            out.push(o);
269        }
270        unique(out)
271    }
272}
273
274/// Parsed SHACL shapes graph.
275#[derive(Debug, Clone)]
276pub struct ShaclShapes {
277    graph: DataGraph,
278}
279
280impl ShaclShapes {
281    pub fn parse_turtle(text: &str) -> Result<Self, ShaclError> {
282        let mut triples = Vec::new();
283        for r in oxttl::TurtleParser::new().for_reader(text.as_bytes()) {
284            let t = r.map_err(|e| ShaclError::Parse(e.to_string()))?;
285            triples.push((
286                t.subject.to_string(),
287                t.predicate.to_string(),
288                t.object.to_string(),
289            ));
290        }
291        Ok(Self {
292            graph: DataGraph::from_triples(triples),
293        })
294    }
295
296    fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
297        self.graph.objects(subject, predicate)
298    }
299
300    fn subjects(&self, predicate: &str, object: &str) -> Vec<String> {
301        unique(
302            self.graph
303                .triples
304                .iter()
305                .filter(|(_, p, o)| p == predicate && o == object)
306                .map(|(s, _, _)| s.clone())
307                .collect(),
308        )
309    }
310
311    fn has(&self, s: &str, p: &str, o: &str) -> bool {
312        self.graph.has(s, p, o)
313    }
314
315    fn list(&self, head: &str) -> Result<Vec<String>, ShaclError> {
316        if head == RDF_NIL {
317            return Ok(Vec::new());
318        }
319        let mut out = Vec::new();
320        let mut cur = head.to_string();
321        let mut seen = HashSet::new();
322        loop {
323            if cur == RDF_NIL {
324                break;
325            }
326            if !seen.insert(cur.clone()) {
327                return Err(ShaclError::MalformedList(head.to_string()));
328            }
329            let first = self.objects(&cur, RDF_FIRST);
330            let rest = self.objects(&cur, RDF_REST);
331            if first.len() != 1 || rest.len() != 1 {
332                return Err(ShaclError::MalformedList(head.to_string()));
333            }
334            out.push(first[0].clone());
335            cur = rest[0].clone();
336        }
337        Ok(out)
338    }
339
340    fn target_shapes(&self) -> Vec<String> {
341        let mut ids = Vec::new();
342        for (s, p, o) in &self.graph.triples {
343            if matches!(
344                p.as_str(),
345                sh!("targetNode")
346                    | sh!("targetClass")
347                    | sh!("targetSubjectsOf")
348                    | sh!("targetObjectsOf")
349            ) || (p == RDF_TYPE
350                && matches!(
351                    o.as_str(),
352                    sh!("NodeShape") | sh!("PropertyShape") | RDFS_CLASS | OWL_CLASS
353                ))
354            {
355                ids.push(s.clone());
356            }
357        }
358        unique(ids)
359    }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363#[non_exhaustive]
364pub enum Severity {
365    Info,
366    Warning,
367    Violation,
368    Other(String),
369}
370
371impl Severity {
372    fn from_token(token: Option<String>) -> Self {
373        match token.as_deref() {
374            Some(sh!("Info")) => Severity::Info,
375            Some(sh!("Warning")) => Severity::Warning,
376            Some(sh!("Violation")) | None => Severity::Violation,
377            Some(other) => Severity::Other(strip_iri(other).unwrap_or(other).to_string()),
378        }
379    }
380
381    pub fn iri(&self) -> String {
382        match self {
383            Severity::Info => format!("{SH}Info"),
384            Severity::Warning => format!("{SH}Warning"),
385            Severity::Violation => format!("{SH}Violation"),
386            Severity::Other(iri) => iri.clone(),
387        }
388    }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct ValidationResult {
393    pub focus_node: String,
394    pub value_node: Option<String>,
395    pub result_path: Option<String>,
396    pub source_shape: String,
397    pub source_constraint_component: String,
398    pub severity: Severity,
399    pub messages: Vec<String>,
400}
401
402#[derive(Debug, Clone, Default, PartialEq, Eq)]
403#[must_use]
404pub struct ValidationReport {
405    pub conforms: bool,
406    pub results: Vec<ValidationResult>,
407}
408
409impl ValidationReport {
410    pub fn to_json(&self) -> String {
411        use serde_json::json;
412        let results: Vec<_> = self
413            .results
414            .iter()
415            .map(|r| {
416                json!({
417                    "focusNode": term_json_string(&r.focus_node),
418                    "valueNode": r.value_node.as_deref().map(term_json_string),
419                    "resultPath": r.result_path,
420                    "sourceShape": term_json_string(&r.source_shape),
421                    "sourceConstraintComponent": r.source_constraint_component,
422                    "resultSeverity": r.severity.iri(),
423                    "resultMessage": r.messages,
424                })
425            })
426            .collect();
427        serde_json::to_string_pretty(&json!({
428            "schemaVersion": 1,
429            "conforms": self.conforms,
430            "results": results,
431        }))
432        .unwrap_or_default()
433    }
434
435    pub fn to_turtle(&self) -> String {
436        let mut out = String::new();
437        out.push_str("@prefix sh: <http://www.w3.org/ns/shacl#> .\n\n");
438        out.push_str("[] a <http://www.w3.org/ns/shacl#ValidationReport> ;\n");
439        out.push_str(&format!(
440            "   <http://www.w3.org/ns/shacl#conforms> {} ",
441            self.conforms
442        ));
443        if self.results.is_empty() {
444            out.push_str(".\n");
445            return out;
446        }
447        out.push_str(";\n");
448        for (i, r) in self.results.iter().enumerate() {
449            out.push_str("   <http://www.w3.org/ns/shacl#result> [\n");
450            out.push_str("      a <http://www.w3.org/ns/shacl#ValidationResult> ;\n");
451            out.push_str(&format!(
452                "      <http://www.w3.org/ns/shacl#focusNode> {} ;\n",
453                r.focus_node
454            ));
455            if let Some(v) = &r.value_node {
456                out.push_str(&format!("      <http://www.w3.org/ns/shacl#value> {v} ;\n"));
457            }
458            if let Some(path) = &r.result_path {
459                out.push_str(&format!(
460                    "      <http://www.w3.org/ns/shacl#resultPath> \"{}\" ;\n",
461                    escape_string(path)
462                ));
463            }
464            out.push_str(&format!(
465                "      <http://www.w3.org/ns/shacl#sourceShape> {} ;\n",
466                r.source_shape
467            ));
468            out.push_str(&format!(
469                "      <http://www.w3.org/ns/shacl#sourceConstraintComponent> <{}> ;\n",
470                r.source_constraint_component
471            ));
472            out.push_str(&format!(
473                "      <http://www.w3.org/ns/shacl#resultSeverity> <{}>",
474                r.severity.iri()
475            ));
476            for msg in &r.messages {
477                out.push_str(&format!(
478                    " ;\n      <http://www.w3.org/ns/shacl#resultMessage> \"{}\"",
479                    escape_string(msg)
480                ));
481            }
482            out.push_str("\n   ]");
483            out.push_str(if i + 1 == self.results.len() {
484                " .\n"
485            } else {
486                " ;\n"
487            });
488        }
489        out
490    }
491}
492
493#[derive(Debug, Clone)]
494enum Path {
495    Predicate(String),
496    Inverse(Box<Path>),
497    Sequence(Vec<Path>),
498    Alternative(Vec<Path>),
499    ZeroOrMore(Box<Path>),
500    OneOrMore(Box<Path>),
501    ZeroOrOne(Box<Path>),
502}
503
504impl Path {
505    fn display(&self) -> String {
506        match self {
507            Path::Predicate(p) => p.clone(),
508            Path::Inverse(p) => format!("^{}", p.display()),
509            Path::Sequence(ps) => format!(
510                "({})",
511                ps.iter().map(Path::display).collect::<Vec<_>>().join(" ")
512            ),
513            Path::Alternative(ps) => format!(
514                "({})",
515                ps.iter().map(Path::display).collect::<Vec<_>>().join("|")
516            ),
517            Path::ZeroOrMore(p) => format!("{}*", p.display()),
518            Path::OneOrMore(p) => format!("{}+", p.display()),
519            Path::ZeroOrOne(p) => format!("{}?", p.display()),
520        }
521    }
522}
523
524#[derive(Debug)]
525struct ShapeView<'a> {
526    id: &'a str,
527    path: Option<Path>,
528    severity: Severity,
529    messages: Vec<String>,
530}
531
532struct Validator<'a, G: GraphView> {
533    data: &'a G,
534    shapes: &'a ShaclShapes,
535}
536
537/// Validate `data` against `shapes`. `data` is any [`GraphView`] — an in-memory
538/// [`DataGraph`] (eager) or a [`ReteGraph`] that routes lookups as range reads
539/// (lazy / remote, fetching only the shapes' targets).
540pub fn validate_shacl<G: GraphView>(data: &G, shapes: &ShaclShapes) -> ValidationReport {
541    let validator = Validator { data, shapes };
542    let mut results = Vec::new();
543    for shape in shapes.target_shapes() {
544        let targets = validator.targets(&shape);
545        for focus in targets {
546            results.extend(validator.validate_shape(&shape, &focus, &mut Vec::new()));
547        }
548    }
549    results.sort_by(|a, b| {
550        (
551            &a.focus_node,
552            &a.result_path,
553            &a.source_constraint_component,
554            &a.value_node,
555        )
556            .cmp(&(
557                &b.focus_node,
558                &b.result_path,
559                &b.source_constraint_component,
560                &b.value_node,
561            ))
562    });
563    results.dedup();
564    ValidationReport {
565        conforms: results.is_empty(),
566        results,
567    }
568}
569
570impl<'a, G: GraphView> Validator<'a, G> {
571    fn view(&self, shape: &'a str) -> ShapeView<'a> {
572        let path = self
573            .shapes
574            .objects(shape, sh!("path"))
575            .first()
576            .and_then(|p| self.parse_path(p).ok());
577        let severity =
578            Severity::from_token(self.shapes.objects(shape, sh!("severity")).first().cloned());
579        let messages = self
580            .shapes
581            .objects(shape, sh!("message"))
582            .into_iter()
583            .filter_map(|m| literal_lexical(&m).map(|l| l.value))
584            .collect();
585        ShapeView {
586            id: shape,
587            path,
588            severity,
589            messages,
590        }
591    }
592
593    fn targets(&self, shape: &str) -> Vec<String> {
594        let mut out = Vec::new();
595        out.extend(self.shapes.objects(shape, sh!("targetNode")));
596        for class in self.shapes.objects(shape, sh!("targetClass")) {
597            out.extend(self.data.instances_of(&class));
598        }
599        for pred in self.shapes.objects(shape, sh!("targetSubjectsOf")) {
600            out.extend(self.data.subjects_of(&pred));
601        }
602        for pred in self.shapes.objects(shape, sh!("targetObjectsOf")) {
603            out.extend(self.data.objects_of(&pred));
604        }
605        if self.shapes.has(shape, RDF_TYPE, RDFS_CLASS)
606            || self.shapes.has(shape, RDF_TYPE, OWL_CLASS)
607        {
608            out.extend(self.data.instances_of(shape));
609        }
610        unique(out)
611    }
612
613    fn validate_shape(
614        &self,
615        shape: &str,
616        focus: &str,
617        stack: &mut Vec<(String, String)>,
618    ) -> Vec<ValidationResult> {
619        if stack.iter().any(|(s, f)| s == shape && f == focus) {
620            return vec![self.result(
621                &self.view(shape),
622                focus,
623                None,
624                component("RecursiveConstraintComponent"),
625                None,
626            )];
627        }
628        stack.push((shape.to_string(), focus.to_string()));
629        let view = self.view(shape);
630        if bool_param(self.shapes.objects(shape, sh!("deactivated")).first()) {
631            stack.pop();
632            return Vec::new();
633        }
634        let (values, result_path) = match &view.path {
635            Some(path) => (self.eval_path(path, focus), Some(path.display())),
636            None => (vec![focus.to_string()], None),
637        };
638        let mut out = Vec::new();
639
640        self.check_cardinality(&view, focus, &values, result_path.as_deref(), &mut out);
641        self.check_value_type(&view, focus, &values, result_path.as_deref(), &mut out);
642        self.check_value_ranges(&view, focus, &values, result_path.as_deref(), &mut out);
643        self.check_strings(&view, focus, &values, result_path.as_deref(), &mut out);
644        self.check_property_pairs(&view, focus, &values, result_path.as_deref(), &mut out);
645        self.check_has_value_and_in(&view, focus, &values, result_path.as_deref(), &mut out);
646        self.check_nested_shapes(
647            &view,
648            focus,
649            &values,
650            result_path.as_deref(),
651            stack,
652            &mut out,
653        );
654        self.check_logical(&view, focus, stack, &mut out);
655        self.check_closed(&view, focus, &mut out);
656        self.check_qualified(
657            &view,
658            focus,
659            &values,
660            result_path.as_deref(),
661            stack,
662            &mut out,
663        );
664
665        stack.pop();
666        out
667    }
668
669    fn conforms(&self, shape: &str, focus: &str, stack: &mut Vec<(String, String)>) -> bool {
670        self.validate_shape(shape, focus, stack).is_empty()
671    }
672
673    fn check_cardinality(
674        &self,
675        view: &ShapeView<'_>,
676        focus: &str,
677        values: &[String],
678        path: Option<&str>,
679        out: &mut Vec<ValidationResult>,
680    ) {
681        for min in self.shapes.objects(view.id, sh!("minCount")) {
682            if let Some(n) = int_literal(&min) {
683                if values.len() < n as usize {
684                    out.push(self.result(
685                        view,
686                        focus,
687                        None,
688                        component("MinCountConstraintComponent"),
689                        path,
690                    ));
691                }
692            }
693        }
694        for max in self.shapes.objects(view.id, sh!("maxCount")) {
695            if let Some(n) = int_literal(&max) {
696                if values.len() > n as usize {
697                    out.push(self.result(
698                        view,
699                        focus,
700                        None,
701                        component("MaxCountConstraintComponent"),
702                        path,
703                    ));
704                }
705            }
706        }
707    }
708
709    fn check_value_type(
710        &self,
711        view: &ShapeView<'_>,
712        focus: &str,
713        values: &[String],
714        path: Option<&str>,
715        out: &mut Vec<ValidationResult>,
716    ) {
717        for kind in self.shapes.objects(view.id, sh!("nodeKind")) {
718            for v in values {
719                if !node_kind(v, &kind) {
720                    out.push(self.result(
721                        view,
722                        focus,
723                        Some(v.clone()),
724                        component("NodeKindConstraintComponent"),
725                        path,
726                    ));
727                }
728            }
729        }
730        for class in self.shapes.objects(view.id, sh!("class")) {
731            for v in values {
732                if !self.data.is_instance_of(v, &class) {
733                    out.push(self.result(
734                        view,
735                        focus,
736                        Some(v.clone()),
737                        component("ClassConstraintComponent"),
738                        path,
739                    ));
740                }
741            }
742        }
743        for datatype in self.shapes.objects(view.id, sh!("datatype")) {
744            for v in values {
745                if !datatype_matches(v, &datatype) {
746                    out.push(self.result(
747                        view,
748                        focus,
749                        Some(v.clone()),
750                        component("DatatypeConstraintComponent"),
751                        path,
752                    ));
753                }
754            }
755        }
756    }
757
758    fn check_value_ranges(
759        &self,
760        view: &ShapeView<'_>,
761        focus: &str,
762        values: &[String],
763        path: Option<&str>,
764        out: &mut Vec<ValidationResult>,
765    ) {
766        let checks = [
767            (sh!("minExclusive"), "MinExclusiveConstraintComponent", 0_u8),
768            (sh!("minInclusive"), "MinInclusiveConstraintComponent", 1),
769            (sh!("maxExclusive"), "MaxExclusiveConstraintComponent", 2),
770            (sh!("maxInclusive"), "MaxInclusiveConstraintComponent", 3),
771        ];
772        for (pred, comp, mode) in checks {
773            for bound in self.shapes.objects(view.id, pred) {
774                for v in values {
775                    let ok = compare_terms(v, &bound).is_some_and(|ord| match mode {
776                        0 => ord.is_gt(),
777                        1 => !ord.is_lt(),
778                        2 => ord.is_lt(),
779                        _ => !ord.is_gt(),
780                    });
781                    if !ok {
782                        out.push(self.result(view, focus, Some(v.clone()), component(comp), path));
783                    }
784                }
785            }
786        }
787    }
788
789    fn check_strings(
790        &self,
791        view: &ShapeView<'_>,
792        focus: &str,
793        values: &[String],
794        path: Option<&str>,
795        out: &mut Vec<ValidationResult>,
796    ) {
797        for min in self.shapes.objects(view.id, sh!("minLength")) {
798            if let Some(n) = int_literal(&min) {
799                for v in values {
800                    if string_value(v).chars().count() < n as usize {
801                        out.push(self.result(
802                            view,
803                            focus,
804                            Some(v.clone()),
805                            component("MinLengthConstraintComponent"),
806                            path,
807                        ));
808                    }
809                }
810            }
811        }
812        for max in self.shapes.objects(view.id, sh!("maxLength")) {
813            if let Some(n) = int_literal(&max) {
814                for v in values {
815                    if string_value(v).chars().count() > n as usize {
816                        out.push(self.result(
817                            view,
818                            focus,
819                            Some(v.clone()),
820                            component("MaxLengthConstraintComponent"),
821                            path,
822                        ));
823                    }
824                }
825            }
826        }
827        for pattern in self.shapes.objects(view.id, sh!("pattern")) {
828            let flags = self
829                .shapes
830                .objects(view.id, sh!("flags"))
831                .first()
832                .and_then(|f| literal_lexical(f).map(|l| l.value))
833                .unwrap_or_default();
834            let pat = literal_lexical(&pattern)
835                .map(|l| l.value)
836                .unwrap_or(pattern);
837            let inline: String = ['i', 'm', 's', 'x']
838                .iter()
839                .filter(|c| flags.contains(**c))
840                .collect();
841            let full = if inline.is_empty() {
842                pat
843            } else {
844                format!("(?{inline}){pat}")
845            };
846            let re = regex_lite::Regex::new(&full);
847            for v in values {
848                if re.as_ref().map_or(true, |r| !r.is_match(&string_value(v))) {
849                    out.push(self.result(
850                        view,
851                        focus,
852                        Some(v.clone()),
853                        component("PatternConstraintComponent"),
854                        path,
855                    ));
856                }
857            }
858        }
859        for head in self.shapes.objects(view.id, sh!("languageIn")) {
860            let allowed = self
861                .shapes
862                .list(&head)
863                .unwrap_or_default()
864                .into_iter()
865                .filter_map(|t| literal_lexical(&t).map(|l| l.value.to_ascii_lowercase()))
866                .collect::<BTreeSet<_>>();
867            for v in values {
868                let lang = literal_lexical(v)
869                    .and_then(|l| l.lang)
870                    .map(|l| l.to_ascii_lowercase());
871                if lang.is_none_or(|l| !allowed.contains("*") && !allowed.contains(&l)) {
872                    out.push(self.result(
873                        view,
874                        focus,
875                        Some(v.clone()),
876                        component("LanguageInConstraintComponent"),
877                        path,
878                    ));
879                }
880            }
881        }
882        if bool_param(self.shapes.objects(view.id, sh!("uniqueLang")).first()) {
883            let mut seen = BTreeSet::new();
884            let mut duplicate = false;
885            for v in values {
886                if let Some(lang) = literal_lexical(v).and_then(|l| l.lang) {
887                    if !seen.insert(lang.to_ascii_lowercase()) {
888                        duplicate = true;
889                    }
890                }
891            }
892            if duplicate {
893                out.push(self.result(
894                    view,
895                    focus,
896                    None,
897                    component("UniqueLangConstraintComponent"),
898                    path,
899                ));
900            }
901        }
902    }
903
904    fn check_property_pairs(
905        &self,
906        view: &ShapeView<'_>,
907        focus: &str,
908        values: &[String],
909        path: Option<&str>,
910        out: &mut Vec<ValidationResult>,
911    ) {
912        for other in self.shapes.objects(view.id, sh!("equals")) {
913            let other_values = self.eval_path(&Path::Predicate(other), focus);
914            if set(values) != set(&other_values) {
915                out.push(self.result(
916                    view,
917                    focus,
918                    None,
919                    component("EqualsConstraintComponent"),
920                    path,
921                ));
922            }
923        }
924        for other in self.shapes.objects(view.id, sh!("disjoint")) {
925            let other_values = self.eval_path(&Path::Predicate(other), focus);
926            if values.iter().any(|v| other_values.contains(v)) {
927                out.push(self.result(
928                    view,
929                    focus,
930                    None,
931                    component("DisjointConstraintComponent"),
932                    path,
933                ));
934            }
935        }
936        for other in self.shapes.objects(view.id, sh!("lessThan")) {
937            let other_values = self.eval_path(&Path::Predicate(other), focus);
938            for v in values {
939                if other_values
940                    .iter()
941                    .any(|o| compare_terms(v, o).is_none_or(|ord| !ord.is_lt()))
942                {
943                    out.push(self.result(
944                        view,
945                        focus,
946                        Some(v.clone()),
947                        component("LessThanConstraintComponent"),
948                        path,
949                    ));
950                }
951            }
952        }
953        for other in self.shapes.objects(view.id, sh!("lessThanOrEquals")) {
954            let other_values = self.eval_path(&Path::Predicate(other), focus);
955            for v in values {
956                if other_values
957                    .iter()
958                    .any(|o| compare_terms(v, o).is_none_or(|ord| ord.is_gt()))
959                {
960                    out.push(self.result(
961                        view,
962                        focus,
963                        Some(v.clone()),
964                        component("LessThanOrEqualsConstraintComponent"),
965                        path,
966                    ));
967                }
968            }
969        }
970    }
971
972    fn check_has_value_and_in(
973        &self,
974        view: &ShapeView<'_>,
975        focus: &str,
976        values: &[String],
977        path: Option<&str>,
978        out: &mut Vec<ValidationResult>,
979    ) {
980        for required in self.shapes.objects(view.id, sh!("hasValue")) {
981            if !values.contains(&required) {
982                out.push(self.result(
983                    view,
984                    focus,
985                    Some(required),
986                    component("HasValueConstraintComponent"),
987                    path,
988                ));
989            }
990        }
991        for head in self.shapes.objects(view.id, sh!("in")) {
992            let allowed = self.shapes.list(&head).unwrap_or_default();
993            for v in values {
994                if !allowed.contains(v) {
995                    out.push(self.result(
996                        view,
997                        focus,
998                        Some(v.clone()),
999                        component("InConstraintComponent"),
1000                        path,
1001                    ));
1002                }
1003            }
1004        }
1005    }
1006
1007    fn check_nested_shapes(
1008        &self,
1009        view: &ShapeView<'_>,
1010        focus: &str,
1011        values: &[String],
1012        path: Option<&str>,
1013        stack: &mut Vec<(String, String)>,
1014        out: &mut Vec<ValidationResult>,
1015    ) {
1016        for node_shape in self.shapes.objects(view.id, sh!("node")) {
1017            for v in values {
1018                if !self.conforms(&node_shape, v, stack) {
1019                    out.push(self.result(
1020                        view,
1021                        focus,
1022                        Some(v.clone()),
1023                        component("NodeConstraintComponent"),
1024                        path,
1025                    ));
1026                }
1027            }
1028        }
1029        for property_shape in self.shapes.objects(view.id, sh!("property")) {
1030            out.extend(self.validate_shape(&property_shape, focus, stack));
1031        }
1032    }
1033
1034    fn check_logical(
1035        &self,
1036        view: &ShapeView<'_>,
1037        focus: &str,
1038        stack: &mut Vec<(String, String)>,
1039        out: &mut Vec<ValidationResult>,
1040    ) {
1041        for s in self.shapes.objects(view.id, sh!("not")) {
1042            if self.conforms(&s, focus, stack) {
1043                out.push(self.result(
1044                    view,
1045                    focus,
1046                    Some(focus.to_string()),
1047                    component("NotConstraintComponent"),
1048                    None,
1049                ));
1050            }
1051        }
1052        for head in self.shapes.objects(view.id, sh!("and")) {
1053            let shapes = self.shapes.list(&head).unwrap_or_default();
1054            if shapes.iter().any(|s| !self.conforms(s, focus, stack)) {
1055                out.push(self.result(
1056                    view,
1057                    focus,
1058                    Some(focus.to_string()),
1059                    component("AndConstraintComponent"),
1060                    None,
1061                ));
1062            }
1063        }
1064        for head in self.shapes.objects(view.id, sh!("or")) {
1065            let shapes = self.shapes.list(&head).unwrap_or_default();
1066            if !shapes.iter().any(|s| self.conforms(s, focus, stack)) {
1067                out.push(self.result(
1068                    view,
1069                    focus,
1070                    Some(focus.to_string()),
1071                    component("OrConstraintComponent"),
1072                    None,
1073                ));
1074            }
1075        }
1076        for head in self.shapes.objects(view.id, sh!("xone")) {
1077            let shapes = self.shapes.list(&head).unwrap_or_default();
1078            let n = shapes
1079                .iter()
1080                .filter(|s| self.conforms(s, focus, stack))
1081                .count();
1082            if n != 1 {
1083                out.push(self.result(
1084                    view,
1085                    focus,
1086                    Some(focus.to_string()),
1087                    component("XoneConstraintComponent"),
1088                    None,
1089                ));
1090            }
1091        }
1092    }
1093
1094    fn check_closed(&self, view: &ShapeView<'_>, focus: &str, out: &mut Vec<ValidationResult>) {
1095        if !bool_param(self.shapes.objects(view.id, sh!("closed")).first()) {
1096            return;
1097        }
1098        let mut allowed = BTreeSet::new();
1099        for prop_shape in self.shapes.objects(view.id, sh!("property")) {
1100            if let Some(path_node) = self.shapes.objects(&prop_shape, sh!("path")).first() {
1101                if is_iri(path_node) {
1102                    allowed.insert(path_node.clone());
1103                }
1104            }
1105        }
1106        for head in self.shapes.objects(view.id, sh!("ignoredProperties")) {
1107            for pred in self.shapes.list(&head).unwrap_or_default() {
1108                allowed.insert(pred);
1109            }
1110        }
1111        for pred in self.data.predicates_for_subject(focus) {
1112            if !allowed.contains(&pred) {
1113                out.push(self.result(
1114                    view,
1115                    focus,
1116                    Some(pred),
1117                    component("ClosedConstraintComponent"),
1118                    None,
1119                ));
1120            }
1121        }
1122    }
1123
1124    fn check_qualified(
1125        &self,
1126        view: &ShapeView<'_>,
1127        focus: &str,
1128        values: &[String],
1129        path: Option<&str>,
1130        stack: &mut Vec<(String, String)>,
1131        out: &mut Vec<ValidationResult>,
1132    ) {
1133        let Some(qshape) = self
1134            .shapes
1135            .objects(view.id, sh!("qualifiedValueShape"))
1136            .first()
1137            .cloned()
1138        else {
1139            return;
1140        };
1141        let sibling_shapes = self.qualified_sibling_shapes(view.id, &qshape);
1142        let mut count = 0;
1143        for value in values {
1144            if !self.conforms(&qshape, value, stack) {
1145                continue;
1146            }
1147            if sibling_shapes
1148                .iter()
1149                .any(|sibling| self.conforms(sibling, value, stack))
1150            {
1151                continue;
1152            }
1153            count += 1;
1154        }
1155        for min in self.shapes.objects(view.id, sh!("qualifiedMinCount")) {
1156            if let Some(n) = int_literal(&min) {
1157                if count < n as usize {
1158                    out.push(self.result(
1159                        view,
1160                        focus,
1161                        None,
1162                        component("QualifiedMinCountConstraintComponent"),
1163                        path,
1164                    ));
1165                }
1166            }
1167        }
1168        for max in self.shapes.objects(view.id, sh!("qualifiedMaxCount")) {
1169            if let Some(n) = int_literal(&max) {
1170                if count > n as usize {
1171                    out.push(self.result(
1172                        view,
1173                        focus,
1174                        None,
1175                        component("QualifiedMaxCountConstraintComponent"),
1176                        path,
1177                    ));
1178                }
1179            }
1180        }
1181    }
1182
1183    fn qualified_sibling_shapes(&self, property_shape: &str, qshape: &str) -> Vec<String> {
1184        if !bool_param(
1185            self.shapes
1186                .objects(property_shape, sh!("qualifiedValueShapesDisjoint"))
1187                .first(),
1188        ) {
1189            return Vec::new();
1190        }
1191
1192        let mut siblings = Vec::new();
1193        for parent_shape in self.shapes.subjects(sh!("property"), property_shape) {
1194            for sibling_property_shape in self.shapes.objects(&parent_shape, sh!("property")) {
1195                siblings.extend(
1196                    self.shapes
1197                        .objects(&sibling_property_shape, sh!("qualifiedValueShape"))
1198                        .into_iter()
1199                        .filter(|sibling| sibling != qshape),
1200                );
1201            }
1202        }
1203        unique(siblings)
1204    }
1205
1206    fn result(
1207        &self,
1208        view: &ShapeView<'_>,
1209        focus: &str,
1210        value: Option<String>,
1211        component: String,
1212        path: Option<&str>,
1213    ) -> ValidationResult {
1214        ValidationResult {
1215            focus_node: focus.to_string(),
1216            value_node: value,
1217            result_path: path.map(str::to_string),
1218            source_shape: view.id.to_string(),
1219            source_constraint_component: component,
1220            severity: view.severity.clone(),
1221            messages: view.messages.clone(),
1222        }
1223    }
1224
1225    fn parse_path(&self, node: &str) -> Result<Path, ShaclError> {
1226        if is_iri(node) {
1227            return Ok(Path::Predicate(node.to_string()));
1228        }
1229        if let Some(p) = self.shapes.objects(node, sh!("inversePath")).first() {
1230            return Ok(Path::Inverse(Box::new(self.parse_path(p)?)));
1231        }
1232        if let Some(head) = self.shapes.objects(node, sh!("alternativePath")).first() {
1233            let paths = self
1234                .shapes
1235                .list(head)?
1236                .iter()
1237                .map(|n| self.parse_path(n))
1238                .collect::<Result<Vec<_>, _>>()?;
1239            return Ok(Path::Alternative(paths));
1240        }
1241        if let Some(p) = self.shapes.objects(node, sh!("zeroOrMorePath")).first() {
1242            return Ok(Path::ZeroOrMore(Box::new(self.parse_path(p)?)));
1243        }
1244        if let Some(p) = self.shapes.objects(node, sh!("oneOrMorePath")).first() {
1245            return Ok(Path::OneOrMore(Box::new(self.parse_path(p)?)));
1246        }
1247        if let Some(p) = self.shapes.objects(node, sh!("zeroOrOnePath")).first() {
1248            return Ok(Path::ZeroOrOne(Box::new(self.parse_path(p)?)));
1249        }
1250        let paths = self
1251            .shapes
1252            .list(node)?
1253            .iter()
1254            .map(|n| self.parse_path(n))
1255            .collect::<Result<Vec<_>, _>>()?;
1256        Ok(Path::Sequence(paths))
1257    }
1258
1259    fn eval_path(&self, path: &Path, start: &str) -> Vec<String> {
1260        match path {
1261            Path::Predicate(p) => unique(self.data.objects(start, p)),
1262            // The common inverse `^p` routes to `(?, p, start)` — one targeted
1263            // query — instead of scanning every node. A general inverse path
1264            // (`^(p1/p2)`) still enumerates all nodes (inherently non-targeted).
1265            Path::Inverse(inner) => match inner.as_ref() {
1266                Path::Predicate(p) => unique(self.data.subjects_with(p, start)),
1267                _ => unique(
1268                    self.data
1269                        .all_nodes()
1270                        .into_iter()
1271                        .filter(|n| self.eval_path(inner, n).contains(&start.to_string()))
1272                        .collect(),
1273                ),
1274            },
1275            Path::Sequence(paths) => {
1276                let mut frontier = vec![start.to_string()];
1277                for p in paths {
1278                    let mut next = Vec::new();
1279                    for n in &frontier {
1280                        next.extend(self.eval_path(p, n));
1281                    }
1282                    frontier = unique(next);
1283                }
1284                frontier
1285            }
1286            Path::Alternative(paths) => unique(
1287                paths
1288                    .iter()
1289                    .flat_map(|p| self.eval_path(p, start))
1290                    .collect::<Vec<_>>(),
1291            ),
1292            Path::ZeroOrOne(p) => {
1293                let mut out = vec![start.to_string()];
1294                out.extend(self.eval_path(p, start));
1295                unique(out)
1296            }
1297            Path::ZeroOrMore(p) => {
1298                let mut out = vec![start.to_string()];
1299                out.extend(self.transitive_path(p, start));
1300                unique(out)
1301            }
1302            Path::OneOrMore(p) => self.transitive_path(p, start),
1303        }
1304    }
1305
1306    fn transitive_path(&self, path: &Path, start: &str) -> Vec<String> {
1307        let mut out = Vec::new();
1308        let mut seen = HashSet::new();
1309        let mut stack = self.eval_path(path, start);
1310        while let Some(n) = stack.pop() {
1311            if !seen.insert(n.clone()) {
1312                continue;
1313            }
1314            out.push(n.clone());
1315            stack.extend(self.eval_path(path, &n));
1316        }
1317        unique(out)
1318    }
1319}
1320
1321fn component(local: &str) -> String {
1322    format!("{SH}{local}")
1323}
1324
1325fn unique(mut v: Vec<String>) -> Vec<String> {
1326    v.sort();
1327    v.dedup();
1328    v
1329}
1330
1331fn set(values: &[String]) -> BTreeSet<String> {
1332    values.iter().cloned().collect()
1333}
1334
1335use crate::terms::{iri_content as strip_iri, is_iri};
1336
1337fn bool_param(v: Option<&String>) -> bool {
1338    v.is_some_and(|t| {
1339        literal_lexical(t)
1340            .map(|l| l.value == "true" || l.value == "1")
1341            .unwrap_or(false)
1342    })
1343}
1344
1345fn int_literal(t: &str) -> Option<i64> {
1346    literal_lexical(t)?.value.parse().ok()
1347}
1348
1349#[derive(Debug, Clone)]
1350struct Lit {
1351    value: String,
1352    datatype: Option<String>,
1353    lang: Option<String>,
1354}
1355
1356fn literal_lexical(token: &str) -> Option<Lit> {
1357    if !token.starts_with('"') {
1358        return None;
1359    }
1360    let bytes = token.as_bytes();
1361    let mut i = 1;
1362    while i < bytes.len() {
1363        match bytes[i] {
1364            b'\\' => i += 2,
1365            b'"' => break,
1366            _ => i += 1,
1367        }
1368    }
1369    let value = unescape_nt(&token[1..i.min(token.len())]);
1370    let rest = token.get(i + 1..).unwrap_or("");
1371    let datatype = rest
1372        .strip_prefix("^^<")
1373        .and_then(|s| s.strip_suffix('>'))
1374        .map(|s| format!("<{s}>"));
1375    let lang = rest.strip_prefix('@').map(str::to_string);
1376    Some(Lit {
1377        value,
1378        datatype,
1379        lang,
1380    })
1381}
1382
1383fn literal_datatype(token: &str) -> Option<String> {
1384    let lit = literal_lexical(token)?;
1385    if lit.lang.is_some() {
1386        Some(RDF_LANG_STRING.to_string())
1387    } else {
1388        Some(lit.datatype.unwrap_or_else(|| XSD_STRING.to_string()))
1389    }
1390}
1391
1392fn datatype_matches(value: &str, datatype: &str) -> bool {
1393    literal_datatype(value).is_some_and(|dt| dt == datatype)
1394}
1395
1396fn node_kind(value: &str, kind: &str) -> bool {
1397    match kind {
1398        sh!("IRI") => is_iri(value),
1399        sh!("BlankNode") => value.starts_with("_:"),
1400        sh!("Literal") => value.starts_with('"'),
1401        sh!("BlankNodeOrIRI") => value.starts_with("_:") || is_iri(value),
1402        sh!("BlankNodeOrLiteral") => value.starts_with("_:") || value.starts_with('"'),
1403        sh!("IRIOrLiteral") => is_iri(value) || value.starts_with('"'),
1404        _ => true,
1405    }
1406}
1407
1408fn string_value(value: &str) -> String {
1409    if let Some(l) = literal_lexical(value) {
1410        l.value
1411    } else if let Some(iri) = strip_iri(value) {
1412        iri.to_string()
1413    } else {
1414        value.to_string()
1415    }
1416}
1417
1418fn compare_terms(a: &str, b: &str) -> Option<std::cmp::Ordering> {
1419    let av = literal_lexical(a)
1420        .map(|l| l.value)
1421        .unwrap_or_else(|| string_value(a));
1422    let bv = literal_lexical(b)
1423        .map(|l| l.value)
1424        .unwrap_or_else(|| string_value(b));
1425    match (av.parse::<f64>(), bv.parse::<f64>()) {
1426        (Ok(x), Ok(y)) => x.partial_cmp(&y),
1427        _ => Some(av.cmp(&bv)),
1428    }
1429}
1430
1431fn term_json_string(token: &str) -> String {
1432    strip_iri(token).unwrap_or(token).to_string()
1433}
1434
1435fn escape_string(s: &str) -> String {
1436    s.replace('\\', "\\\\").replace('"', "\\\"")
1437}
1438
1439fn unescape_nt(s: &str) -> String {
1440    crate::terms::unescape_literal(s)
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445    use super::*;
1446
1447    fn graph(triples: &[(&str, &str, &str)]) -> DataGraph {
1448        DataGraph::from_triples(
1449            triples
1450                .iter()
1451                .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
1452                .collect(),
1453        )
1454    }
1455
1456    #[test]
1457    fn graph_view_covers_subclasses_instances_and_all_lookup_shapes() {
1458        let data = graph(&[
1459            ("<alice>", RDF_TYPE, "<Child>"),
1460            ("<Child>", RDFS_SUBCLASS_OF, "<Parent>"),
1461            ("<Parent>", RDFS_SUBCLASS_OF, "<Ancestor>"),
1462            ("<Ancestor>", RDFS_SUBCLASS_OF, "<Child>"),
1463            ("<alice>", "<p>", "<bob>"),
1464            ("<alice>", "<q>", "\"value\""),
1465            ("<bob>", "<p>", "<carol>"),
1466        ]);
1467        assert!(data.has("<alice>", "<p>", "<bob>"));
1468        assert!(!data.has("<bob>", "<q>", "<alice>"));
1469        assert_eq!(data.objects("<alice>", "<p>"), ["<bob>"]);
1470        assert_eq!(data.subjects_with("<p>", "<bob>"), ["<alice>"]);
1471        assert_eq!(data.subjects_of("<p>"), ["<alice>", "<bob>"]);
1472        assert_eq!(data.objects_of("<p>"), ["<bob>", "<carol>"]);
1473        assert_eq!(data.predicates_for_subject("<bob>"), ["<p>"]);
1474        assert!(data.all_nodes().contains(&"<alice>".to_string()));
1475        assert!(data.is_subclass_of("<Child>", "<Child>"));
1476        assert!(data.is_subclass_of("<Child>", "<Ancestor>"));
1477        assert!(!data.is_subclass_of("<Unrelated>", "<Ancestor>"));
1478        assert!(data.subclasses_of("<Parent>").contains("<Child>"));
1479        assert_eq!(data.instances_of("<Ancestor>"), ["<alice>"]);
1480        assert!(data.is_instance_of("<alice>", "<Parent>"));
1481        assert!(!data.is_instance_of("<bob>", "<Parent>"));
1482    }
1483
1484    #[test]
1485    fn shapes_lists_targets_severity_and_parse_errors_are_explicit() {
1486        assert!(matches!(
1487            ShaclShapes::parse_turtle("@prefix sh: <http://www.w3.org/ns/shacl#> . ["),
1488            Err(ShaclError::Parse(_))
1489        ));
1490        let shapes = ShaclShapes {
1491            graph: graph(&[
1492                ("<shape-node>", sh!("targetNode"), "<alice>"),
1493                ("<shape-class>", sh!("targetClass"), "<Person>"),
1494                ("<shape-subjects>", sh!("targetSubjectsOf"), "<p>"),
1495                ("<shape-objects>", sh!("targetObjectsOf"), "<q>"),
1496                ("<shape-type>", RDF_TYPE, sh!("NodeShape")),
1497                ("<shape-property>", RDF_TYPE, sh!("PropertyShape")),
1498                ("<shape-rdfs>", RDF_TYPE, RDFS_CLASS),
1499                ("<shape-owl>", RDF_TYPE, OWL_CLASS),
1500                ("_:one", RDF_FIRST, "\"a\""),
1501                ("_:one", RDF_REST, "_:two"),
1502                ("_:two", RDF_FIRST, "\"b\""),
1503                ("_:two", RDF_REST, RDF_NIL),
1504            ]),
1505        };
1506        assert_eq!(
1507            shapes.objects("<shape-node>", sh!("targetNode")),
1508            ["<alice>"]
1509        );
1510        assert_eq!(
1511            shapes.subjects(sh!("targetNode"), "<alice>"),
1512            ["<shape-node>"]
1513        );
1514        assert!(shapes.has("<shape-type>", RDF_TYPE, sh!("NodeShape")));
1515        assert_eq!(shapes.list(RDF_NIL).unwrap(), Vec::<String>::new());
1516        assert_eq!(shapes.list("_:one").unwrap(), ["\"a\"", "\"b\""]);
1517        assert_eq!(shapes.target_shapes().len(), 8);
1518
1519        let missing = ShaclShapes {
1520            graph: graph(&[("_:bad", RDF_FIRST, "\"a\"")]),
1521        };
1522        assert!(matches!(
1523            missing.list("_:bad"),
1524            Err(ShaclError::MalformedList(_))
1525        ));
1526        let cyclic = ShaclShapes {
1527            graph: graph(&[
1528                ("_:cycle", RDF_FIRST, "\"a\""),
1529                ("_:cycle", RDF_REST, "_:cycle"),
1530            ]),
1531        };
1532        assert!(matches!(
1533            cyclic.list("_:cycle"),
1534            Err(ShaclError::MalformedList(_))
1535        ));
1536
1537        for (token, expected) in [
1538            (Some(sh!("Info").to_string()), Severity::Info),
1539            (Some(sh!("Warning").to_string()), Severity::Warning),
1540            (Some(sh!("Violation").to_string()), Severity::Violation),
1541            (None, Severity::Violation),
1542            (
1543                Some("<http://ex/custom>".to_string()),
1544                Severity::Other("http://ex/custom".into()),
1545            ),
1546        ] {
1547            assert_eq!(Severity::from_token(token), expected);
1548        }
1549        assert_eq!(Severity::Info.iri(), format!("{SH}Info"));
1550        assert_eq!(
1551            Severity::Other("http://ex/custom".into()).iri(),
1552            "http://ex/custom"
1553        );
1554    }
1555
1556    #[test]
1557    fn reports_serialize_empty_and_detailed_results() {
1558        let empty = ValidationReport {
1559            conforms: true,
1560            results: vec![],
1561        };
1562        assert!(empty.to_json().contains("\"schemaVersion\": 1"));
1563        assert!(empty.to_turtle().contains("conforms> true ."));
1564
1565        let report = ValidationReport {
1566            conforms: false,
1567            results: vec![ValidationResult {
1568                focus_node: "<http://ex/alice>".into(),
1569                value_node: Some("\"bad\"".into()),
1570                result_path: Some("<http://ex/p>\"quoted".into()),
1571                source_shape: "_:shape".into(),
1572                source_constraint_component: component("PatternConstraintComponent"),
1573                severity: Severity::Warning,
1574                messages: vec!["line \\\"quoted\\\"".into(), "second".into()],
1575            }],
1576        };
1577        let json = report.to_json();
1578        assert!(json.contains("http://ex/alice"));
1579        assert!(json.contains("PatternConstraintComponent"));
1580        let turtle = report.to_turtle();
1581        assert!(turtle.contains("ValidationResult"));
1582        assert!(turtle.contains("resultPath"));
1583        assert!(turtle.contains("resultMessage"));
1584        assert!(turtle.contains("Warning"));
1585    }
1586
1587    #[test]
1588    fn path_display_parse_and_evaluation_cover_every_path_form() {
1589        let data = graph(&[
1590            ("<A>", "<p>", "<B>"),
1591            ("<B>", "<p>", "<C>"),
1592            ("<C>", "<p>", "<A>"),
1593            ("<A>", "<q>", "<C>"),
1594        ]);
1595        let shapes = ShaclShapes::parse_turtle(
1596            r#"
1597            @prefix sh: <http://www.w3.org/ns/shacl#> .
1598            @prefix ex: <http://ex/> .
1599            ex:inverse sh:path [ sh:inversePath <http://data/p> ] .
1600            ex:alternative sh:path [ sh:alternativePath ( <http://data/p> <http://data/q> ) ] .
1601            ex:zeroMore sh:path [ sh:zeroOrMorePath <http://data/p> ] .
1602            ex:oneMore sh:path [ sh:oneOrMorePath <http://data/p> ] .
1603            ex:zeroOne sh:path [ sh:zeroOrOnePath <http://data/p> ] .
1604            ex:sequence sh:path ( <http://data/p> <http://data/q> ) .
1605            "#,
1606        )
1607        .unwrap();
1608        let validator = Validator {
1609            data: &data,
1610            shapes: &shapes,
1611        };
1612        for id in [
1613            "inverse",
1614            "alternative",
1615            "zeroMore",
1616            "oneMore",
1617            "zeroOne",
1618            "sequence",
1619        ] {
1620            let shape = format!("<http://ex/{id}>");
1621            let node = shapes.objects(&shape, sh!("path")).remove(0);
1622            assert!(!validator.parse_path(&node).unwrap().display().is_empty());
1623        }
1624
1625        let p = Path::Predicate("<p>".into());
1626        let q = Path::Predicate("<q>".into());
1627        assert_eq!(p.display(), "<p>");
1628        assert_eq!(Path::Inverse(Box::new(p.clone())).display(), "^<p>");
1629        assert_eq!(
1630            Path::Sequence(vec![p.clone(), q.clone()]).display(),
1631            "(<p> <q>)"
1632        );
1633        assert_eq!(
1634            Path::Alternative(vec![p.clone(), q.clone()]).display(),
1635            "(<p>|<q>)"
1636        );
1637        assert_eq!(Path::ZeroOrMore(Box::new(p.clone())).display(), "<p>*");
1638        assert_eq!(Path::OneOrMore(Box::new(p.clone())).display(), "<p>+");
1639        assert_eq!(Path::ZeroOrOne(Box::new(p.clone())).display(), "<p>?");
1640
1641        assert_eq!(validator.eval_path(&p, "<A>"), ["<B>"]);
1642        assert_eq!(
1643            validator.eval_path(&Path::Inverse(Box::new(p.clone())), "<B>"),
1644            ["<A>"]
1645        );
1646        assert!(validator
1647            .eval_path(
1648                &Path::Inverse(Box::new(Path::Sequence(vec![p.clone(), p.clone()]))),
1649                "<C>"
1650            )
1651            .contains(&"<A>".to_string()));
1652        assert_eq!(
1653            validator.eval_path(&Path::Sequence(vec![p.clone(), p.clone()]), "<A>"),
1654            ["<C>"]
1655        );
1656        assert_eq!(
1657            validator.eval_path(&Path::Alternative(vec![p.clone(), q]), "<A>"),
1658            ["<B>", "<C>"]
1659        );
1660        assert!(validator
1661            .eval_path(&Path::ZeroOrOne(Box::new(p.clone())), "<A>")
1662            .contains(&"<A>".into()));
1663        assert!(validator
1664            .eval_path(&Path::ZeroOrMore(Box::new(p.clone())), "<A>")
1665            .contains(&"<C>".into()));
1666        assert!(validator
1667            .eval_path(&Path::OneOrMore(Box::new(p)), "<A>")
1668            .contains(&"<B>".into()));
1669    }
1670
1671    #[test]
1672    fn term_helpers_cover_literals_node_kinds_ordering_and_escaping() {
1673        assert!(bool_param(Some(
1674            &"\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>".into()
1675        )));
1676        assert!(bool_param(Some(&"\"1\"".into())));
1677        assert!(!bool_param(Some(&"<iri>".into())));
1678        assert_eq!(int_literal("\"-12\""), Some(-12));
1679        assert_eq!(int_literal("\"nope\""), None);
1680        assert!(literal_lexical("<iri>").is_none());
1681        let escaped = literal_lexical("\"a\\\"b\\n\"@EN").unwrap();
1682        assert_eq!(escaped.value, "a\"b\n");
1683        assert_eq!(escaped.lang.as_deref(), Some("EN"));
1684        assert_eq!(
1685            literal_datatype("\"x\"@en").as_deref(),
1686            Some(RDF_LANG_STRING)
1687        );
1688        assert_eq!(literal_datatype("\"x\"").as_deref(), Some(XSD_STRING));
1689        assert!(datatype_matches("\"x\"", XSD_STRING));
1690        assert!(!datatype_matches("<iri>", XSD_STRING));
1691
1692        for (kind, value, expected) in [
1693            (sh!("IRI"), "<iri>", true),
1694            (sh!("BlankNode"), "_:b", true),
1695            (sh!("Literal"), "\"x\"", true),
1696            (sh!("BlankNodeOrIRI"), "<iri>", true),
1697            (sh!("BlankNodeOrLiteral"), "\"x\"", true),
1698            (sh!("IRIOrLiteral"), "\"x\"", true),
1699            ("<unknown-kind>", "anything", true),
1700            (sh!("IRI"), "\"x\"", false),
1701        ] {
1702            assert_eq!(node_kind(value, kind), expected);
1703        }
1704        assert_eq!(string_value("\"hello\"@en"), "hello");
1705        assert_eq!(string_value("<http://ex/a>"), "http://ex/a");
1706        assert_eq!(string_value("_:b"), "_:b");
1707        assert_eq!(
1708            compare_terms("\"2\"", "\"10\""),
1709            Some(std::cmp::Ordering::Less)
1710        );
1711        assert_eq!(
1712            compare_terms("\"z\"", "\"a\""),
1713            Some(std::cmp::Ordering::Greater)
1714        );
1715        assert_eq!(term_json_string("<http://ex/a>"), "http://ex/a");
1716        assert_eq!(term_json_string("_:b"), "_:b");
1717        assert_eq!(escape_string("a\\\"b"), "a\\\\\\\"b");
1718        assert_eq!(unescape_nt("a\\tb"), "a\tb");
1719        assert_eq!(set(&["b".into(), "a".into(), "b".into()]).len(), 2);
1720        assert_eq!(unique(vec!["b".into(), "a".into(), "b".into()]), ["a", "b"]);
1721    }
1722}