1use std::collections::HashMap;
22use std::fmt;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum NodeSelector {
29 NodeIri(String),
31 Pattern(String),
33 Wildcard,
35 Focus,
37}
38
39impl fmt::Display for NodeSelector {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::NodeIri(iri) => write!(f, "<{iri}>"),
43 Self::Pattern(p) => write!(f, "/{p}/"),
44 Self::Wildcard => write!(f, "*"),
45 Self::Focus => write!(f, "FOCUS"),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub enum ShapeLabel {
55 Iri(String),
57 Start,
59 All,
61}
62
63impl fmt::Display for ShapeLabel {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Iri(iri) => write!(f, "<{iri}>"),
67 Self::Start => write!(f, "START"),
68 Self::All => write!(f, "*"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct ShapeAssociation {
78 pub node: NodeSelector,
80 pub shape: ShapeLabel,
82}
83
84impl fmt::Display for ShapeAssociation {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "{}@{}", self.node, self.shape)
87 }
88}
89
90#[derive(Debug, Clone, Default)]
94pub struct ShapeMap {
95 pub associations: Vec<ShapeAssociation>,
97}
98
99impl ShapeMap {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn add(&mut self, node: NodeSelector, shape: ShapeLabel) {
107 self.associations.push(ShapeAssociation { node, shape });
108 }
109
110 pub fn len(&self) -> usize {
112 self.associations.len()
113 }
114
115 pub fn is_empty(&self) -> bool {
117 self.associations.is_empty()
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum ValidationStatus {
126 Conformant,
128 NonConformant,
130 Incomplete,
132}
133
134impl fmt::Display for ValidationStatus {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::Conformant => write!(f, "conformant"),
138 Self::NonConformant => write!(f, "nonConformant"),
139 Self::Incomplete => write!(f, "incomplete"),
140 }
141 }
142}
143
144#[derive(Debug, Clone)]
148pub struct ValidationResult {
149 pub node: String,
151 pub shape: String,
153 pub status: ValidationStatus,
155 pub messages: Vec<String>,
157}
158
159impl ValidationResult {
160 pub fn conformant(node: impl Into<String>, shape: impl Into<String>) -> Self {
162 Self {
163 node: node.into(),
164 shape: shape.into(),
165 status: ValidationStatus::Conformant,
166 messages: Vec::new(),
167 }
168 }
169
170 pub fn non_conformant(
172 node: impl Into<String>,
173 shape: impl Into<String>,
174 messages: Vec<String>,
175 ) -> Self {
176 Self {
177 node: node.into(),
178 shape: shape.into(),
179 status: ValidationStatus::NonConformant,
180 messages,
181 }
182 }
183
184 pub fn incomplete(
186 node: impl Into<String>,
187 shape: impl Into<String>,
188 reason: impl Into<String>,
189 ) -> Self {
190 Self {
191 node: node.into(),
192 shape: shape.into(),
193 status: ValidationStatus::Incomplete,
194 messages: vec![reason.into()],
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
203pub struct ShapeMapError(pub String);
204
205impl fmt::Display for ShapeMapError {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 write!(f, "ShapeMapError: {}", self.0)
208 }
209}
210
211impl std::error::Error for ShapeMapError {}
212
213pub fn parse_shape_map(text: &str) -> Result<ShapeMap, ShapeMapError> {
228 let mut map = ShapeMap::new();
229
230 for raw_line in text.lines() {
231 for part in raw_line.split(';') {
233 let part = part.trim();
234 if part.is_empty() || part.starts_with('#') {
235 continue;
236 }
237
238 let at_pos = find_at_separator(part)
239 .ok_or_else(|| ShapeMapError(format!("missing '@' separator in: '{part}'")))?;
240
241 let node_str = part[..at_pos].trim();
242 let shape_str = part[at_pos + 1..].trim();
243
244 let node = parse_node_selector(node_str)?;
245 let shape = parse_shape_label(shape_str)?;
246
247 map.add(node, shape);
248 }
249 }
250
251 Ok(map)
252}
253
254fn find_at_separator(s: &str) -> Option<usize> {
258 let bytes = s.as_bytes();
259 let mut i = 0;
260 while i < bytes.len() {
261 match bytes[i] {
262 b'<' => {
263 i += 1;
264 while i < bytes.len() && bytes[i] != b'>' {
265 i += 1;
266 }
267 }
268 b'/' => {
269 i += 1;
270 while i < bytes.len() && bytes[i] != b'/' {
271 if bytes[i] == b'\\' {
272 i += 1;
273 }
274 i += 1;
275 }
276 }
277 b'@' => return Some(i),
278 _ => {}
279 }
280 i += 1;
281 }
282 None
283}
284
285fn parse_node_selector(s: &str) -> Result<NodeSelector, ShapeMapError> {
286 if s == "*" {
287 Ok(NodeSelector::Wildcard)
288 } else if s.eq_ignore_ascii_case("FOCUS") {
289 Ok(NodeSelector::Focus)
290 } else if s.starts_with('<') && s.ends_with('>') {
291 Ok(NodeSelector::NodeIri(s[1..s.len() - 1].to_string()))
292 } else if s.starts_with('/') && s.ends_with('/') && s.len() >= 2 {
293 Ok(NodeSelector::Pattern(s[1..s.len() - 1].to_string()))
294 } else {
295 Err(ShapeMapError(format!("unrecognised node selector: '{s}'")))
296 }
297}
298
299fn parse_shape_label(s: &str) -> Result<ShapeLabel, ShapeMapError> {
300 if s == "*" {
301 Ok(ShapeLabel::All)
302 } else if s.eq_ignore_ascii_case("START") {
303 Ok(ShapeLabel::Start)
304 } else if s.starts_with('<') && s.ends_with('>') {
305 Ok(ShapeLabel::Iri(s[1..s.len() - 1].to_string()))
306 } else {
307 Err(ShapeMapError(format!("unrecognised shape label: '{s}'")))
308 }
309}
310
311pub struct ShapeMapValidator {
324 constraints: HashMap<String, Vec<(String, String)>>,
327}
328
329impl ShapeMapValidator {
330 pub fn new() -> Self {
332 Self {
333 constraints: HashMap::new(),
334 }
335 }
336
337 pub fn add_constraint(
342 &mut self,
343 shape_iri: impl Into<String>,
344 property: impl Into<String>,
345 value: impl Into<String>,
346 ) {
347 self.constraints
348 .entry(shape_iri.into())
349 .or_default()
350 .push((property.into(), value.into()));
351 }
352
353 pub fn validate(
357 &self,
358 shape_map: &ShapeMap,
359 graph_triples: &[(String, String, String)],
360 ) -> Vec<ValidationResult> {
361 let mut results = Vec::new();
362
363 let all_nodes: Vec<String> = {
365 let mut nodes: Vec<String> = graph_triples.iter().map(|(s, _, _)| s.clone()).collect();
366 nodes.sort();
367 nodes.dedup();
368 nodes
369 };
370
371 let mut subj_props: HashMap<&str, Vec<(&str, &str)>> = HashMap::new();
373 for (s, p, o) in graph_triples {
374 subj_props
375 .entry(s.as_str())
376 .or_default()
377 .push((p.as_str(), o.as_str()));
378 }
379
380 for assoc in &shape_map.associations {
381 let node_iris: Vec<String> = match &assoc.node {
383 NodeSelector::NodeIri(iri) => vec![iri.clone()],
384 NodeSelector::Wildcard => all_nodes.clone(),
385 NodeSelector::Focus => {
386 for node in &all_nodes {
389 results.push(ValidationResult::incomplete(
390 node,
391 assoc.shape.to_string(),
392 "FOCUS selector requires a query context".to_string(),
393 ));
394 }
395 continue;
396 }
397 NodeSelector::Pattern(pat) => {
398 all_nodes
400 .iter()
401 .filter(|n| n.contains(pat.as_str()))
402 .cloned()
403 .collect()
404 }
405 };
406
407 let shape_iris: Vec<String> = match &assoc.shape {
409 ShapeLabel::Iri(iri) => vec![iri.clone()],
410 ShapeLabel::Start => {
411 self.constraints.keys().take(1).cloned().collect()
413 }
414 ShapeLabel::All => self.constraints.keys().cloned().collect(),
415 };
416
417 for node_iri in &node_iris {
418 for shape_iri in &shape_iris {
419 let result = self.validate_node_shape(node_iri, shape_iri, &subj_props);
420 results.push(result);
421 }
422 }
423 }
424
425 results
426 }
427
428 fn validate_node_shape(
429 &self,
430 node: &str,
431 shape: &str,
432 subj_props: &HashMap<&str, Vec<(&str, &str)>>,
433 ) -> ValidationResult {
434 let Some(constraints) = self.constraints.get(shape) else {
435 return ValidationResult::incomplete(
436 node,
437 shape,
438 format!("shape '{shape}' has no registered constraints"),
439 );
440 };
441
442 let props = subj_props.get(node).cloned().unwrap_or_default();
443 let mut messages = Vec::new();
444
445 for (req_prop, req_val) in constraints {
446 let found = props.iter().any(|(p, o)| {
447 *p == req_prop.as_str() && (req_val.is_empty() || *o == req_val.as_str())
448 });
449 if !found {
450 messages.push(format!(
451 "Node <{node}> missing required property <{req_prop}> = '{req_val}' for shape <{shape}>"
452 ));
453 }
454 }
455
456 if messages.is_empty() {
457 ValidationResult::conformant(node, shape)
458 } else {
459 ValidationResult::non_conformant(node, shape, messages)
460 }
461 }
462}
463
464impl Default for ShapeMapValidator {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470pub fn render_json(results: &[ValidationResult]) -> String {
476 let mut out = String::from("[\n");
477 for (i, r) in results.iter().enumerate() {
478 out.push_str(" {\n");
479 out.push_str(&format!(" \"node\": \"{}\",\n", json_escape(&r.node)));
480 out.push_str(&format!(" \"shape\": \"{}\",\n", json_escape(&r.shape)));
481 out.push_str(&format!(" \"status\": \"{}\"", r.status));
482 if !r.messages.is_empty() {
483 out.push_str(",\n \"reason\": [");
484 for (j, msg) in r.messages.iter().enumerate() {
485 out.push_str(&format!("\"{}\"", json_escape(msg)));
486 if j + 1 < r.messages.len() {
487 out.push_str(", ");
488 }
489 }
490 out.push(']');
491 }
492 out.push_str("\n }");
493 if i + 1 < results.len() {
494 out.push(',');
495 }
496 out.push('\n');
497 }
498 out.push(']');
499 out
500}
501
502fn json_escape(s: &str) -> String {
503 s.replace('\\', "\\\\")
504 .replace('"', "\\\"")
505 .replace('\n', "\\n")
506 .replace('\r', "\\r")
507 .replace('\t', "\\t")
508}
509
510#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn triples(v: &[(&str, &str, &str)]) -> Vec<(String, String, String)> {
517 v.iter()
518 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
519 .collect()
520 }
521
522 #[test]
525 fn test_node_selector_wildcard() {
526 assert_eq!(NodeSelector::Wildcard.to_string(), "*");
527 }
528
529 #[test]
530 fn test_node_selector_iri() {
531 let ns = NodeSelector::NodeIri("http://example.org/alice".to_string());
532 assert_eq!(ns.to_string(), "<http://example.org/alice>");
533 }
534
535 #[test]
536 fn test_node_selector_pattern() {
537 let ns = NodeSelector::Pattern("example".to_string());
538 assert_eq!(ns.to_string(), "/example/");
539 }
540
541 #[test]
542 fn test_node_selector_focus() {
543 assert_eq!(NodeSelector::Focus.to_string(), "FOCUS");
544 }
545
546 #[test]
549 fn test_shape_label_iri() {
550 let sl = ShapeLabel::Iri("http://schema.org/PersonShape".to_string());
551 assert_eq!(sl.to_string(), "<http://schema.org/PersonShape>");
552 }
553
554 #[test]
555 fn test_shape_label_start() {
556 assert_eq!(ShapeLabel::Start.to_string(), "START");
557 }
558
559 #[test]
560 fn test_shape_label_all() {
561 assert_eq!(ShapeLabel::All.to_string(), "*");
562 }
563
564 #[test]
567 fn test_shape_map_new_empty() {
568 let sm = ShapeMap::new();
569 assert!(sm.is_empty());
570 assert_eq!(sm.len(), 0);
571 }
572
573 #[test]
574 fn test_shape_map_add() {
575 let mut sm = ShapeMap::new();
576 sm.add(
577 NodeSelector::NodeIri("http://example.org/alice".to_string()),
578 ShapeLabel::Iri("http://schema.org/PersonShape".to_string()),
579 );
580 assert_eq!(sm.len(), 1);
581 assert!(!sm.is_empty());
582 }
583
584 #[test]
585 fn test_association_display() {
586 let assoc = ShapeAssociation {
587 node: NodeSelector::NodeIri("http://ex.org/n".to_string()),
588 shape: ShapeLabel::Iri("http://ex.org/s".to_string()),
589 };
590 assert!(assoc.to_string().contains('@'));
591 }
592
593 #[test]
596 fn test_parse_simple_iri_association() {
597 let text = "<http://example.org/alice>@<http://schema.org/PersonShape>";
598 let sm = parse_shape_map(text).expect("parse ok");
599 assert_eq!(sm.len(), 1);
600 assert_eq!(
601 sm.associations[0].node,
602 NodeSelector::NodeIri("http://example.org/alice".to_string())
603 );
604 assert_eq!(
605 sm.associations[0].shape,
606 ShapeLabel::Iri("http://schema.org/PersonShape".to_string())
607 );
608 }
609
610 #[test]
611 fn test_parse_wildcard_node() {
612 let text = "*@<http://schema.org/EntityShape>";
613 let sm = parse_shape_map(text).expect("parse ok");
614 assert_eq!(sm.len(), 1);
615 assert_eq!(sm.associations[0].node, NodeSelector::Wildcard);
616 }
617
618 #[test]
619 fn test_parse_start_shape() {
620 let text = "<http://example.org/bob>@START";
621 let sm = parse_shape_map(text).expect("parse ok");
622 assert_eq!(sm.associations[0].shape, ShapeLabel::Start);
623 }
624
625 #[test]
626 fn test_parse_all_shape() {
627 let text = "<http://example.org/x>@*";
628 let sm = parse_shape_map(text).expect("parse ok");
629 assert_eq!(sm.associations[0].shape, ShapeLabel::All);
630 }
631
632 #[test]
633 fn test_parse_pattern_node() {
634 let text = "/alice/@<http://schema.org/PersonShape>";
635 let sm = parse_shape_map(text).expect("parse ok");
636 assert_eq!(
637 sm.associations[0].node,
638 NodeSelector::Pattern("alice".to_string())
639 );
640 }
641
642 #[test]
643 fn test_parse_focus_node() {
644 let text = "FOCUS@<http://schema.org/PersonShape>";
645 let sm = parse_shape_map(text).expect("parse ok");
646 assert_eq!(sm.associations[0].node, NodeSelector::Focus);
647 }
648
649 #[test]
650 fn test_parse_multiple_lines() {
651 let text = "<http://ex.org/a>@<http://ex.org/S>\n<http://ex.org/b>@START";
652 let sm = parse_shape_map(text).expect("parse ok");
653 assert_eq!(sm.len(), 2);
654 }
655
656 #[test]
657 fn test_parse_semicolon_separated() {
658 let text = "<http://ex.org/a>@<http://ex.org/S>;<http://ex.org/b>@START";
659 let sm = parse_shape_map(text).expect("parse ok");
660 assert_eq!(sm.len(), 2);
661 }
662
663 #[test]
664 fn test_parse_empty_string() {
665 let sm = parse_shape_map("").expect("parse ok");
666 assert!(sm.is_empty());
667 }
668
669 #[test]
670 fn test_parse_comments() {
671 let text = "# This is a comment\n<http://ex.org/a>@<http://ex.org/S>";
672 let sm = parse_shape_map(text).expect("parse ok");
673 assert_eq!(sm.len(), 1);
674 }
675
676 #[test]
677 fn test_parse_error_missing_at() {
678 let result = parse_shape_map("<http://ex.org/a><http://ex.org/S>");
679 assert!(result.is_err());
680 }
681
682 #[test]
683 fn test_parse_error_bad_node() {
684 let result = parse_shape_map("badnode@<http://ex.org/S>");
685 assert!(result.is_err());
686 }
687
688 #[test]
691 fn test_validate_conformant() {
692 let mut validator = ShapeMapValidator::new();
693 validator.add_constraint(
694 "http://schema.org/PersonShape",
695 "http://schema.org/name",
696 "",
697 );
698 let graph = triples(&[(
699 "http://example.org/alice",
700 "http://schema.org/name",
701 "Alice",
702 )]);
703 let mut sm = ShapeMap::new();
704 sm.add(
705 NodeSelector::NodeIri("http://example.org/alice".to_string()),
706 ShapeLabel::Iri("http://schema.org/PersonShape".to_string()),
707 );
708 let results = validator.validate(&sm, &graph);
709 assert_eq!(results.len(), 1);
710 assert_eq!(results[0].status, ValidationStatus::Conformant);
711 }
712
713 #[test]
714 fn test_validate_non_conformant() {
715 let mut validator = ShapeMapValidator::new();
716 validator.add_constraint(
717 "http://schema.org/PersonShape",
718 "http://schema.org/name",
719 "",
720 );
721 let graph = triples(&[("http://example.org/bob", "http://schema.org/age", "30")]);
722 let mut sm = ShapeMap::new();
723 sm.add(
724 NodeSelector::NodeIri("http://example.org/bob".to_string()),
725 ShapeLabel::Iri("http://schema.org/PersonShape".to_string()),
726 );
727 let results = validator.validate(&sm, &graph);
728 assert_eq!(results[0].status, ValidationStatus::NonConformant);
729 assert!(!results[0].messages.is_empty());
730 }
731
732 #[test]
733 fn test_validate_incomplete_unknown_shape() {
734 let validator = ShapeMapValidator::new();
735 let graph = triples(&[("http://ex.org/a", "http://ex.org/p", "v")]);
736 let mut sm = ShapeMap::new();
737 sm.add(
738 NodeSelector::NodeIri("http://ex.org/a".to_string()),
739 ShapeLabel::Iri("http://ex.org/UnknownShape".to_string()),
740 );
741 let results = validator.validate(&sm, &graph);
742 assert_eq!(results[0].status, ValidationStatus::Incomplete);
743 }
744
745 #[test]
746 fn test_validate_wildcard_expands_to_all_nodes() {
747 let mut validator = ShapeMapValidator::new();
748 validator.add_constraint("http://ex.org/S", "http://ex.org/p", "");
749 let graph = triples(&[
750 ("http://ex.org/a", "http://ex.org/p", "x"),
751 ("http://ex.org/b", "http://ex.org/p", "y"),
752 ]);
753 let mut sm = ShapeMap::new();
754 sm.add(
755 NodeSelector::Wildcard,
756 ShapeLabel::Iri("http://ex.org/S".to_string()),
757 );
758 let results = validator.validate(&sm, &graph);
759 assert_eq!(results.len(), 2);
760 }
761
762 #[test]
763 fn test_validate_pattern_selector() {
764 let mut validator = ShapeMapValidator::new();
765 validator.add_constraint("http://ex.org/S", "http://ex.org/p", "");
766 let graph = triples(&[
767 ("http://ex.org/alice_1", "http://ex.org/p", "v"),
768 ("http://ex.org/bob_1", "http://ex.org/p", "v"),
769 ("http://other.org/charlie", "http://ex.org/p", "v"),
770 ]);
771 let mut sm = ShapeMap::new();
772 sm.add(
773 NodeSelector::Pattern("ex.org/alice".to_string()),
774 ShapeLabel::Iri("http://ex.org/S".to_string()),
775 );
776 let results = validator.validate(&sm, &graph);
777 assert_eq!(results.len(), 1);
779 assert_eq!(results[0].node, "http://ex.org/alice_1");
780 assert_eq!(results[0].status, ValidationStatus::Conformant);
781 }
782
783 #[test]
784 fn test_validate_multiple_constraints() {
785 let mut validator = ShapeMapValidator::new();
786 validator.add_constraint("http://ex.org/S", "http://ex.org/name", "");
787 validator.add_constraint("http://ex.org/S", "http://ex.org/age", "");
788 let graph = triples(&[("http://ex.org/n", "http://ex.org/name", "N")]);
789 let mut sm = ShapeMap::new();
790 sm.add(
791 NodeSelector::NodeIri("http://ex.org/n".to_string()),
792 ShapeLabel::Iri("http://ex.org/S".to_string()),
793 );
794 let results = validator.validate(&sm, &graph);
795 assert_eq!(results[0].status, ValidationStatus::NonConformant);
796 assert_eq!(results[0].messages.len(), 1);
798 }
799
800 #[test]
801 fn test_validate_focus_returns_incomplete() {
802 let validator = ShapeMapValidator::new();
803 let graph = triples(&[("http://ex.org/a", "http://ex.org/p", "v")]);
804 let mut sm = ShapeMap::new();
805 sm.add(
806 NodeSelector::Focus,
807 ShapeLabel::Iri("http://ex.org/S".to_string()),
808 );
809 let results = validator.validate(&sm, &graph);
810 for r in &results {
811 assert_eq!(r.status, ValidationStatus::Incomplete);
812 }
813 }
814
815 #[test]
818 fn test_render_json_conformant() {
819 let results = vec![ValidationResult::conformant(
820 "http://ex.org/alice",
821 "http://ex.org/S",
822 )];
823 let json = render_json(&results);
824 assert!(json.contains("conformant"));
825 assert!(json.contains("alice"));
826 assert!(json.contains('['));
827 assert!(json.contains(']'));
828 }
829
830 #[test]
831 fn test_render_json_non_conformant() {
832 let results = vec![ValidationResult::non_conformant(
833 "http://ex.org/bob",
834 "http://ex.org/S",
835 vec!["Missing required property".to_string()],
836 )];
837 let json = render_json(&results);
838 assert!(json.contains("nonConformant"));
839 assert!(json.contains("reason"));
840 }
841
842 #[test]
843 fn test_render_json_empty() {
844 let json = render_json(&[]);
845 assert!(json.contains('['));
846 assert!(json.contains(']'));
847 }
848
849 #[test]
850 fn test_render_json_multiple() {
851 let results = vec![
852 ValidationResult::conformant("http://ex.org/a", "http://ex.org/S"),
853 ValidationResult::non_conformant(
854 "http://ex.org/b",
855 "http://ex.org/S",
856 vec!["err".to_string()],
857 ),
858 ];
859 let json = render_json(&results);
860 assert!(json.contains("conformant"));
861 assert!(json.contains("nonConformant"));
862 }
863
864 #[test]
865 fn test_render_json_escapes() {
866 let results = vec![ValidationResult::non_conformant(
867 r#"http://ex.org/n"ode"#,
868 "http://ex.org/S",
869 vec!["line1\nline2".to_string()],
870 )];
871 let json = render_json(&results);
872 assert!(json.contains("\\\""));
874 }
875
876 #[test]
877 fn test_validation_status_display() {
878 assert_eq!(ValidationStatus::Conformant.to_string(), "conformant");
879 assert_eq!(ValidationStatus::NonConformant.to_string(), "nonConformant");
880 assert_eq!(ValidationStatus::Incomplete.to_string(), "incomplete");
881 }
882
883 #[test]
884 fn test_specific_value_constraint() {
885 let mut validator = ShapeMapValidator::new();
886 validator.add_constraint("http://ex.org/S", "http://ex.org/type", "Person");
887 let graph = triples(&[("http://ex.org/a", "http://ex.org/type", "Person")]);
888 let mut sm = ShapeMap::new();
889 sm.add(
890 NodeSelector::NodeIri("http://ex.org/a".to_string()),
891 ShapeLabel::Iri("http://ex.org/S".to_string()),
892 );
893 let results = validator.validate(&sm, &graph);
894 assert_eq!(results[0].status, ValidationStatus::Conformant);
895 }
896
897 #[test]
898 fn test_specific_value_constraint_wrong_value() {
899 let mut validator = ShapeMapValidator::new();
900 validator.add_constraint("http://ex.org/S", "http://ex.org/type", "Person");
901 let graph = triples(&[("http://ex.org/a", "http://ex.org/type", "Organization")]);
902 let mut sm = ShapeMap::new();
903 sm.add(
904 NodeSelector::NodeIri("http://ex.org/a".to_string()),
905 ShapeLabel::Iri("http://ex.org/S".to_string()),
906 );
907 let results = validator.validate(&sm, &graph);
908 assert_eq!(results[0].status, ValidationStatus::NonConformant);
909 }
910}