1use std::collections::{HashMap, HashSet};
20
21use crate::family::Family;
22use crate::model::{CimSpecification, CimType};
23use crate::shacl::model::{ConstraintInfo, FileResults, ShapeInfo, LITERALS, NEGATED, UNSUPPORTED};
24use crate::shacl::skip::SkipCollector;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum AltBranch {
32 Forward(String),
33 Inverse(String),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum Path {
38 Forward(String),
39 Inverse(String),
40 Alternative(Vec<AltBranch>),
41 Chain(Vec<Step>),
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Step {
50 Forward(String),
51 Inverse(String),
52 Type,
54}
55
56impl Path {
57 pub fn yields_types(&self) -> bool {
59 match self {
60 Path::Chain(steps) => matches!(steps.last(), Some(Step::Type)),
61 _ => false,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum NodeKind {
68 Iri,
69 Literal,
70 BlankNode,
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub enum Constraint {
75 MinCount(u32),
76 MaxCount(u32),
77 Datatype(String),
78 NodeKind(NodeKind),
79 Class(Vec<String>),
80 In(Vec<String>),
81 HasValue(String),
82 MaxLength(u32),
83 MinLength(u32),
84 RefClass(Vec<String>),
87 MinInclusive(f64),
88 MaxInclusive(f64),
89 MinExclusive(f64),
90 MaxExclusive(f64),
91 LessThan(String),
93 LessThanOrEquals(String),
94 NotClass(Vec<String>),
97 QualifiedIn { allowed: Vec<String>, min: u32 },
100 Length(u32),
102}
103
104#[derive(Debug, Clone)]
107pub struct Branch {
108 pub props: Vec<PropShape>,
109 pub negate: bool,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum LogicOp {
115 And,
116 Or,
117 Xone,
118}
119
120#[derive(Debug, Clone)]
126pub struct Logic {
127 pub op: LogicOp,
128 pub branches: Vec<Branch>,
129 pub rule_id: String,
130 pub name: String,
131 pub message: String,
132 pub description: String,
133 pub severity: String,
134}
135
136#[derive(Debug, Clone)]
137pub struct Check {
138 pub constraint: Constraint,
139 pub rule_id: String,
140 pub name: String,
141 pub message: String,
142 pub description: String,
143 pub severity: String,
144}
145
146#[derive(Debug, Clone)]
147pub struct PropShape {
148 pub path: Path,
149 pub checks: Vec<Check>,
150}
151
152#[derive(Debug, Clone)]
153pub enum Target {
154 Class(Vec<String>),
155 SubjectsOf(String),
156}
157
158#[derive(Debug, Clone)]
159pub struct ClosedShape {
160 pub allowed: Vec<String>,
161 pub rule_id: String,
162 pub name: String,
163 pub message: String,
164 pub description: String,
165 pub severity: String,
166}
167
168#[derive(Debug, Clone)]
169pub struct ShapeDef {
170 pub targets: Vec<Target>,
171 pub props: Vec<PropShape>,
172 pub closed: Option<ClosedShape>,
173 pub logic: Vec<Logic>,
174 pub profiles: Vec<String>,
175 pub file: String,
176}
177
178#[derive(Debug, Default)]
179pub struct Stats {
180 pub shapes: usize,
181 pub props: usize,
182 pub checks: usize,
183 pub closed: usize,
184 pub logic: usize,
185}
186
187pub struct Resolver {
194 by_ns: HashMap<(String, String), String>,
200 concrete_of: HashMap<String, Vec<String>>,
201 any_family: HashMap<(String, String), String>,
207 any_concrete: HashMap<String, Vec<String>>,
208 ancestors: HashMap<String, Vec<String>>,
210}
211
212type ClassMaps = (HashMap<(String, String), String>, HashMap<String, Vec<String>>);
215
216fn class_maps(spec: &CimSpecification) -> ClassMaps {
217 let prefix = spec.family.type_prefix;
218 let qualified = |t: &CimType| format!("{prefix}{}", t.id);
219 let by_ns = spec
220 .types
221 .values()
222 .map(|t| ((t.namespace.clone(), t.id.clone()), qualified(t)))
223 .collect();
224
225 let mut concrete: HashMap<String, Vec<String>> = HashMap::new();
226 for t in spec.types.values() {
227 if t.concrete_in.is_empty() {
228 concrete.entry(qualified(t)).or_default();
229 continue;
230 }
231 let q = qualified(t);
234 let mut cursor = Some(t);
235 let mut guard = 0;
236 while let Some(c) = cursor {
237 concrete.entry(qualified(c)).or_default().push(q.clone());
238 guard += 1;
241 if guard > 64 {
242 break;
243 }
244 cursor = spec.types.get(&c.super_type);
245 }
246 }
247 for v in concrete.values_mut() {
248 v.sort();
249 v.dedup();
250 }
251 (by_ns, concrete)
252}
253
254impl Resolver {
255 pub fn new(spec: &CimSpecification, others: &[&CimSpecification]) -> Self {
256 let (by_ns, concrete_of) = class_maps(spec);
257 let mut any_family = by_ns.clone();
258 let mut any_concrete = concrete_of.clone();
259 for other in others {
260 let (b, c) = class_maps(other);
261 any_family.extend(b);
262 for (k, v) in c {
263 any_concrete.entry(k).or_default().extend(v);
264 }
265 }
266 for v in any_concrete.values_mut() {
267 v.sort();
268 v.dedup();
269 }
270 let prefix = spec.family.type_prefix;
271 let ancestors = spec
272 .types
273 .values()
274 .map(|t| {
275 let mut chain = Vec::new();
276 let mut cursor = Some(t);
277 while let Some(c) = cursor.filter(|_| chain.len() < 64) {
279 chain.push(format!("{prefix}{}", c.id));
280 cursor = spec.types.get(&c.super_type);
281 }
282 (format!("{prefix}{}", t.id), chain)
283 })
284 .collect();
285 Self { by_ns, concrete_of, any_family, any_concrete, ancestors }
286 }
287
288 fn closed_concrete(&self, target: &str, closed_classes: &HashSet<String>) -> Vec<String> {
299 if self.is_concrete(target) {
300 return vec![target.to_string()];
301 }
302 let below_target = |c: &String| {
303 c != target && self.ancestors.get(c).is_some_and(|a| a.iter().any(|x| x == target))
304 };
305 self.concrete(target)
306 .iter()
307 .filter(|d| {
308 !self.ancestors.get(*d).is_some_and(|chain| {
309 chain.iter().any(|a| closed_classes.contains(a) && below_target(a))
310 })
311 })
312 .cloned()
313 .collect()
314 }
315
316 fn class(&self, iri: &str, prefixes: &HashMap<String, String>) -> Option<&String> {
323 let (prefix, local) = iri.split_once(':')?;
324 let ns = prefixes.get(prefix)?;
325 self.by_ns.get(&(ns.clone(), local.to_string()))
326 }
327
328 fn concrete(&self, qualified: &str) -> &[String] {
329 self.concrete_of.get(qualified).map_or(&[], Vec::as_slice)
330 }
331
332 fn is_concrete(&self, qualified: &str) -> bool {
333 self.concrete(qualified).iter().any(|c| c == qualified)
334 }
335
336 fn targets_of(&self, qualified: &str) -> Vec<String> {
349 if self.is_concrete(qualified) {
350 vec![qualified.to_string()]
351 } else {
352 self.concrete(qualified).to_vec()
353 }
354 }
355
356 fn any_classes(&self, iris: &[String], prefixes: &HashMap<String, String>) -> Vec<String> {
362 let mut out: Vec<String> = Vec::new();
363 for iri in iris {
364 let Some((prefix, local)) = iri.split_once(':') else { continue };
365 let Some(ns) = prefixes.get(prefix) else { continue };
366 if let Some(q) = self.any_family.get(&(ns.clone(), local.to_string())) {
367 match self.any_concrete.get(q) {
368 Some(c) if !c.is_empty() => out.extend(c.iter().cloned()),
369 _ => out.push(q.clone()),
370 }
371 }
372 }
373 out.sort();
374 out.dedup();
375 out
376 }
377}
378
379fn field_key(iri: &str) -> &str {
386 iri.rsplit_once(':').map_or(iri, |(_, local)| local)
387}
388
389fn value_key(c: &ConstraintInfo, v: &str, prefixes: &HashMap<String, String>) -> String {
392 let literal = c
393 .payload
394 .get(LITERALS)
395 .and_then(|l| l.as_list())
396 .is_some_and(|l| l.iter().any(|x| x == v));
397 if literal { v.to_string() } else { iri_key(v, prefixes) }
398}
399
400fn iri_key(v: &str, prefixes: &HashMap<String, String>) -> String {
410 let full = match v.strip_prefix('<') {
411 Some(inner) => inner.trim_end_matches('>').to_string(),
412 None => match v.split_once(':') {
413 Some((prefix, local)) => match prefixes.get(prefix) {
414 Some(ns) => format!("{ns}{local}"),
415 None => return local.to_string(),
416 },
417 None => v.to_string(),
418 },
419 };
420 match full.rfind('#') {
421 Some(i) => full[i + 1..].to_string(),
422 None => full,
423 }
424}
425
426pub fn resolve_shapes(
432 spec: &CimSpecification,
433 others: &[&CimSpecification],
434 files: &[FileResults],
435 profiles_of: &HashMap<String, Vec<String>>,
436 collector: &mut SkipCollector,
437) -> (Vec<ShapeDef>, Stats) {
438 let r = Resolver::new(spec, others);
439 let mut stats = Stats::default();
440 let mut out = Vec::new();
441
442 for fr in files {
443 let profiles = match profiles_of.get(&fr.file_name) {
444 Some(p) if !p.is_empty() => p.clone(),
445 _ => {
446 collector.push("", &fr.file_name, "file", "",
447 "not imported by any profile manifest");
448 continue;
449 }
450 };
451 let closed_classes: HashSet<String> = fr
453 .shapes
454 .iter()
455 .filter(|shape| shape.closed.is_some())
456 .flat_map(|shape| &shape.targets)
457 .filter(|t| t.kind == "targetClass")
458 .filter_map(|t| r.class(&t.value, &fr.prefixes).cloned())
459 .collect();
460 for shape in &fr.shapes {
461 for def in resolve_shape(shape, fr, &profiles, &r, &closed_classes, collector, &mut stats) {
462 stats.shapes += 1;
463 out.push(def);
464 }
465 }
466 }
467 (out, stats)
468}
469
470fn resolve_shape(
471 shape: &ShapeInfo,
472 fr: &FileResults,
473 profiles: &[String],
474 r: &Resolver,
475 closed_classes: &HashSet<String>,
476 collector: &mut SkipCollector,
477 stats: &mut Stats,
478) -> Vec<ShapeDef> {
479 let mut targets: Vec<Target> = Vec::new();
480 let mut closed_targets: Vec<Target> = Vec::new();
482 let mut target_classes: Vec<String> = Vec::new();
483
484 for t in &shape.targets {
485 match t.kind.as_str() {
486 "targetClass" | "targetNode" => match r.class(&t.value, &fr.prefixes) {
487 Some(q) => {
488 let concrete = r.targets_of(q);
489 if concrete.is_empty() {
490 collector.push(&t.value, "", "sh:targetClass", &shape.name,
491 "class has no concrete subclass in this family");
492 continue;
493 }
494 target_classes.push(q.clone());
495 targets.push(Target::Class(concrete));
496 if shape.closed.is_some() {
497 let narrowed = r.closed_concrete(q, closed_classes);
498 if !narrowed.is_empty() {
499 closed_targets.push(Target::Class(narrowed));
500 }
501 }
502 }
503 None => {
504 collector.push(&t.value, "", "sh:targetClass", &shape.name,
510 "target class is not in this family's schema (cross-family shape)");
511 continue;
512 }
513 },
514 "targetSubjectsOf" => {
515 targets.push(Target::SubjectsOf(field_key(&t.value).to_string()));
516 closed_targets.push(Target::SubjectsOf(field_key(&t.value).to_string()));
517 }
518 other => {
519 collector.push(&t.value, "", other, &shape.name,
520 "unsupported SHACL target mechanism");
521 }
522 }
523 }
524 if targets.is_empty() {
525 return Vec::new();
526 }
527
528 let class_label = target_classes.first().cloned().unwrap_or_default();
529
530 let mut props: Vec<PropShape> = Vec::new();
531 for prop in &shape.properties {
532 if let Some(p) = resolve_prop(prop, fr, &class_label, r, collector, stats) {
533 props.push(p);
534 }
535 }
536
537 let closed = shape.closed.as_ref().map(|allowed| {
538 stats.closed += 1;
539 ClosedShape {
540 allowed: allowed.iter().map(|p| field_key(p).to_string()).collect(),
541 rule_id: shape.id.clone(),
542 name: shape.name.clone(),
543 message: shape.description.clone(),
544 description: shape.description.clone(),
545 severity: "sh:Info".to_string(),
547 }
548 });
549
550 let mut logic: Vec<Logic> = Vec::new();
551 for c in &shape.constraints {
552 if let Some(l) = resolve_logic(c, shape, fr, &class_label, r, collector) {
553 logic.push(l);
554 }
555 }
556 stats.logic += logic.len();
557
558 if props.is_empty() && closed.is_none() && logic.is_empty() {
559 return Vec::new();
560 }
561 stats.props += props.len();
562
563 let def = |targets, props, closed, logic| ShapeDef {
564 targets,
565 props,
566 closed,
567 logic,
568 profiles: profiles.to_vec(),
569 file: fr.file_name.clone(),
570 };
571 match closed {
572 None => vec![def(targets, props, None, logic)],
573 Some(closed) => {
577 let mut out = Vec::new();
578 if !props.is_empty() || !logic.is_empty() {
579 out.push(def(targets, props, None, logic));
580 }
581 if !closed_targets.is_empty() {
582 out.push(def(closed_targets, Vec::new(), Some(closed), Vec::new()));
583 }
584 out
585 }
586 }
587}
588
589fn resolve_prop(
590 prop: &ShapeInfo,
591 fr: &FileResults,
592 class_label: &str,
593 r: &Resolver,
594 collector: &mut SkipCollector,
595 stats: &mut Stats,
596) -> Option<PropShape> {
597 let seg = prop.path.first().map_or("", String::as_str);
598 let path = match resolve_path(&prop.path) {
599 Ok(p) => p,
600 Err(reason) => {
601 collector.push(class_label, &prop.path.join(" / "), "sh:path", &prop.name, reason);
602 return None;
603 }
604 };
605
606 let is_ref_type = path.yields_types();
618 let mut checks: Vec<Check> = Vec::new();
619 for c in &prop.constraints {
620 match resolve_constraint(c, r, &fr.prefixes, is_ref_type) {
621 Some(constraint) => checks.push(Check {
622 constraint,
623 rule_id: if c.rule_id.is_empty() { prop.id.clone() } else { c.rule_id.clone() },
624 name: if c.name.is_empty() { prop.name.clone() } else { c.name.clone() },
625 message: c.message.clone(),
626 description: if c.description.is_empty() {
627 prop.description.clone()
628 } else {
629 c.description.clone()
630 },
631 severity: if c.severity.is_empty() {
632 "sh:Violation".to_string()
633 } else {
634 c.severity.clone()
635 },
636 }),
637 None => {
638 collector.push(class_label, seg, &c.component, &c.name,
639 "constraint component not supported for bag families");
640 }
641 }
642 }
643 if checks.is_empty() {
644 return None;
645 }
646 stats.checks += checks.len();
647 Some(PropShape { path, checks })
648}
649
650fn resolve_path(segs: &[String]) -> Result<Path, &'static str> {
653 match segs {
654 [] => Err("shape has no resolvable path"),
655 [one] if one == "^rdf:type" => {
660 Err("instance count of a class (`[ sh:inversePath rdf:type ]`), written by hand")
661 }
662 [one] => decode_path(one).ok_or("unsupported path form"),
663 _ => {
664 let mut steps = Vec::with_capacity(segs.len());
665 for (i, seg) in segs.iter().enumerate() {
666 let last = i + 1 == segs.len();
667 steps.push(match seg.as_str() {
668 "rdf:type" if last => Step::Type,
669 "rdf:type" => return Err("rdf:type before the end of a sequence path"),
670 s if s.starts_with('|') => {
671 return Err("alternative path inside a sequence path")
672 }
673 s => match s.strip_prefix('^') {
674 Some(inv) => Step::Inverse(field_key(inv).to_string()),
675 None => Step::Forward(field_key(s).to_string()),
676 },
677 });
678 }
679 Ok(Path::Chain(steps))
680 }
681 }
682}
683
684fn resolve_logic(
692 c: &ConstraintInfo,
693 shape: &ShapeInfo,
694 fr: &FileResults,
695 class_label: &str,
696 r: &Resolver,
697 collector: &mut SkipCollector,
698) -> Option<Logic> {
699 let op = match c.component.as_str() {
700 "sh:AndConstraintComponent" => LogicOp::And,
701 "sh:OrConstraintComponent" => LogicOp::Or,
702 "sh:XoneConstraintComponent" => LogicOp::Xone,
703 _ => return None,
704 };
705 let mut skip = |reason: &str| {
706 collector.push(class_label, "", &c.component, &shape.name, reason);
707 None
708 };
709 if c.payload.get(UNSUPPORTED).and_then(|v| v.as_list()).is_some_and(|u| !u.is_empty()) {
710 return skip("logical branch uses a construct the importer does not represent");
711 }
712 let Some(raw) = c.payload.get("branches").and_then(|v| v.as_shapes()) else {
713 return skip("logical constraint without branches");
714 };
715
716 let negated: Vec<usize> = c
717 .payload
718 .get(NEGATED)
719 .and_then(|v| v.as_list())
720 .map(|l| l.iter().filter_map(|i| i.parse().ok()).collect())
721 .unwrap_or_default();
722 let mut branches: Vec<Branch> = Vec::new();
723 for (bi, raw_branch) in raw.iter().enumerate() {
724 let mut props: Vec<PropShape> = Vec::new();
726 for bc in raw_branch {
727 let Ok(path) = resolve_path(&bc.path) else {
728 return skip("logical branch path does not resolve");
729 };
730 if bc.component == "sh:MinCountConstraintComponent"
733 && bc.payload.get("minCount").and_then(|v| v.as_int()) == Some(0)
734 {
735 continue;
736 }
737 let Some(constraint) = resolve_constraint(bc, r, &fr.prefixes, path.yields_types()) else {
738 return skip("logical branch constraint not supported for bag families");
739 };
740 let check = Check {
741 constraint,
742 rule_id: String::new(),
743 name: String::new(),
744 message: String::new(),
745 description: String::new(),
746 severity: String::new(),
747 };
748 match props.iter_mut().find(|p| p.path == path) {
749 Some(p) => p.checks.push(check),
750 None => props.push(PropShape { path, checks: vec![check] }),
751 }
752 }
753 branches.push(Branch { props, negate: negated.contains(&bi) });
754 }
755 Some(Logic {
756 op,
757 branches,
758 rule_id: c.rule_id.clone(),
759 name: c.name.clone(),
760 message: c.message.clone(),
761 description: c.description.clone(),
762 severity: if c.severity.is_empty() { "sh:Violation".to_string() } else { c.severity.clone() },
763 })
764}
765
766fn decode_path(seg: &str) -> Option<Path> {
769 if let Some(alts) = seg.strip_prefix('|') {
770 let branches: Vec<AltBranch> = alts
771 .split('|')
772 .filter(|b| !b.is_empty())
773 .map(|b| match b.strip_prefix('^') {
774 Some(inv) => AltBranch::Inverse(field_key(inv).to_string()),
775 None => AltBranch::Forward(field_key(b).to_string()),
776 })
777 .collect();
778 if branches.is_empty() {
779 return None;
780 }
781 return Some(Path::Alternative(branches));
782 }
783 if let Some(inv) = seg.strip_prefix('^') {
784 return Some(Path::Inverse(field_key(inv).to_string()));
785 }
786 if seg.is_empty() {
787 return None;
788 }
789 Some(Path::Forward(field_key(seg).to_string()))
790}
791
792fn resolve_constraint(
793 c: &ConstraintInfo,
794 r: &Resolver,
795 prefixes: &HashMap<String, String>,
796 is_ref_type: bool,
797) -> Option<Constraint> {
798 let int = |key: &str| c.payload.get(key).and_then(|v| v.as_int());
799 let float = |key: &str| c.payload.get(key).and_then(|v| v.as_float());
800
801 if is_ref_type {
805 return match c.component.as_str() {
806 "sh:InConstraintComponent" => {
807 let values = c.payload.get("in")?.as_list()?;
808 let classes = r.any_classes(values, prefixes);
809 (!classes.is_empty()).then_some(Constraint::RefClass(classes))
810 }
811 "sh:HasValueConstraintComponent" => {
812 let v = c.payload.get("hasValue")?.as_str()?.to_string();
813 let classes = r.any_classes(std::slice::from_ref(&v), prefixes);
814 (!classes.is_empty()).then_some(Constraint::RefClass(classes))
815 }
816 "sh:NodeKindConstraintComponent" => match c.payload.get("nodeKind")?.as_str()? {
821 "sh:IRI" => Some(Constraint::NodeKind(NodeKind::Iri)),
822 _ => None,
823 },
824 "sh:RequiredConstraintComponent" => Some(Constraint::MinCount(1)),
825 "sh:MinCountConstraintComponent" => Some(Constraint::MinCount(int("minCount")? as u32)),
826 "sh:MaxCountConstraintComponent" => Some(Constraint::MaxCount(int("maxCount")? as u32)),
827 _ => None,
828 };
829 }
830
831 match c.component.as_str() {
832 "sh:RequiredConstraintComponent" => Some(Constraint::MinCount(1)),
833 "sh:MinCountConstraintComponent" => Some(Constraint::MinCount(int("minCount")? as u32)),
834 "sh:MaxCountConstraintComponent" => Some(Constraint::MaxCount(int("maxCount")? as u32)),
835 "sh:ExactCountConstraintComponent" => {
836 let n = int("minCount").or_else(|| int("maxCount"))?;
837 Some(Constraint::MinCount(n as u32))
838 }
839 "sh:DatatypeConstraintComponent" => {
840 let dt = c.payload.get("datatype")?.as_str()?;
841 Some(Constraint::Datatype(field_key(dt.trim_end_matches('>')).to_string()))
842 }
843 "sh:NodeKindConstraintComponent" => {
844 let kind = match c.payload.get("nodeKind")?.as_str()? {
845 "sh:IRI" => NodeKind::Iri,
846 "sh:Literal" => NodeKind::Literal,
847 "sh:BlankNode" => NodeKind::BlankNode,
848 _ => return None,
851 };
852 Some(Constraint::NodeKind(kind))
853 }
854 "sh:ClassConstraintComponent" => {
855 let cls = c.payload.get("class")?.as_str()?.to_string();
856 let classes = r.any_classes(std::slice::from_ref(&cls), prefixes);
857 (!classes.is_empty()).then_some(Constraint::Class(classes))
858 }
859 "sh:OrClassConstraintComponent" => {
863 let classes = r.any_classes(c.payload.get("classes")?.as_list()?, prefixes);
864 (!classes.is_empty()).then_some(Constraint::Class(classes))
865 }
866 "sh:QualifiedMinCountConstraintComponent" => {
869 let min = int("qualifiedMinCount")? as u32;
870 let inner = c.payload.get("shape")?.as_shapes()?.first()?;
871 let [ic] = inner.as_slice() else { return None };
872 let allowed: Vec<String> = match ic.component.as_str() {
873 "sh:InConstraintComponent" => {
874 ic.payload.get("in")?.as_list()?.iter().map(|v| value_key(ic, v, prefixes)).collect()
875 }
876 "sh:HasValueConstraintComponent" => {
877 vec![value_key(ic, ic.payload.get("hasValue")?.as_str()?, prefixes)]
878 }
879 _ => return None,
880 };
881 Some(Constraint::QualifiedIn { allowed, min })
882 }
883 "sh:NotClassConstraintComponent" => {
884 let cls = c.payload.get("class")?.as_str()?.to_string();
885 let classes = r.any_classes(std::slice::from_ref(&cls), prefixes);
886 (!classes.is_empty()).then_some(Constraint::NotClass(classes))
887 }
888 "sh:InConstraintComponent" => {
889 let values = c.payload.get("in")?.as_list()?;
890 Some(Constraint::In(values.iter().map(|v| value_key(c, v, prefixes)).collect()))
891 }
892 "sh:HasValueConstraintComponent" => {
893 let v = c.payload.get("hasValue")?.as_str()?;
894 Some(Constraint::HasValue(value_key(c, v, prefixes)))
895 }
896 "sh:MaxLengthConstraintComponent" => Some(Constraint::MaxLength(int("maxLength")? as u32)),
897 "sh:MinLengthConstraintComponent" => Some(Constraint::MinLength(int("minLength")? as u32)),
898 "sh:LengthConstraintComponent" => Some(Constraint::Length(int("length")? as u32)),
899 "sh:MinInclusiveConstraintComponent" => Some(Constraint::MinInclusive(float("minInclusive")?)),
900 "sh:MaxInclusiveConstraintComponent" => Some(Constraint::MaxInclusive(float("maxInclusive")?)),
901 "sh:MinExclusiveConstraintComponent" => Some(Constraint::MinExclusive(float("minExclusive")?)),
902 "sh:MaxExclusiveConstraintComponent" => Some(Constraint::MaxExclusive(float("maxExclusive")?)),
903 "sh:LessThanConstraintComponent" => {
906 let other = c.payload.get("lessThan")?.as_str()?;
907 Some(Constraint::LessThan(field_key(other).to_string()))
908 }
909 "sh:LessThanOrEqualsConstraintComponent" => {
910 let other = c.payload.get("lessThanOrEquals")?.as_str()?;
911 Some(Constraint::LessThanOrEquals(field_key(other).to_string()))
912 }
913 _ => None,
914 }
915}
916
917#[derive(Debug)]
923pub struct ShapeTable {
924 pub shapes: Vec<ShapeDef>,
925 pub profile_iris: Vec<(String, String)>,
927 pub profiles: Vec<String>,
929 pub stats: Stats,
930}
931
932pub fn load_shape_table(
938 family: &'static Family,
939 spec: &CimSpecification,
940 others: &[&CimSpecification],
941 dir: &std::path::Path,
942 collector: &mut SkipCollector,
943) -> Result<ShapeTable, Box<dyn std::error::Error>> {
944 let mut ttl_paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)?
945 .filter_map(|e| e.ok())
946 .map(|e| e.path())
947 .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("ttl"))
948 .collect();
949 ttl_paths.sort();
950 if ttl_paths.is_empty() {
951 return Err(format!("no SHACL .ttl files in {}", dir.display()).into());
952 }
953
954 let profiles_of: HashMap<String, Vec<String>> = match family.shacl_manifest {
955 Some(manifest) => {
956 let mut m: HashMap<String, Vec<String>> = HashMap::new();
957 for (stem, tag) in manifest {
958 m.entry((*stem).to_string()).or_default().push((*tag).to_string());
959 }
960 m
961 }
962 None => profile_files(&dir.join("Validation"))?,
963 };
964
965 let mut files: Vec<FileResults> = Vec::new();
966 for path in &ttl_paths {
967 match crate::shacl::ttl_import::import_ttl_file(path) {
968 Ok(fr) => files.push(fr),
969 Err(e) => eprintln!("warning: skipping {}: {e}", path.display()),
970 }
971 }
972 for (_, entries) in crate::shacl::simplify::simplify(&mut files) {
976 for e in entries {
977 collector.push(
978 e.class_names.first().map(String::as_str).unwrap_or(""),
979 &e.prop,
980 &e.component,
981 &e.name,
982 &e.reason,
983 );
984 }
985 }
986
987 let (shapes, stats) = resolve_shapes(spec, others, &files, &profiles_of, collector);
988
989 let prof_dir = dir.parent().map_or_else(|| dir.join("PROF"), |p| p.join("PROF"));
993 let index = crate::import::import_profile_index(&prof_dir)?;
994 let mut profile_iris: Vec<(String, String)> = index
995 .iter()
996 .flat_map(|p| p.iris.iter().map(|i| (i.clone(), p.keyword.clone())))
997 .collect();
998 profile_iris.sort();
999 let mut profiles: Vec<String> = index.iter().map(|p| p.keyword.clone()).collect();
1000 profiles.sort();
1001
1002 Ok(ShapeTable { shapes, profile_iris, profiles, stats })
1003}
1004
1005pub const MERGED_PROFILE: &str = "ALL";
1014
1015pub fn profile_files(
1021 dir: &std::path::Path,
1022) -> Result<HashMap<String, Vec<String>>, Box<dyn std::error::Error>> {
1023 let mut map: HashMap<String, Vec<String>> = HashMap::new();
1024 let mut merged_only: Vec<String> = Vec::new();
1025 let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)?
1026 .filter_map(|e| e.ok())
1027 .map(|e| e.path())
1028 .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("ttl"))
1029 .collect();
1030 paths.sort();
1031 for path in &paths {
1032 let m = crate::shacl::ttl_import::import_manifest(path)?;
1033 if m.profile == MERGED_PROFILE {
1034 merged_only = m.imports;
1035 continue;
1036 }
1037 for file in m.imports {
1038 map.entry(file).or_default().push(m.profile.clone());
1039 }
1040 }
1041 for file in merged_only {
1042 map.entry(file).or_insert_with(|| vec![MERGED_PROFILE.to_string()]);
1043 }
1044 for v in map.values_mut() {
1045 v.sort();
1046 v.dedup();
1047 }
1048 Ok(map)
1049}