use crate::base::Schema;
pub fn resolve(family: &'static str, generated: &'static Schema) -> &'static Schema {
#[cfg(feature = "dynamic-schema")]
{
dynamic::resolve(family, generated)
}
#[cfg(not(feature = "dynamic-schema"))]
{
let _ = family;
generated
}
}
#[cfg(feature = "dynamic-schema")]
pub use dynamic::{family_of_dir, load_from, load_table, SchemaError, RDFS_DIR_ENV};
#[cfg(feature = "dynamic-schema")]
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::model::{CimAttribute, CimSpecification};
use crate::base::{AttrDef, AttrKind, ClassDef, EnumDef, EnumValueDef, Schema};
pub const RDFS_DIR_ENV: &str = "CIMOXIDE_RDFS_DIR";
#[derive(Debug)]
pub enum SchemaError {
TooLate,
AlreadyLoaded,
UnknownFamily(String),
Parse(String),
}
impl std::fmt::Display for SchemaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLate => write!(
f,
"the type registry is already built; load the schema before decoding"
),
Self::AlreadyLoaded => write!(f, "a schema is already loaded for this family"),
Self::UnknownFamily(id) => write!(f, "unknown family: {id}"),
Self::Parse(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SchemaError {}
fn explicit() -> &'static Mutex<HashMap<&'static str, &'static Schema>> {
static E: OnceLock<Mutex<HashMap<&'static str, &'static Schema>>> = 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<(), SchemaError> {
let family = family::by_id(family_id)
.ok_or_else(|| SchemaError::UnknownFamily(family_id.to_string()))?;
if RESOLVED.load(Ordering::SeqCst) {
return Err(SchemaError::TooLate);
}
let classes = load_table(family.id, dir)?;
let mut map = explicit().lock().unwrap();
if map.contains_key(family.id) {
return Err(SchemaError::AlreadyLoaded);
}
map.insert(family.id, classes);
Ok(())
}
pub(super) fn resolve(
family_id: &'static str,
generated: &'static Schema,
) -> &'static Schema {
RESOLVED.store(true, Ordering::SeqCst);
if let Some(classes) = explicit().lock().unwrap().get(family_id) {
return classes;
}
let Some(dirs) = env_dirs() else {
return generated;
};
let Some(family) = family::by_id(family_id) else {
return generated;
};
let Some(dir) = dirs.iter().find(|d| family_of_dir(d).is_some_and(|f| f.id == family.id)) else {
return generated;
};
match load_family(family, dir) {
Ok(classes) => classes,
Err(e) => {
eprintln!(
"warning: {RDFS_DIR_ENV} set but the {family_id} schema could not be \
loaded ({e}); falling back to the generated class table"
);
generated
}
}
}
fn env_dirs() -> Option<&'static [PathBuf]> {
static DIRS: OnceLock<Option<Vec<PathBuf>>> = OnceLock::new();
DIRS.get_or_init(|| {
let value = std::env::var_os(RDFS_DIR_ENV)?;
let dirs: Vec<PathBuf> = std::env::split_paths(&value).collect();
for d in &dirs {
if family_of_dir(d).is_none() {
eprintln!(
"warning: {RDFS_DIR_ENV} lists {}, which holds no CGMES or NC RDFS \
vocabularies; ignoring it",
d.display()
);
}
}
Some(dirs)
})
.as_deref()
}
pub fn family_of_dir(dir: &Path) -> Option<&'static Family> {
let names: Vec<String> = std::fs::read_dir(dir)
.ok()?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
let matches = |f: &Family| {
let pattern = Path::new(f.default_schema)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let (prefix, suffix) = pattern.split_once('*').unwrap_or((&pattern, ""));
names.iter().any(|n| n.starts_with(prefix) && n.ends_with(suffix))
};
[&family::CGMES, &family::NC].into_iter().find(|f| matches(f))
}
pub fn load_table(family_id: &str, dir: &Path) -> Result<&'static Schema, SchemaError> {
let family = family::by_id(family_id)
.ok_or_else(|| SchemaError::UnknownFamily(family_id.to_string()))?;
load_family(family, dir)
}
fn load_family(
family: &'static Family,
dir: &Path,
) -> Result<&'static Schema, SchemaError> {
let file_pattern = Path::new(family.default_schema)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "*.rdf".to_string());
let pattern = dir.join(file_pattern);
let spec = cimschema::import::import_schema_files(&pattern.to_string_lossy(), family, false)
.map_err(|e| SchemaError::Parse(e.to_string()))?;
Ok(build(&spec))
}
#[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] {
let v: Vec<&'static str> = items.iter().map(|s| self.intern(s)).collect();
Vec::leak(v)
}
}
fn attr_kind(a: &CimAttribute) -> AttrKind {
match cimschema::table::attr_kind(a) {
cimschema::table::Kind::Literal => AttrKind::Literal,
cimschema::table::Kind::Association => AttrKind::Association,
cimschema::table::Kind::Enum => AttrKind::Enum,
}
}
fn build(spec: &CimSpecification) -> &'static Schema {
let mut ids: Vec<&String> = spec.types.keys().collect();
ids.sort();
let index_of: HashMap<&str, usize> = ids
.iter()
.enumerate()
.map(|(i, id)| (id.as_str(), i))
.collect();
let mut interner = Interner::default();
let mut out: Vec<ClassDef> = Vec::with_capacity(ids.len());
for id in &ids {
let t = &spec.types[*id];
let attrs: Vec<AttrDef> = t
.attributes
.iter()
.map(|a| AttrDef {
id: interner.intern(&a.id),
ns: interner.intern(&a.namespace),
kind: attr_kind(a),
range: interner.intern(cimschema::table::attr_range(a)),
is_list: a.is_list,
used: a.is_association_used,
xsd: cimschema::table::xsd_type(a),
value_ns: interner.intern(cimschema::table::value_namespace(spec, a)),
origins: interner.intern_all(&a.origins),
comment: interner.intern(&a.comment),
})
.collect();
let qualified = format!("{}{}", spec.family.type_prefix, t.id);
out.push(ClassDef {
ns: interner.intern(&t.namespace),
local: interner.intern(&t.id),
qualified: interner.intern(&qualified),
super_class: index_of.get(t.super_type.as_str()).copied(),
concrete: !t.concrete_in.is_empty(),
attrs: Vec::leak(attrs),
origins: interner.intern_all(&t.origins),
comment: interner.intern(&t.comment),
});
}
let pairs = |interner: &mut Interner, v: Vec<(&str, &str)>| -> &'static [(&'static str, &'static str)] {
Vec::leak(v.into_iter().map(|(a, b)| (interner.intern(a), interner.intern(b))).collect())
};
let profiles = pairs(&mut interner, cimschema::table::profile_uris(spec));
let namespaces = pairs(&mut interner, cimschema::table::namespaces(spec));
let enums: Vec<EnumDef> = cimschema::table::enums(spec)
.into_iter()
.map(|e| EnumDef {
ns: interner.intern(&e.namespace),
local: interner.intern(&e.id),
comment: interner.intern(&e.comment),
values: Vec::leak(
e.values
.iter()
.map(|v| EnumValueDef { id: interner.intern(&v.id), comment: interner.intern(&v.comment) })
.collect(),
),
})
.collect();
Box::leak(Box::new(Schema { classes: Vec::leak(out), profiles, namespaces, enums: Vec::leak(enums) }))
}
}