use crate::shapes::ShapeDef;
pub fn resolve(family: &'static str, generated: &'static [ShapeDef]) -> &'static [ShapeDef] {
#[cfg(feature = "dynamic-shapes")]
{
dynamic::resolve(family, generated)
}
#[cfg(not(feature = "dynamic-shapes"))]
{
let _ = family;
generated
}
}
pub fn resolve_profiles(
family: &'static str,
generated_iris: &'static [(&'static str, &'static str)],
generated_codes: &'static [&'static str],
) -> (&'static [(&'static str, &'static str)], &'static [&'static str]) {
#[cfg(feature = "dynamic-shapes")]
{
dynamic::resolve_profiles(family, generated_iris, generated_codes)
}
#[cfg(not(feature = "dynamic-shapes"))]
{
let _ = family;
(generated_iris, generated_codes)
}
}
#[cfg(feature = "dynamic-shapes")]
pub use dynamic::{family_of_dir, intern_shapes, load_from, load_table, ShapeError, SHACL_DIR_ENV};
#[cfg(feature = "dynamic-shapes")]
mod dynamic {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use cimschema::family::{self, Family};
use cimschema::shacl::resolve as r;
use cimschema::shacl::skip::SkipCollector;
use crate::shapes::{
AltBranch, Branch, Check, ClosedShape, Constraint, Logic, LogicOp, NodeKind, Path as SPath,
PropShape, ShapeDef, Step, Target,
};
pub const SHACL_DIR_ENV: &str = "CIMOXIDE_SHACL_DIR";
#[derive(Debug)]
pub enum ShapeError {
TooLate,
AlreadyLoaded,
UnknownFamily(String),
Schema(String),
Parse(String),
}
impl std::fmt::Display for ShapeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLate => write!(
f,
"the shape table is already resolved; load the shapes before validating"
),
Self::AlreadyLoaded => write!(f, "shapes are already loaded for this family"),
Self::UnknownFamily(id) => write!(f, "unknown family: {id}"),
Self::Schema(e) => write!(f, "cannot read the schema the shapes resolve against: {e}"),
Self::Parse(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for ShapeError {}
type Loaded = (
&'static [ShapeDef],
&'static [(&'static str, &'static str)],
&'static [&'static str],
);
fn explicit() -> &'static Mutex<HashMap<&'static str, Loaded>> {
static E: OnceLock<Mutex<HashMap<&'static str, Loaded>>> = OnceLock::new();
E.get_or_init(|| Mutex::new(HashMap::new()))
}
fn from_env() -> &'static Mutex<HashMap<&'static str, Option<Loaded>>> {
static E: OnceLock<Mutex<HashMap<&'static str, Option<Loaded>>>> = OnceLock::new();
E.get_or_init(|| Mutex::new(HashMap::new()))
}
static RESOLVED: AtomicBool = AtomicBool::new(false);
pub fn load_from(family_id: &str, dir: &Path) -> Result<(), ShapeError> {
let family = bag_family(family_id)?;
if RESOLVED.load(Ordering::SeqCst) {
return Err(ShapeError::TooLate);
}
let loaded = load(family, dir)?;
let mut map = explicit().lock().unwrap();
if map.contains_key(family.id) {
return Err(ShapeError::AlreadyLoaded);
}
map.insert(family.id, loaded);
Ok(())
}
pub fn load_table(family_id: &str, dir: &Path) -> Result<&'static [ShapeDef], ShapeError> {
let family = bag_family(family_id)?;
Ok(load(family, dir)?.0)
}
fn bag_family(id: &str) -> Result<&'static Family, ShapeError> {
family::by_id(id).ok_or_else(|| ShapeError::UnknownFamily(id.to_string()))
}
pub fn family_of_dir(dir: &Path) -> Option<&'static Family> {
if dir.join("Validation").is_dir() {
return family::by_id("nc");
}
family::FAMILIES.iter().copied().find(|f| {
f.shacl_manifest.is_some_and(|m| {
m.iter().any(|(stem, _)| dir.join(format!("{stem}.ttl")).is_file())
})
})
}
pub(super) fn resolve(
family_id: &'static str,
generated: &'static [ShapeDef],
) -> &'static [ShapeDef] {
env_loaded(family_id).map_or(generated, |l| l.0)
}
pub(super) fn resolve_profiles(
family_id: &'static str,
generated_iris: &'static [(&'static str, &'static str)],
generated_codes: &'static [&'static str],
) -> (&'static [(&'static str, &'static str)], &'static [&'static str]) {
env_loaded(family_id).map_or((generated_iris, generated_codes), |l| (l.1, l.2))
}
fn env_loaded(family_id: &'static str) -> Option<Loaded> {
RESOLVED.store(true, Ordering::SeqCst);
if let Some(l) = explicit().lock().unwrap().get(family_id) {
return Some(*l);
}
let mut cache = from_env().lock().unwrap();
if let Some(cached) = cache.get(family_id) {
return *cached;
}
let dir: Option<PathBuf> = std::env::var_os(SHACL_DIR_ENV).and_then(|dirs| {
let dirs: Vec<PathBuf> = std::env::split_paths(&dirs).collect();
static REPORTED: AtomicBool = AtomicBool::new(false);
if !REPORTED.swap(true, Ordering::SeqCst) {
for d in dirs.iter().filter(|d| family_of_dir(d).is_none()) {
eprintln!(
"warning: {SHACL_DIR_ENV} names {}, which holds no NC or CGMES \
constraint files; ignoring it",
d.display()
);
}
}
dirs.into_iter().find(|d| family_of_dir(d).is_some_and(|f| f.id == family_id))
});
let loaded = match (dir, bag_family(family_id)) {
(Some(dir), Ok(family)) => match load(family, &dir) {
Ok(l) => Some(l),
Err(e) => {
eprintln!(
"warning: {SHACL_DIR_ENV} names {} but the {family_id} shapes could not \
be loaded ({e}); falling back to the generated shape table",
dir.display()
);
None
}
},
_ => None,
};
cache.insert(family_id, loaded);
loaded
}
fn load(family: &'static Family, dir: &Path) -> Result<Loaded, ShapeError> {
if !dir.is_dir() {
return Err(ShapeError::Parse(format!(
"not a directory: {}",
dir.display()
)));
}
let (spec, others) = specs(family, dir)?;
let other_refs: Vec<&cimschema::model::CimSpecification> = others.iter().collect();
let mut collector = SkipCollector::new();
let table = r::load_shape_table(family, &spec, &other_refs, dir, &mut collector)
.map_err(|e| ShapeError::Parse(e.to_string()))?;
let mut interner = Interner::default();
let shapes: Vec<ShapeDef> = table
.shapes
.iter()
.map(|s| shape(s, &mut interner))
.collect();
let iris: Vec<(&'static str, &'static str)> = table
.profile_iris
.iter()
.map(|(iri, code)| (interner.intern(iri), interner.intern(code)))
.collect();
let codes: Vec<&'static str> =
table.profiles.iter().map(|c| interner.intern(c)).collect();
Ok((Vec::leak(shapes), Vec::leak(iris), Vec::leak(codes)))
}
fn specs(
family: &'static Family,
shacl_dir: &Path,
) -> Result<
(cimschema::model::CimSpecification, Vec<cimschema::model::CimSpecification>),
ShapeError,
> {
let import = |f: &'static Family, pattern: &str| {
cimschema::import::import_schema_files(pattern, f, false)
.map_err(|e| ShapeError::Schema(e.to_string()))
};
let root = shacl_dir.parent().and_then(Path::parent);
let spec = import(family, &schema_glob(family, shacl_dir, root))?;
let mut others = Vec::new();
for f in family::FAMILIES.iter().filter(|f| f.id != family.id) {
match import(f, &schema_glob(f, shacl_dir, root)) {
Ok(s) => others.push(s),
Err(e) => eprintln!(
"warning: the {} schema could not be read ({e}); its classes will be \
missing from {}'s cross-family value-type rules",
f.id, family.id
),
}
}
Ok((spec, others))
}
fn schema_glob(f: &'static Family, shacl_dir: &Path, root: Option<&Path>) -> String {
let file_name = |p: &str| {
Path::new(p)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "*.rdf".to_string())
};
if shacl_dir
.parent()
.and_then(|p| p.file_name())
.zip(Path::new(f.default_schema).parent().and_then(Path::parent).and_then(|p| p.file_name()))
.is_some_and(|(a, b)| a == b)
&& let Some(dir) = std::env::var_os(cimmodel::schema_source::RDFS_DIR_ENV) {
return PathBuf::from(dir)
.join(file_name(f.default_schema))
.to_string_lossy()
.into_owned();
}
let tail: Option<PathBuf> = Path::new(f.default_schema)
.components()
.skip(1)
.fold(None::<PathBuf>, |acc, c| {
Some(acc.map_or_else(|| PathBuf::from(c.as_os_str()), |p| p.join(c.as_os_str())))
});
match (root, tail) {
(Some(root), Some(tail)) => {
let candidate = root.join(tail);
if candidate
.parent()
.is_some_and(|p| p.is_dir())
{
return candidate.to_string_lossy().into_owned();
}
f.default_schema.to_string()
}
_ => f.default_schema.to_string(),
}
}
pub fn intern_shapes(shapes: &[r::ShapeDef]) -> &'static [ShapeDef] {
let mut interner = Interner::default();
Vec::leak(shapes.iter().map(|s| shape(s, &mut interner)).collect())
}
#[derive(Default)]
struct Interner {
seen: HashMap<String, &'static str>,
}
impl Interner {
fn intern(&mut self, s: &str) -> &'static str {
if let Some(v) = self.seen.get(s) {
return v;
}
let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
self.seen.insert(s.to_string(), leaked);
leaked
}
fn intern_all(&mut self, items: &[String]) -> &'static [&'static str] {
Vec::leak(items.iter().map(|s| self.intern(s)).collect::<Vec<_>>())
}
}
fn shape(s: &r::ShapeDef, i: &mut Interner) -> ShapeDef {
ShapeDef {
targets: Vec::leak(s.targets.iter().map(|t| target(t, i)).collect::<Vec<_>>()),
props: Vec::leak(
s.props
.iter()
.map(|p| &*Box::leak(Box::new(prop(p, i))))
.collect::<Vec<_>>(),
),
closed: s.closed.as_ref().map(|c| &*Box::leak(Box::new(closed(c, i)))),
logic: Vec::leak(s.logic.iter().map(|l| logic(l, i)).collect::<Vec<_>>()),
profiles: i.intern_all(&s.profiles),
file: i.intern(&s.file),
}
}
fn target(t: &r::Target, i: &mut Interner) -> Target {
match t {
r::Target::Class(classes) => Target::Class(i.intern_all(classes)),
r::Target::SubjectsOf(f) => Target::SubjectsOf(i.intern(f)),
}
}
fn prop(p: &r::PropShape, i: &mut Interner) -> PropShape {
PropShape {
path: path(&p.path, i),
checks: Vec::leak(p.checks.iter().map(|c| check(c, i)).collect::<Vec<_>>()),
}
}
fn path(p: &r::Path, i: &mut Interner) -> SPath {
match p {
r::Path::Forward(f) => SPath::Forward(i.intern(f)),
r::Path::Inverse(f) => SPath::Inverse(i.intern(f)),
r::Path::Chain(steps) => SPath::Chain(Vec::leak(
steps
.iter()
.map(|s| match s {
r::Step::Forward(f) => Step::Forward(i.intern(f)),
r::Step::Inverse(f) => Step::Inverse(i.intern(f)),
r::Step::Type => Step::Type,
})
.collect::<Vec<_>>(),
)),
r::Path::Alternative(branches) => SPath::Alternative(Vec::leak(
branches
.iter()
.map(|b| match b {
r::AltBranch::Forward(f) => AltBranch::Forward(i.intern(f)),
r::AltBranch::Inverse(f) => AltBranch::Inverse(i.intern(f)),
})
.collect::<Vec<_>>(),
)),
}
}
fn check(c: &r::Check, i: &mut Interner) -> Check {
Check {
constraint: constraint(&c.constraint, i),
rule_id: i.intern(&c.rule_id),
name: i.intern(&c.name),
message: i.intern(&c.message),
description: i.intern(&c.description),
severity: i.intern(&c.severity),
}
}
fn constraint(c: &r::Constraint, i: &mut Interner) -> Constraint {
match c {
r::Constraint::MinCount(n) => Constraint::MinCount(*n),
r::Constraint::MaxCount(n) => Constraint::MaxCount(*n),
r::Constraint::MaxLength(n) => Constraint::MaxLength(*n),
r::Constraint::MinLength(n) => Constraint::MinLength(*n),
r::Constraint::Datatype(d) => Constraint::Datatype(i.intern(d)),
r::Constraint::HasValue(v) => Constraint::HasValue(i.intern(v)),
r::Constraint::NodeKind(k) => Constraint::NodeKind(match k {
r::NodeKind::Iri => NodeKind::Iri,
r::NodeKind::Literal => NodeKind::Literal,
r::NodeKind::BlankNode => NodeKind::BlankNode,
}),
r::Constraint::Class(v) => Constraint::Class(i.intern_all(v)),
r::Constraint::RefClass(v) => Constraint::RefClass(i.intern_all(v)),
r::Constraint::In(v) => Constraint::In(i.intern_all(v)),
r::Constraint::MinInclusive(v) => Constraint::MinInclusive(*v),
r::Constraint::MaxInclusive(v) => Constraint::MaxInclusive(*v),
r::Constraint::MinExclusive(v) => Constraint::MinExclusive(*v),
r::Constraint::MaxExclusive(v) => Constraint::MaxExclusive(*v),
r::Constraint::LessThan(f) => Constraint::LessThan(i.intern(f)),
r::Constraint::LessThanOrEquals(f) => Constraint::LessThanOrEquals(i.intern(f)),
r::Constraint::NotClass(v) => Constraint::NotClass(i.intern_all(v)),
r::Constraint::Length(n) => Constraint::Length(*n),
r::Constraint::QualifiedIn { allowed, min } => {
Constraint::QualifiedIn { allowed: i.intern_all(allowed), min: *min }
}
}
}
fn logic(l: &r::Logic, i: &mut Interner) -> Logic {
Logic {
op: match l.op {
r::LogicOp::And => LogicOp::And,
r::LogicOp::Or => LogicOp::Or,
r::LogicOp::Xone => LogicOp::Xone,
},
branches: Vec::leak(
l.branches
.iter()
.map(|b| Branch {
props: Vec::leak(b.props.iter().map(|p| prop(p, i)).collect::<Vec<_>>()),
negate: b.negate,
})
.collect::<Vec<_>>(),
),
rule_id: i.intern(&l.rule_id),
name: i.intern(&l.name),
message: i.intern(&l.message),
description: i.intern(&l.description),
severity: i.intern(&l.severity),
}
}
fn closed(c: &r::ClosedShape, i: &mut Interner) -> ClosedShape {
ClosedShape {
allowed: i.intern_all(&c.allowed),
rule_id: i.intern(&c.rule_id),
name: i.intern(&c.name),
message: i.intern(&c.message),
description: i.intern(&c.description),
severity: i.intern(&c.severity),
}
}
}