1use super::shacl_types::{
27 CompiledShape, NodeKindType, PropertyPath, ShaclConstraint, ShaclSeverity, ValidationReport,
28 ValidationResult,
29};
30use crate::frame_layout::{
31 object_tag, unpack_float_object, INLINE_TAG_BOOLEAN, INLINE_TAG_DECIMAL, INLINE_TAG_FLOAT,
32 INLINE_TAG_INTEGER, INLINE_VALUE_MASK, MSB_FLAG,
33};
34use crate::{q_hash, NQuin};
35
36pub type Resolver<'r> = &'r dyn Fn(u64) -> Option<String>;
39
40pub fn object_as_f64(object: u64) -> Option<f64> {
43 if object & MSB_FLAG != 0 {
44 return None; }
46 match object_tag(object) {
47 INLINE_TAG_FLOAT => Some(unpack_float_object(object) as f64),
48 INLINE_TAG_INTEGER => {
49 let mut n = (object & INLINE_VALUE_MASK) as i64;
50 if n & (1i64 << 59) != 0 {
51 n |= !((1i64 << 60) - 1); }
53 Some(n as f64)
54 }
55 INLINE_TAG_DECIMAL => {
56 let mut raw = (object & INLINE_VALUE_MASK) as i64;
57 if raw & (1i64 << 59) != 0 {
58 raw |= !((1i64 << 60) - 1);
59 }
60 Some(raw as f64 / 1_000_000.0) }
62 INLINE_TAG_BOOLEAN => Some(if object & 1 != 0 { 1.0 } else { 0.0 }),
63 _ => None,
64 }
65}
66
67fn node_kind_of(object: u64) -> u8 {
69 if object & MSB_FLAG != 0 {
70 1 } else if object_tag(object) != 0 {
72 2 } else {
74 1 }
76}
77
78fn node_kind_matches(object: u64, want: NodeKindType) -> bool {
79 let k = node_kind_of(object); match want {
81 NodeKindType::BlankNode => k == 0,
82 NodeKindType::Iri => k == 1,
83 NodeKindType::Literal => k == 2,
84 NodeKindType::BlankNodeOrIri => k == 0 || k == 1,
85 NodeKindType::BlankNodeOrLiteral => k == 0 || k == 2,
86 NodeKindType::IriOrLiteral => k == 1 || k == 2,
87 }
88}
89
90fn datatype_tag(dt: &str) -> Option<u64> {
92 let h = q_hash(dt);
93 if h == q_hash("xsd:string") {
94 Some(0)
95 } else if h == q_hash("xsd:integer") {
96 Some(INLINE_TAG_INTEGER)
97 } else if h == q_hash("xsd:decimal") {
98 Some(INLINE_TAG_DECIMAL)
99 } else if h == q_hash("xsd:float") || h == q_hash("xsd:double") {
100 Some(INLINE_TAG_FLOAT)
101 } else if h == q_hash("xsd:boolean") {
102 Some(INLINE_TAG_BOOLEAN)
103 } else {
104 None
105 }
106}
107
108fn label(node: u64, resolve: Resolver) -> String {
111 if let Some(s) = resolve(node) {
112 return s;
113 }
114 if node & MSB_FLAG == 0 {
115 match object_tag(node) {
116 INLINE_TAG_BOOLEAN => return (node & 1 != 0).to_string(),
117 INLINE_TAG_FLOAT | INLINE_TAG_INTEGER | INLINE_TAG_DECIMAL => {
118 if let Some(n) = object_as_f64(node) {
119 return if n.fract() == 0.0 && n.abs() < 1e15 {
120 (n as i64).to_string()
121 } else {
122 n.to_string()
123 };
124 }
125 }
126 _ => {}
127 }
128 }
129 format!("node:{node:016x}")
130}
131
132const RDF_TYPE_KEYS: [&str; 2] = ["rdf:type", "a"];
133
134pub struct ShaclEngine<'a> {
136 pub quins: &'a [NQuin],
137 pub shapes: &'a [CompiledShape],
140}
141
142impl<'a> ShaclEngine<'a> {
143 pub fn new(quins: &'a [NQuin], shapes: &'a [CompiledShape]) -> Self {
144 Self { quins, shapes }
145 }
146
147 pub fn validate(&self, resolve: Resolver) -> ValidationReport {
149 let mut results = Vec::new();
150 for shape in self.shapes {
151 for focus in self.target_nodes(shape) {
152 self.validate_focus(focus, shape, resolve, &mut results);
153 }
154 }
155 ValidationReport {
156 conforms: !results
157 .iter()
158 .any(|r| r.severity == ShaclSeverity::Violation),
159 results,
160 }
161 }
162
163 pub fn validate_focus(
165 &self,
166 focus: u64,
167 shape: &CompiledShape,
168 resolve: Resolver,
169 out: &mut Vec<ValidationResult>,
170 ) {
171 let path_set = !shape.property_path.is_empty();
172 let values: Vec<u64> = if path_set {
173 self.values_at(focus, &shape.property_path)
174 } else {
175 vec![focus]
176 };
177 let path = if path_set {
178 Some(shape.property_path.clone())
179 } else {
180 None
181 };
182 for c in &shape.constraints {
183 self.check(focus, &path, &values, c, shape.severity, resolve, out);
184 }
185 }
186
187 fn values_at(&self, focus: u64, path: &str) -> Vec<u64> {
189 let p = q_hash(path);
190 self.quins
191 .iter()
192 .filter(|q| q.subject == focus && q.predicate == p)
193 .map(|q| q.object)
194 .collect()
195 }
196
197 fn is_a(&self, node: u64, class_hash: u64) -> bool {
199 let type_keys = RDF_TYPE_KEYS.map(q_hash);
200 self.quins.iter().any(|q| {
201 q.subject == node && type_keys.contains(&q.predicate) && q.object == class_hash
202 })
203 }
204
205 fn shape_named(&self, name: &str) -> Option<&CompiledShape> {
207 let h = q_hash(name);
208 self.shapes.iter().find(|s| q_hash(&s.shape_class) == h)
209 }
210
211 fn focus_conforms(&self, focus: u64, name: &str, resolve: Resolver) -> bool {
213 match self.shape_named(name) {
214 Some(s) => {
215 let mut tmp = Vec::new();
216 self.validate_focus(focus, s, resolve, &mut tmp);
217 !tmp.iter().any(|r| r.severity == ShaclSeverity::Violation)
218 }
219 None => false,
221 }
222 }
223
224 #[allow(clippy::too_many_arguments)]
225 fn check(
226 &self,
227 focus: u64,
228 path: &Option<String>,
229 values: &[u64],
230 c: &ShaclConstraint,
231 severity: ShaclSeverity,
232 resolve: Resolver,
233 out: &mut Vec<ValidationResult>,
234 ) {
235 let mut violate = |component: &str, value: Option<u64>, msg: String| {
236 out.push(ValidationResult {
237 severity,
238 focus_node: label(focus, resolve),
239 result_path: path.clone(),
240 message: Some(msg),
241 source_constraint: None,
242 source_constraint_component: Some(component.to_string()),
243 value: value.map(|v| label(v, resolve)),
244 });
245 };
246
247 match c {
248 ShaclConstraint::MinInclusive(m) => {
250 for &v in values {
251 match object_as_f64(v) {
252 Some(x) if x >= *m => {}
253 _ => violate(
254 "sh:MinInclusiveConstraintComponent",
255 Some(v),
256 format!("value < minInclusive {m} (or not numeric)"),
257 ),
258 }
259 }
260 }
261 ShaclConstraint::MaxInclusive(m) => {
262 for &v in values {
263 match object_as_f64(v) {
264 Some(x) if x <= *m => {}
265 _ => violate(
266 "sh:MaxInclusiveConstraintComponent",
267 Some(v),
268 format!("value > maxInclusive {m} (or not numeric)"),
269 ),
270 }
271 }
272 }
273 ShaclConstraint::MinExclusive(m) => {
274 for &v in values {
275 match object_as_f64(v) {
276 Some(x) if x > *m => {}
277 _ => violate(
278 "sh:MinExclusiveConstraintComponent",
279 Some(v),
280 format!("value <= minExclusive {m} (or not numeric)"),
281 ),
282 }
283 }
284 }
285 ShaclConstraint::MaxExclusive(m) => {
286 for &v in values {
287 match object_as_f64(v) {
288 Some(x) if x < *m => {}
289 _ => violate(
290 "sh:MaxExclusiveConstraintComponent",
291 Some(v),
292 format!("value >= maxExclusive {m} (or not numeric)"),
293 ),
294 }
295 }
296 }
297 ShaclConstraint::DatatypeRange {
298 min_inclusive,
299 max_inclusive,
300 min_exclusive,
301 max_exclusive,
302 } => {
303 for &v in values {
304 let x = object_as_f64(v);
305 let ok = match x {
306 Some(x) => {
307 min_inclusive.map_or(true, |b| x >= b)
308 && max_inclusive.map_or(true, |b| x <= b)
309 && min_exclusive.map_or(true, |b| x > b)
310 && max_exclusive.map_or(true, |b| x < b)
311 }
312 None => false,
313 };
314 if !ok {
315 violate(
316 "sh:DatatypeRange",
317 Some(v),
318 "value outside datatype range".into(),
319 );
320 }
321 }
322 }
323
324 ShaclConstraint::MinCount(n) => {
326 if (values.len() as u32) < *n {
327 violate(
328 "sh:MinCountConstraintComponent",
329 None,
330 format!("{} value(s) < minCount {n}", values.len()),
331 );
332 }
333 }
334 ShaclConstraint::MaxCount(n) => {
335 if (values.len() as u32) > *n {
336 violate(
337 "sh:MaxCountConstraintComponent",
338 None,
339 format!("{} value(s) > maxCount {n}", values.len()),
340 );
341 }
342 }
343
344 ShaclConstraint::Class(class) => {
346 let ch = q_hash(class);
347 for &v in values {
348 if !self.is_a(v, ch) {
349 violate(
350 "sh:ClassConstraintComponent",
351 Some(v),
352 format!("value is not an instance of {class}"),
353 );
354 }
355 }
356 }
357 ShaclConstraint::DataType(dt) => {
358 if let Some(tag) = datatype_tag(dt) {
359 for &v in values {
360 let ok = if v & MSB_FLAG != 0 {
361 false
362 } else if tag == 0 {
363 object_tag(v) == 0 } else {
365 object_tag(v) == tag
366 };
367 if !ok {
368 violate(
369 "sh:DatatypeConstraintComponent",
370 Some(v),
371 format!("value is not a {dt}"),
372 );
373 }
374 }
375 }
376 }
377 ShaclConstraint::NodeKind(kind_str) => {
378 let want = parse_node_kind(kind_str);
379 if let Some(want) = want {
380 for &v in values {
381 if !node_kind_matches(v, want) {
382 violate(
383 "sh:NodeKindConstraintComponent",
384 Some(v),
385 format!("value is not nodeKind {kind_str}"),
386 );
387 }
388 }
389 }
390 }
391 ShaclConstraint::NodeKindStrict(kind) => {
392 for &v in values {
393 if !node_kind_matches(v, *kind) {
394 violate(
395 "sh:NodeKindConstraintComponent",
396 Some(v),
397 format!("value is not nodeKind {kind:?}"),
398 );
399 }
400 }
401 }
402
403 ShaclConstraint::In(allowed) => {
405 let set: Vec<u64> = allowed.iter().map(|s| q_hash(s)).collect();
406 for &v in values {
407 if !set.contains(&v) {
408 violate(
409 "sh:InConstraintComponent",
410 Some(v),
411 "value not in the allowed set".into(),
412 );
413 }
414 }
415 }
416 ShaclConstraint::HasValue(expected) => {
417 let e = q_hash(expected);
418 if !values.contains(&e) {
419 violate(
420 "sh:HasValueConstraintComponent",
421 None,
422 format!("required value {expected} absent"),
423 );
424 }
425 }
426
427 ShaclConstraint::MinLength(n) => {
429 for &v in values {
430 match resolve(v) {
431 Some(s) if s.chars().count() as u32 >= *n => {}
432 _ => violate(
433 "sh:MinLengthConstraintComponent",
434 Some(v),
435 format!("length < minLength {n} (or unresolvable)"),
436 ),
437 }
438 }
439 }
440 ShaclConstraint::MaxLength(n) => {
441 for &v in values {
442 match resolve(v) {
443 Some(s) if s.chars().count() as u32 <= *n => {}
444 _ => violate(
445 "sh:MaxLengthConstraintComponent",
446 Some(v),
447 format!("length > maxLength {n} (or unresolvable)"),
448 ),
449 }
450 }
451 }
452 ShaclConstraint::Pattern(pat) => match regex::Regex::new(pat) {
453 Ok(re) => {
454 for &v in values {
455 match resolve(v) {
456 Some(s) if re.is_match(&s) => {}
457 _ => violate(
458 "sh:PatternConstraintComponent",
459 Some(v),
460 format!("value does not match /{pat}/ (or unresolvable)"),
461 ),
462 }
463 }
464 }
465 Err(_) => violate(
466 "sh:PatternConstraintComponent",
467 None,
468 format!("invalid regex pattern /{pat}/"),
469 ),
470 },
471 ShaclConstraint::LanguageIn(langs) => {
472 for &v in values {
473 let ok = resolve(v)
474 .map(|s| langs.iter().any(|l| lang_tag_matches(&s, l)))
475 .unwrap_or(false);
476 if !ok {
477 violate(
478 "sh:LanguageInConstraintComponent",
479 Some(v),
480 "value language tag not in languageIn (or unresolvable)".into(),
481 );
482 }
483 }
484 }
485 ShaclConstraint::UniqueLang => {
486 let mut seen: Vec<String> = Vec::new();
487 for &v in values {
488 if let Some(tag) = resolve(v).and_then(|s| lang_tag_of(&s)) {
489 if seen.contains(&tag) {
490 violate(
491 "sh:UniqueLangConstraintComponent",
492 Some(v),
493 format!("duplicate language tag @{tag}"),
494 );
495 } else {
496 seen.push(tag);
497 }
498 }
499 }
500 }
501
502 ShaclConstraint::Equals(other) => {
504 let theirs = self.values_at(focus, other);
505 if !same_set(values, &theirs) {
506 violate(
507 "sh:EqualsConstraintComponent",
508 None,
509 format!("value set != values of {other}"),
510 );
511 }
512 }
513 ShaclConstraint::LessThan(other) => {
514 self.compare_pair(
515 focus,
516 values,
517 other,
518 severity,
519 path,
520 resolve,
521 out,
522 "sh:LessThanConstraintComponent",
523 |a, b| a < b,
524 );
525 }
526 ShaclConstraint::LessThanOrEquals(other) => {
527 self.compare_pair(
528 focus,
529 values,
530 other,
531 severity,
532 path,
533 resolve,
534 out,
535 "sh:LessThanOrEqualsConstraintComponent",
536 |a, b| a <= b,
537 );
538 }
539 ShaclConstraint::GreaterThan(other) => {
540 self.compare_pair(
541 focus,
542 values,
543 other,
544 severity,
545 path,
546 resolve,
547 out,
548 "sh:GreaterThanConstraintComponent",
549 |a, b| a > b,
550 );
551 }
552 ShaclConstraint::GreaterThanOrEquals(other) => {
553 self.compare_pair(
554 focus,
555 values,
556 other,
557 severity,
558 path,
559 resolve,
560 out,
561 "sh:GreaterThanOrEqualsConstraintComponent",
562 |a, b| a >= b,
563 );
564 }
565
566 ShaclConstraint::Node(shape) => {
568 for &v in values {
569 if !self.focus_conforms(v, shape, resolve) {
570 violate(
571 "sh:NodeConstraintComponent",
572 Some(v),
573 format!("value does not conform to shape {shape}"),
574 );
575 }
576 }
577 }
578 ShaclConstraint::And(shapes) => {
579 for &v in values {
580 if !shapes.iter().all(|s| self.focus_conforms(v, s, resolve)) {
581 violate(
582 "sh:AndConstraintComponent",
583 Some(v),
584 "value fails one or more sh:and shapes".into(),
585 );
586 }
587 }
588 }
589 ShaclConstraint::Or(shapes) => {
590 for &v in values {
591 if !shapes.iter().any(|s| self.focus_conforms(v, s, resolve)) {
592 violate(
593 "sh:OrConstraintComponent",
594 Some(v),
595 "value conforms to none of the sh:or shapes".into(),
596 );
597 }
598 }
599 }
600 ShaclConstraint::Not(shape) => {
601 for &v in values {
602 if self.focus_conforms(v, shape, resolve) {
603 violate(
604 "sh:NotConstraintComponent",
605 Some(v),
606 format!("value conforms to negated shape {shape}"),
607 );
608 }
609 }
610 }
611 ShaclConstraint::Xone(shapes) => {
612 for &v in values {
613 let n = shapes
614 .iter()
615 .filter(|s| self.focus_conforms(v, s, resolve))
616 .count();
617 if n != 1 {
618 violate(
619 "sh:XoneConstraintComponent",
620 Some(v),
621 format!("value conforms to {n} sh:xone shapes (need exactly 1)"),
622 );
623 }
624 }
625 }
626 ShaclConstraint::Closed { ignored_properties } => {
627 let mut allowed: Vec<u64> = ignored_properties.iter().map(|s| q_hash(s)).collect();
628 if let Some(p) = path {
629 allowed.push(q_hash(p));
630 }
631 let type_keys = RDF_TYPE_KEYS.map(q_hash);
632 for q in self.quins.iter().filter(|q| q.subject == focus) {
633 if !allowed.contains(&q.predicate) && !type_keys.contains(&q.predicate) {
634 violate(
635 "sh:ClosedConstraintComponent",
636 Some(q.object),
637 format!("closed shape: unexpected predicate {:016x}", q.predicate),
638 );
639 }
640 }
641 }
642
643 ShaclConstraint::PropertyPath {
645 path: pp,
646 constraint,
647 } => {
648 let pvals = self.values_for_path(focus, pp);
649 let pstr = property_path_label(pp);
650 self.check(
651 focus,
652 &Some(pstr),
653 &pvals,
654 constraint,
655 severity,
656 resolve,
657 out,
658 );
659 }
660 ShaclConstraint::QualifierValue { path: pp, value } => {
661 let pvals = self.values_for_path(focus, pp);
662 if !pvals.contains(&q_hash(value)) {
663 violate(
664 "sh:QualifiedValueShapeConstraintComponent",
665 None,
666 format!("qualified value {value} absent at path"),
667 );
668 }
669 }
670
671 ShaclConstraint::EpistemicConstraint {
675 certainty_threshold,
676 } => {
677 self.check_truth_degree(
678 focus,
679 path,
680 *certainty_threshold,
681 severity,
682 resolve,
683 out,
684 "q42:EpistemicConstraintComponent",
685 );
686 }
687 ShaclConstraint::ProbabilisticConstraint {
688 confidence_threshold,
689 } => {
690 self.check_truth_degree(
691 focus,
692 path,
693 *confidence_threshold,
694 severity,
695 resolve,
696 out,
697 "q42:ProbabilisticConstraintComponent",
698 );
699 }
700 ShaclConstraint::DeonticPolicy { .. }
708 | ShaclConstraint::DeonticObligate
709 | ShaclConstraint::DeonticPermit
710 | ShaclConstraint::DeonticForbid
711 | ShaclConstraint::DeonticNotExpired { .. }
712 | ShaclConstraint::EpistemicKnowledge { .. }
713 | ShaclConstraint::EpistemicBelief { .. }
714 | ShaclConstraint::CommonKnowledge
715 | ShaclConstraint::LtlConstraint { .. }
716 | ShaclConstraint::ParaconsistentConstraint { .. }
717 | ShaclConstraint::CalculusConstraint { .. }
718 | ShaclConstraint::GraphConstraint { .. }
719 | ShaclConstraint::ArgumentationConstraint { .. }
720 | ShaclConstraint::DialecticalConstraint { .. }
721 | ShaclConstraint::EconVaRPositive
722 | ShaclConstraint::EconConvergedModel
723 | ShaclConstraint::EconPositivePrice
724 | ShaclConstraint::EconRiskBelowThreshold { .. }
725 | ShaclConstraint::EconWelfareAboveFloor { .. }
726 | ShaclConstraint::AspConstraint { .. }
727 | ShaclConstraint::DiffusionConstraint { .. }
728 | ShaclConstraint::LinearLogicConstraint { .. }
729 | ShaclConstraint::ControlFeedbackConstraint { .. }
730 | ShaclConstraint::IntervalArithmeticConstraint { .. } => {}
731 }
732 }
733
734 fn values_for_path(&self, focus: u64, path: &PropertyPath) -> Vec<u64> {
736 match path {
737 PropertyPath::Predicate(p) => self.values_at(focus, p),
738 PropertyPath::Inverse(inner) => {
739 if let PropertyPath::Predicate(p) = inner.as_ref() {
740 let ph = q_hash(p);
741 self.quins
742 .iter()
743 .filter(|q| q.object == focus && q.predicate == ph)
744 .map(|q| q.subject)
745 .collect()
746 } else {
747 Vec::new()
748 }
749 }
750 PropertyPath::Sequence(steps) => {
751 let mut frontier = vec![focus];
752 for step in steps {
753 let mut next = Vec::new();
754 for f in frontier {
755 next.extend(self.values_for_path(f, step));
756 }
757 frontier = next;
758 }
759 frontier
760 }
761 PropertyPath::Alternative(alts) => {
762 let mut out = Vec::new();
763 for a in alts {
764 out.extend(self.values_for_path(focus, a));
765 }
766 out
767 }
768 PropertyPath::ZeroOrMore(inner) => {
769 let mut seen = vec![focus];
770 let mut frontier = vec![focus];
771 while let Some(f) = frontier.pop() {
772 for v in self.values_for_path(f, inner) {
773 if !seen.contains(&v) {
774 seen.push(v);
775 frontier.push(v);
776 }
777 }
778 }
779 seen
780 }
781 PropertyPath::OneOrMore(inner) => {
782 let mut seen = Vec::new();
783 let mut frontier = self.values_for_path(focus, inner);
784 while let Some(f) = frontier.pop() {
785 if !seen.contains(&f) {
786 seen.push(f);
787 frontier.extend(self.values_for_path(f, inner));
788 }
789 }
790 seen
791 }
792 PropertyPath::ZeroOrOne(inner) => {
793 let mut out = vec![focus];
794 out.extend(self.values_for_path(focus, inner));
795 out
796 }
797 }
798 }
799
800 #[allow(clippy::too_many_arguments)]
801 fn compare_pair(
802 &self,
803 focus: u64,
804 values: &[u64],
805 other: &str,
806 severity: ShaclSeverity,
807 path: &Option<String>,
808 resolve: Resolver,
809 out: &mut Vec<ValidationResult>,
810 component: &str,
811 cmp: fn(f64, f64) -> bool,
812 ) {
813 let theirs = self.values_at(focus, other);
814 for &a in values {
815 for &b in &theirs {
816 let ok = match (object_as_f64(a), object_as_f64(b)) {
817 (Some(x), Some(y)) => cmp(x, y),
818 _ => false,
819 };
820 if !ok {
821 out.push(ValidationResult {
822 severity,
823 focus_node: label(focus, resolve),
824 result_path: path.clone(),
825 message: Some(format!("comparison vs {other} failed")),
826 source_constraint: None,
827 source_constraint_component: Some(component.to_string()),
828 value: Some(label(a, resolve)),
829 });
830 }
831 }
832 }
833 }
834
835 #[allow(clippy::too_many_arguments)]
836 fn check_truth_degree(
837 &self,
838 focus: u64,
839 path: &Option<String>,
840 threshold: f32,
841 severity: ShaclSeverity,
842 resolve: Resolver,
843 out: &mut Vec<ValidationResult>,
844 component: &str,
845 ) {
846 let p = path.as_ref().map(|s| q_hash(s));
847 for q in self.quins.iter().filter(|q| q.subject == focus) {
848 if let Some(ph) = p {
849 if q.predicate != ph {
850 continue;
851 }
852 }
853 if crate::frame_layout::truth_degree(q.metadata) < threshold {
854 out.push(ValidationResult {
855 severity,
856 focus_node: label(focus, resolve),
857 result_path: path.clone(),
858 message: Some(format!("truth/confidence below {threshold}")),
859 source_constraint: None,
860 source_constraint_component: Some(component.to_string()),
861 value: Some(label(q.object, resolve)),
862 });
863 }
864 }
865 }
866
867 fn target_nodes(&self, shape: &CompiledShape) -> Vec<u64> {
871 let class = q_hash(&shape.shape_class);
872 let type_keys = RDF_TYPE_KEYS.map(q_hash);
873 let mut nodes: Vec<u64> = self
874 .quins
875 .iter()
876 .filter(|q| type_keys.contains(&q.predicate) && q.object == class)
877 .map(|q| q.subject)
878 .collect();
879 nodes.sort_unstable();
880 nodes.dedup();
881 nodes
882 }
883}
884
885fn parse_node_kind(s: &str) -> Option<NodeKindType> {
886 match s.trim_start_matches("sh:") {
887 "BlankNode" => Some(NodeKindType::BlankNode),
888 "IRI" => Some(NodeKindType::Iri),
889 "Literal" => Some(NodeKindType::Literal),
890 "BlankNodeOrIRI" => Some(NodeKindType::BlankNodeOrIri),
891 "BlankNodeOrLiteral" => Some(NodeKindType::BlankNodeOrLiteral),
892 "IRIOrLiteral" => Some(NodeKindType::IriOrLiteral),
893 _ => None,
894 }
895}
896
897fn property_path_label(p: &PropertyPath) -> String {
898 match p {
899 PropertyPath::Predicate(s) => s.clone(),
900 PropertyPath::Inverse(i) => format!("^{}", property_path_label(i)),
901 PropertyPath::Sequence(s) => s
902 .iter()
903 .map(property_path_label)
904 .collect::<Vec<_>>()
905 .join("/"),
906 PropertyPath::Alternative(s) => s
907 .iter()
908 .map(property_path_label)
909 .collect::<Vec<_>>()
910 .join("|"),
911 PropertyPath::ZeroOrMore(i) => format!("{}*", property_path_label(i)),
912 PropertyPath::OneOrMore(i) => format!("{}+", property_path_label(i)),
913 PropertyPath::ZeroOrOne(i) => format!("{}?", property_path_label(i)),
914 }
915}
916
917fn same_set(a: &[u64], b: &[u64]) -> bool {
918 a.iter().all(|x| b.contains(x)) && b.iter().all(|x| a.contains(x))
919}
920
921fn lang_tag_of(s: &str) -> Option<String> {
923 s.rsplit_once('@')
924 .map(|(_, tag)| tag.trim_matches('"').to_ascii_lowercase())
925}
926
927fn lang_tag_matches(value: &str, want: &str) -> bool {
930 match lang_tag_of(value) {
931 Some(tag) => {
932 let want = want.to_ascii_lowercase();
933 tag == want || tag.starts_with(&format!("{want}-"))
934 }
935 None => false,
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::frame_layout::{pack_float_object, INLINE_TAG_INTEGER};
943
944 fn iri(s: &str) -> u64 {
945 q_hash(s)
946 }
947 fn int_obj(n: i64) -> u64 {
948 INLINE_TAG_INTEGER | ((n as u64) & INLINE_VALUE_MASK)
949 }
950 fn quin(s: u64, p: u64, o: u64) -> NQuin {
951 NQuin {
952 subject: s,
953 predicate: p,
954 object: o,
955 context: 0,
956 metadata: 0,
957 parity: 0,
958 }
959 }
960 fn type_quin(node: u64, class: &str) -> NQuin {
961 quin(node, q_hash("rdf:type"), q_hash(class))
962 }
963 fn no_resolve() -> impl Fn(u64) -> Option<String> {
964 |_| None
965 }
966
967 fn shape(class: &str, path: &str, cs: Vec<ShaclConstraint>) -> CompiledShape {
968 let mut s = CompiledShape::new(class.to_string(), cs, ShaclSeverity::Violation);
969 s.property_path = path.to_string();
970 s
971 }
972
973 #[test]
974 fn min_inclusive_passes_and_fails_on_real_numbers() {
975 let alice = iri("ex:Alice");
976 let bob = iri("ex:Bob");
977 let quins = vec![
978 type_quin(alice, "ex:Adult"),
979 quin(alice, q_hash("ex:age"), int_obj(30)),
980 type_quin(bob, "ex:Adult"),
981 quin(bob, q_hash("ex:age"), int_obj(12)),
982 ];
983 let shapes = vec![shape(
984 "ex:Adult",
985 "ex:age",
986 vec![ShaclConstraint::MinInclusive(18.0)],
987 )];
988 let eng = ShaclEngine::new(&quins, &shapes);
989 let rep = eng.validate(&no_resolve());
990 assert!(!rep.conforms, "bob (age 12) must violate minInclusive 18");
991 assert_eq!(rep.results.len(), 1);
992 assert_eq!(
993 rep.results[0].source_constraint_component.as_deref(),
994 Some("sh:MinInclusiveConstraintComponent")
995 );
996 }
997
998 #[test]
999 fn min_max_count_use_property_value_count() {
1000 let n = iri("ex:N");
1001 let quins = vec![
1002 type_quin(n, "ex:Thing"),
1003 quin(n, q_hash("ex:p"), iri("ex:v1")),
1004 quin(n, q_hash("ex:p"), iri("ex:v2")),
1005 ];
1006 let too_few = vec![shape(
1007 "ex:Thing",
1008 "ex:p",
1009 vec![ShaclConstraint::MinCount(3)],
1010 )];
1011 assert!(
1012 !ShaclEngine::new(&quins, &too_few)
1013 .validate(&no_resolve())
1014 .conforms
1015 );
1016 let too_many = vec![shape(
1017 "ex:Thing",
1018 "ex:p",
1019 vec![ShaclConstraint::MaxCount(1)],
1020 )];
1021 assert!(
1022 !ShaclEngine::new(&quins, &too_many)
1023 .validate(&no_resolve())
1024 .conforms
1025 );
1026 let ok = vec![shape(
1027 "ex:Thing",
1028 "ex:p",
1029 vec![ShaclConstraint::MinCount(2), ShaclConstraint::MaxCount(2)],
1030 )];
1031 assert!(
1032 ShaclEngine::new(&quins, &ok)
1033 .validate(&no_resolve())
1034 .conforms
1035 );
1036 }
1037
1038 #[test]
1039 fn class_constraint_checks_rdf_type() {
1040 let n = iri("ex:N");
1041 let dog = iri("ex:Rex");
1042 let quins = vec![
1043 type_quin(n, "ex:Owner"),
1044 quin(n, q_hash("ex:pet"), dog),
1045 type_quin(dog, "ex:Cat"),
1046 ];
1047 let shapes = vec![shape(
1048 "ex:Owner",
1049 "ex:pet",
1050 vec![ShaclConstraint::Class("ex:Dog".into())],
1051 )];
1052 assert!(
1053 !ShaclEngine::new(&quins, &shapes)
1054 .validate(&no_resolve())
1055 .conforms
1056 );
1057 }
1058
1059 #[test]
1060 fn in_and_has_value() {
1061 let n = iri("ex:N");
1062 let quins = vec![
1063 type_quin(n, "ex:T"),
1064 quin(n, q_hash("ex:status"), iri("ex:active")),
1065 ];
1066 let in_ok = vec![shape(
1067 "ex:T",
1068 "ex:status",
1069 vec![ShaclConstraint::In(vec![
1070 "ex:active".into(),
1071 "ex:inactive".into(),
1072 ])],
1073 )];
1074 assert!(
1075 ShaclEngine::new(&quins, &in_ok)
1076 .validate(&no_resolve())
1077 .conforms
1078 );
1079 let in_bad = vec![shape(
1080 "ex:T",
1081 "ex:status",
1082 vec![ShaclConstraint::In(vec!["ex:archived".into()])],
1083 )];
1084 assert!(
1085 !ShaclEngine::new(&quins, &in_bad)
1086 .validate(&no_resolve())
1087 .conforms
1088 );
1089 let hv = vec![shape(
1090 "ex:T",
1091 "ex:status",
1092 vec![ShaclConstraint::HasValue("ex:active".into())],
1093 )];
1094 assert!(
1095 ShaclEngine::new(&quins, &hv)
1096 .validate(&no_resolve())
1097 .conforms
1098 );
1099 }
1100
1101 #[test]
1102 fn pattern_uses_real_regex_with_resolver() {
1103 let n = iri("ex:N");
1104 let email = iri("alice@example.org"); let quins = vec![type_quin(n, "ex:User"), quin(n, q_hash("ex:email"), email)];
1106 let shapes = vec![shape(
1107 "ex:User",
1108 "ex:email",
1109 vec![ShaclConstraint::Pattern(r"^[^@]+@[^@]+\.[a-z]+$".into())],
1110 )];
1111 let resolve = |h: u64| {
1112 if h == email {
1113 Some("alice@example.org".to_string())
1114 } else {
1115 None
1116 }
1117 };
1118 assert!(
1119 ShaclEngine::new(&quins, &shapes)
1120 .validate(&resolve)
1121 .conforms
1122 );
1123 let resolve_bad = |h: u64| {
1125 if h == email {
1126 Some("not-an-email".to_string())
1127 } else {
1128 None
1129 }
1130 };
1131 assert!(
1132 !ShaclEngine::new(&quins, &shapes)
1133 .validate(&resolve_bad)
1134 .conforms
1135 );
1136 assert!(
1138 !ShaclEngine::new(&quins, &shapes)
1139 .validate(&no_resolve())
1140 .conforms
1141 );
1142 }
1143
1144 #[test]
1145 fn min_length_with_resolver() {
1146 let n = iri("ex:N");
1147 let v = iri("ex:val");
1148 let quins = vec![type_quin(n, "ex:T"), quin(n, q_hash("ex:name"), v)];
1149 let shapes = vec![shape(
1150 "ex:T",
1151 "ex:name",
1152 vec![ShaclConstraint::MinLength(5)],
1153 )];
1154 let ok = |h: u64| {
1155 if h == v {
1156 Some("Timothy".to_string())
1157 } else {
1158 None
1159 }
1160 };
1161 assert!(ShaclEngine::new(&quins, &shapes).validate(&ok).conforms);
1162 let bad = |h: u64| {
1163 if h == v {
1164 Some("Tim".to_string())
1165 } else {
1166 None
1167 }
1168 };
1169 assert!(!ShaclEngine::new(&quins, &shapes).validate(&bad).conforms);
1170 }
1171
1172 #[test]
1173 fn logical_or_and_not_xone() {
1174 let n = iri("ex:N");
1175 let quins = vec![
1176 type_quin(n, "ex:Doc"),
1177 quin(n, q_hash("ex:title"), iri("t")),
1178 ];
1179 let has_title = shape(
1181 "ex:HasTitle",
1182 "ex:title",
1183 vec![ShaclConstraint::MinCount(1)],
1184 );
1185 let has_author = shape(
1186 "ex:HasAuthor",
1187 "ex:author",
1188 vec![ShaclConstraint::MinCount(1)],
1189 );
1190 let mut or_shape = CompiledShape::new(
1192 "ex:Doc".into(),
1193 vec![ShaclConstraint::Or(vec![
1194 "ex:HasTitle".into(),
1195 "ex:HasAuthor".into(),
1196 ])],
1197 ShaclSeverity::Violation,
1198 );
1199 or_shape.property_path = String::new(); let shapes = vec![or_shape, has_title.clone(), has_author.clone()];
1201 assert!(
1202 ShaclEngine::new(&quins, &shapes)
1203 .validate(&no_resolve())
1204 .conforms
1205 );
1206
1207 let mut not_author = CompiledShape::new(
1209 "ex:Doc".into(),
1210 vec![ShaclConstraint::Not("ex:HasAuthor".into())],
1211 ShaclSeverity::Violation,
1212 );
1213 not_author.property_path = String::new();
1214 let shapes2 = vec![not_author, has_title.clone(), has_author.clone()];
1215 assert!(
1216 ShaclEngine::new(&quins, &shapes2)
1217 .validate(&no_resolve())
1218 .conforms
1219 );
1220
1221 let mut not_title = CompiledShape::new(
1222 "ex:Doc".into(),
1223 vec![ShaclConstraint::Not("ex:HasTitle".into())],
1224 ShaclSeverity::Violation,
1225 );
1226 not_title.property_path = String::new();
1227 let shapes3 = vec![not_title, has_title, has_author];
1228 assert!(
1229 !ShaclEngine::new(&quins, &shapes3)
1230 .validate(&no_resolve())
1231 .conforms
1232 );
1233 }
1234
1235 #[test]
1236 fn closed_shape_rejects_extra_predicates() {
1237 let n = iri("ex:N");
1238 let quins = vec![
1239 type_quin(n, "ex:Strict"),
1240 quin(n, q_hash("ex:allowed"), iri("v1")),
1241 quin(n, q_hash("ex:sneaky"), iri("v2")),
1242 ];
1243 let mut s = CompiledShape::new(
1244 "ex:Strict".into(),
1245 vec![ShaclConstraint::Closed {
1246 ignored_properties: vec!["ex:allowed".into()],
1247 }],
1248 ShaclSeverity::Violation,
1249 );
1250 s.property_path = String::new();
1251 let shapes = vec![s];
1252 let rep = ShaclEngine::new(&quins, &shapes).validate(&no_resolve());
1253 assert!(!rep.conforms, "ex:sneaky is not in the allowed/ignored set");
1254 }
1255
1256 #[test]
1257 fn property_pair_less_than() {
1258 let n = iri("ex:N");
1259 let quins = vec![
1260 type_quin(n, "ex:Event"),
1261 quin(n, q_hash("ex:start"), int_obj(5)),
1262 quin(n, q_hash("ex:end"), int_obj(10)),
1263 ];
1264 let ok = vec![shape(
1265 "ex:Event",
1266 "ex:start",
1267 vec![ShaclConstraint::LessThan("ex:end".into())],
1268 )];
1269 assert!(
1270 ShaclEngine::new(&quins, &ok)
1271 .validate(&no_resolve())
1272 .conforms
1273 );
1274 let bad = vec![shape(
1276 "ex:Event",
1277 "ex:end",
1278 vec![ShaclConstraint::LessThan("ex:start".into())],
1279 )];
1280 assert!(
1281 !ShaclEngine::new(&quins, &bad)
1282 .validate(&no_resolve())
1283 .conforms
1284 );
1285 }
1286
1287 #[test]
1288 fn object_as_f64_decodes_inline_types() {
1289 assert_eq!(object_as_f64(int_obj(42)), Some(42.0));
1290 assert_eq!(object_as_f64(int_obj(-7)), Some(-7.0));
1291 assert_eq!(object_as_f64(pack_float_object(3.5)), Some(3.5));
1292 assert_eq!(object_as_f64(q_hash("ex:iri")), None); }
1294}