1use crate::shapes::ShapeDef;
32
33pub fn resolve(family: &'static str, generated: &'static [ShapeDef]) -> &'static [ShapeDef] {
38 #[cfg(feature = "dynamic-shapes")]
39 {
40 dynamic::resolve(family, generated)
41 }
42 #[cfg(not(feature = "dynamic-shapes"))]
43 {
44 let _ = family;
45 generated
46 }
47}
48
49pub fn resolve_profiles(
55 family: &'static str,
56 generated_iris: &'static [(&'static str, &'static str)],
57 generated_codes: &'static [&'static str],
58) -> (&'static [(&'static str, &'static str)], &'static [&'static str]) {
59 #[cfg(feature = "dynamic-shapes")]
60 {
61 dynamic::resolve_profiles(family, generated_iris, generated_codes)
62 }
63 #[cfg(not(feature = "dynamic-shapes"))]
64 {
65 let _ = family;
66 (generated_iris, generated_codes)
67 }
68}
69
70#[cfg(feature = "dynamic-shapes")]
71pub use dynamic::{family_of_dir, intern_shapes, load_from, load_table, ShapeError, SHACL_DIR_ENV};
72
73#[cfg(feature = "dynamic-shapes")]
74mod dynamic {
75 use std::collections::HashMap;
76 use std::path::{Path, PathBuf};
77 use std::sync::atomic::{AtomicBool, Ordering};
78 use std::sync::{Mutex, OnceLock};
79
80 use cimschema::family::{self, Family};
81 use cimschema::shacl::resolve as r;
82 use cimschema::shacl::skip::SkipCollector;
83
84 use crate::shapes::{
85 AltBranch, Branch, Check, ClosedShape, Constraint, Logic, LogicOp, NodeKind, Path as SPath,
86 PropShape, ShapeDef, Step, Target,
87 };
88
89 pub const SHACL_DIR_ENV: &str = "CIMOXIDE_SHACL_DIR";
93
94 #[derive(Debug)]
95 pub enum ShapeError {
96 TooLate,
98 AlreadyLoaded,
100 UnknownFamily(String),
102 Schema(String),
104 Parse(String),
105 }
106
107 impl std::fmt::Display for ShapeError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::TooLate => write!(
111 f,
112 "the shape table is already resolved; load the shapes before validating"
113 ),
114 Self::AlreadyLoaded => write!(f, "shapes are already loaded for this family"),
115 Self::UnknownFamily(id) => write!(f, "unknown family: {id}"),
116 Self::Schema(e) => write!(f, "cannot read the schema the shapes resolve against: {e}"),
117 Self::Parse(e) => write!(f, "{e}"),
118 }
119 }
120 }
121
122 impl std::error::Error for ShapeError {}
123
124 type Loaded = (
126 &'static [ShapeDef],
127 &'static [(&'static str, &'static str)],
128 &'static [&'static str],
129 );
130
131 fn explicit() -> &'static Mutex<HashMap<&'static str, Loaded>> {
132 static E: OnceLock<Mutex<HashMap<&'static str, Loaded>>> = OnceLock::new();
133 E.get_or_init(|| Mutex::new(HashMap::new()))
134 }
135
136 fn from_env() -> &'static Mutex<HashMap<&'static str, Option<Loaded>>> {
139 static E: OnceLock<Mutex<HashMap<&'static str, Option<Loaded>>>> = OnceLock::new();
140 E.get_or_init(|| Mutex::new(HashMap::new()))
141 }
142
143 static RESOLVED: AtomicBool = AtomicBool::new(false);
146
147 pub fn load_from(family_id: &str, dir: &Path) -> Result<(), ShapeError> {
157 let family = bag_family(family_id)?;
158 if RESOLVED.load(Ordering::SeqCst) {
159 return Err(ShapeError::TooLate);
160 }
161 let loaded = load(family, dir)?;
162 let mut map = explicit().lock().unwrap();
163 if map.contains_key(family.id) {
164 return Err(ShapeError::AlreadyLoaded);
165 }
166 map.insert(family.id, loaded);
167 Ok(())
168 }
169
170 pub fn load_table(family_id: &str, dir: &Path) -> Result<&'static [ShapeDef], ShapeError> {
172 let family = bag_family(family_id)?;
173 Ok(load(family, dir)?.0)
174 }
175
176 fn bag_family(id: &str) -> Result<&'static Family, ShapeError> {
177 family::by_id(id).ok_or_else(|| ShapeError::UnknownFamily(id.to_string()))
178 }
179
180 pub fn family_of_dir(dir: &Path) -> Option<&'static Family> {
184 if dir.join("Validation").is_dir() {
185 return family::by_id("nc");
186 }
187 family::FAMILIES.iter().copied().find(|f| {
188 f.shacl_manifest.is_some_and(|m| {
189 m.iter().any(|(stem, _)| dir.join(format!("{stem}.ttl")).is_file())
190 })
191 })
192 }
193
194 pub(super) fn resolve(
195 family_id: &'static str,
196 generated: &'static [ShapeDef],
197 ) -> &'static [ShapeDef] {
198 env_loaded(family_id).map_or(generated, |l| l.0)
199 }
200
201 pub(super) fn resolve_profiles(
202 family_id: &'static str,
203 generated_iris: &'static [(&'static str, &'static str)],
204 generated_codes: &'static [&'static str],
205 ) -> (&'static [(&'static str, &'static str)], &'static [&'static str]) {
206 env_loaded(family_id).map_or((generated_iris, generated_codes), |l| (l.1, l.2))
207 }
208
209 fn env_loaded(family_id: &'static str) -> Option<Loaded> {
212 RESOLVED.store(true, Ordering::SeqCst);
213
214 if let Some(l) = explicit().lock().unwrap().get(family_id) {
215 return Some(*l);
216 }
217
218 let mut cache = from_env().lock().unwrap();
219 if let Some(cached) = cache.get(family_id) {
220 return *cached;
221 }
222
223 let dir: Option<PathBuf> = std::env::var_os(SHACL_DIR_ENV).and_then(|dirs| {
227 let dirs: Vec<PathBuf> = std::env::split_paths(&dirs).collect();
228 static REPORTED: AtomicBool = AtomicBool::new(false);
229 if !REPORTED.swap(true, Ordering::SeqCst) {
230 for d in dirs.iter().filter(|d| family_of_dir(d).is_none()) {
231 eprintln!(
232 "warning: {SHACL_DIR_ENV} names {}, which holds no NC or CGMES \
233 constraint files; ignoring it",
234 d.display()
235 );
236 }
237 }
238 dirs.into_iter().find(|d| family_of_dir(d).is_some_and(|f| f.id == family_id))
239 });
240 let loaded = match (dir, bag_family(family_id)) {
241 (Some(dir), Ok(family)) => match load(family, &dir) {
242 Ok(l) => Some(l),
243 Err(e) => {
244 eprintln!(
245 "warning: {SHACL_DIR_ENV} names {} but the {family_id} shapes could not \
246 be loaded ({e}); falling back to the generated shape table",
247 dir.display()
248 );
249 None
250 }
251 },
252 _ => None,
253 };
254 cache.insert(family_id, loaded);
255 loaded
256 }
257
258 fn load(family: &'static Family, dir: &Path) -> Result<Loaded, ShapeError> {
265 if !dir.is_dir() {
268 return Err(ShapeError::Parse(format!(
269 "not a directory: {}",
270 dir.display()
271 )));
272 }
273 let (spec, others) = specs(family, dir)?;
274 let other_refs: Vec<&cimschema::model::CimSpecification> = others.iter().collect();
275
276 let mut collector = SkipCollector::new();
277 let table = r::load_shape_table(family, &spec, &other_refs, dir, &mut collector)
278 .map_err(|e| ShapeError::Parse(e.to_string()))?;
279
280 let mut interner = Interner::default();
281 let shapes: Vec<ShapeDef> = table
282 .shapes
283 .iter()
284 .map(|s| shape(s, &mut interner))
285 .collect();
286
287 let iris: Vec<(&'static str, &'static str)> = table
288 .profile_iris
289 .iter()
290 .map(|(iri, code)| (interner.intern(iri), interner.intern(code)))
291 .collect();
292 let codes: Vec<&'static str> =
293 table.profiles.iter().map(|c| interner.intern(c)).collect();
294
295 Ok((Vec::leak(shapes), Vec::leak(iris), Vec::leak(codes)))
296 }
297
298 fn specs(
309 family: &'static Family,
310 shacl_dir: &Path,
311 ) -> Result<
312 (cimschema::model::CimSpecification, Vec<cimschema::model::CimSpecification>),
313 ShapeError,
314 > {
315 let import = |f: &'static Family, pattern: &str| {
316 cimschema::import::import_schema_files(pattern, f, false)
317 .map_err(|e| ShapeError::Schema(e.to_string()))
318 };
319
320 let root = shacl_dir.parent().and_then(Path::parent);
322
323 let spec = import(family, &schema_glob(family, shacl_dir, root))?;
324
325 let mut others = Vec::new();
333 for f in family::FAMILIES.iter().filter(|f| f.id != family.id) {
334 match import(f, &schema_glob(f, shacl_dir, root)) {
335 Ok(s) => others.push(s),
336 Err(e) => eprintln!(
337 "warning: the {} schema could not be read ({e}); its classes will be \
338 missing from {}'s cross-family value-type rules",
339 f.id, family.id
340 ),
341 }
342 }
343 Ok((spec, others))
344 }
345
346 fn schema_glob(f: &'static Family, shacl_dir: &Path, root: Option<&Path>) -> String {
356 let file_name = |p: &str| {
357 Path::new(p)
358 .file_name()
359 .map(|n| n.to_string_lossy().into_owned())
360 .unwrap_or_else(|| "*.rdf".to_string())
361 };
362
363 if shacl_dir
365 .parent()
366 .and_then(|p| p.file_name())
367 .zip(Path::new(f.default_schema).parent().and_then(Path::parent).and_then(|p| p.file_name()))
368 .is_some_and(|(a, b)| a == b)
369 && let Some(dir) = std::env::var_os(cimmodel::schema_source::RDFS_DIR_ENV) {
370 return PathBuf::from(dir)
371 .join(file_name(f.default_schema))
372 .to_string_lossy()
373 .into_owned();
374 }
375
376 let tail: Option<PathBuf> = Path::new(f.default_schema)
378 .components()
379 .skip(1)
380 .fold(None::<PathBuf>, |acc, c| {
381 Some(acc.map_or_else(|| PathBuf::from(c.as_os_str()), |p| p.join(c.as_os_str())))
382 });
383 match (root, tail) {
384 (Some(root), Some(tail)) => {
385 let candidate = root.join(tail);
386 if candidate
387 .parent()
388 .is_some_and(|p| p.is_dir())
389 {
390 return candidate.to_string_lossy().into_owned();
391 }
392 f.default_schema.to_string()
393 }
394 _ => f.default_schema.to_string(),
395 }
396 }
397
398 pub fn intern_shapes(shapes: &[r::ShapeDef]) -> &'static [ShapeDef] {
402 let mut interner = Interner::default();
403 Vec::leak(shapes.iter().map(|s| shape(s, &mut interner)).collect())
404 }
405
406 #[derive(Default)]
410 struct Interner {
411 seen: HashMap<String, &'static str>,
412 }
413
414 impl Interner {
415 fn intern(&mut self, s: &str) -> &'static str {
416 if let Some(v) = self.seen.get(s) {
417 return v;
418 }
419 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
420 self.seen.insert(s.to_string(), leaked);
421 leaked
422 }
423
424 fn intern_all(&mut self, items: &[String]) -> &'static [&'static str] {
425 Vec::leak(items.iter().map(|s| self.intern(s)).collect::<Vec<_>>())
426 }
427 }
428
429 fn shape(s: &r::ShapeDef, i: &mut Interner) -> ShapeDef {
434 ShapeDef {
435 targets: Vec::leak(s.targets.iter().map(|t| target(t, i)).collect::<Vec<_>>()),
436 props: Vec::leak(
437 s.props
438 .iter()
439 .map(|p| &*Box::leak(Box::new(prop(p, i))))
440 .collect::<Vec<_>>(),
441 ),
442 closed: s.closed.as_ref().map(|c| &*Box::leak(Box::new(closed(c, i)))),
443 logic: Vec::leak(s.logic.iter().map(|l| logic(l, i)).collect::<Vec<_>>()),
444 profiles: i.intern_all(&s.profiles),
445 file: i.intern(&s.file),
446 }
447 }
448
449 fn target(t: &r::Target, i: &mut Interner) -> Target {
450 match t {
451 r::Target::Class(classes) => Target::Class(i.intern_all(classes)),
452 r::Target::SubjectsOf(f) => Target::SubjectsOf(i.intern(f)),
453 }
454 }
455
456 fn prop(p: &r::PropShape, i: &mut Interner) -> PropShape {
457 PropShape {
458 path: path(&p.path, i),
459 checks: Vec::leak(p.checks.iter().map(|c| check(c, i)).collect::<Vec<_>>()),
460 }
461 }
462
463 fn path(p: &r::Path, i: &mut Interner) -> SPath {
464 match p {
465 r::Path::Forward(f) => SPath::Forward(i.intern(f)),
466 r::Path::Inverse(f) => SPath::Inverse(i.intern(f)),
467 r::Path::Chain(steps) => SPath::Chain(Vec::leak(
468 steps
469 .iter()
470 .map(|s| match s {
471 r::Step::Forward(f) => Step::Forward(i.intern(f)),
472 r::Step::Inverse(f) => Step::Inverse(i.intern(f)),
473 r::Step::Type => Step::Type,
474 })
475 .collect::<Vec<_>>(),
476 )),
477 r::Path::Alternative(branches) => SPath::Alternative(Vec::leak(
478 branches
479 .iter()
480 .map(|b| match b {
481 r::AltBranch::Forward(f) => AltBranch::Forward(i.intern(f)),
482 r::AltBranch::Inverse(f) => AltBranch::Inverse(i.intern(f)),
483 })
484 .collect::<Vec<_>>(),
485 )),
486 }
487 }
488
489 fn check(c: &r::Check, i: &mut Interner) -> Check {
490 Check {
491 constraint: constraint(&c.constraint, i),
492 rule_id: i.intern(&c.rule_id),
493 name: i.intern(&c.name),
494 message: i.intern(&c.message),
495 description: i.intern(&c.description),
496 severity: i.intern(&c.severity),
497 }
498 }
499
500 fn constraint(c: &r::Constraint, i: &mut Interner) -> Constraint {
501 match c {
502 r::Constraint::MinCount(n) => Constraint::MinCount(*n),
503 r::Constraint::MaxCount(n) => Constraint::MaxCount(*n),
504 r::Constraint::MaxLength(n) => Constraint::MaxLength(*n),
505 r::Constraint::MinLength(n) => Constraint::MinLength(*n),
506 r::Constraint::Datatype(d) => Constraint::Datatype(i.intern(d)),
507 r::Constraint::HasValue(v) => Constraint::HasValue(i.intern(v)),
508 r::Constraint::NodeKind(k) => Constraint::NodeKind(match k {
509 r::NodeKind::Iri => NodeKind::Iri,
510 r::NodeKind::Literal => NodeKind::Literal,
511 r::NodeKind::BlankNode => NodeKind::BlankNode,
512 }),
513 r::Constraint::Class(v) => Constraint::Class(i.intern_all(v)),
514 r::Constraint::RefClass(v) => Constraint::RefClass(i.intern_all(v)),
515 r::Constraint::In(v) => Constraint::In(i.intern_all(v)),
516 r::Constraint::MinInclusive(v) => Constraint::MinInclusive(*v),
517 r::Constraint::MaxInclusive(v) => Constraint::MaxInclusive(*v),
518 r::Constraint::MinExclusive(v) => Constraint::MinExclusive(*v),
519 r::Constraint::MaxExclusive(v) => Constraint::MaxExclusive(*v),
520 r::Constraint::LessThan(f) => Constraint::LessThan(i.intern(f)),
521 r::Constraint::LessThanOrEquals(f) => Constraint::LessThanOrEquals(i.intern(f)),
522 r::Constraint::NotClass(v) => Constraint::NotClass(i.intern_all(v)),
523 r::Constraint::Length(n) => Constraint::Length(*n),
524 r::Constraint::QualifiedIn { allowed, min } => {
525 Constraint::QualifiedIn { allowed: i.intern_all(allowed), min: *min }
526 }
527 }
528 }
529
530 fn logic(l: &r::Logic, i: &mut Interner) -> Logic {
531 Logic {
532 op: match l.op {
533 r::LogicOp::And => LogicOp::And,
534 r::LogicOp::Or => LogicOp::Or,
535 r::LogicOp::Xone => LogicOp::Xone,
536 },
537 branches: Vec::leak(
538 l.branches
539 .iter()
540 .map(|b| Branch {
541 props: Vec::leak(b.props.iter().map(|p| prop(p, i)).collect::<Vec<_>>()),
542 negate: b.negate,
543 })
544 .collect::<Vec<_>>(),
545 ),
546 rule_id: i.intern(&l.rule_id),
547 name: i.intern(&l.name),
548 message: i.intern(&l.message),
549 description: i.intern(&l.description),
550 severity: i.intern(&l.severity),
551 }
552 }
553
554 fn closed(c: &r::ClosedShape, i: &mut Interner) -> ClosedShape {
555 ClosedShape {
556 allowed: i.intern_all(&c.allowed),
557 rule_id: i.intern(&c.rule_id),
558 name: i.intern(&c.name),
559 message: i.intern(&c.message),
560 description: i.intern(&c.description),
561 severity: i.intern(&c.severity),
562 }
563 }
564}