1use crate::base::Schema;
21
22pub fn resolve(family: &'static str, generated: &'static Schema) -> &'static Schema {
26 #[cfg(feature = "dynamic-schema")]
27 {
28 dynamic::resolve(family, generated)
29 }
30 #[cfg(not(feature = "dynamic-schema"))]
31 {
32 let _ = family;
33 generated
34 }
35}
36
37#[cfg(feature = "dynamic-schema")]
38pub use dynamic::{family_of_dir, load_from, load_table, SchemaError, RDFS_DIR_ENV};
39
40#[cfg(feature = "dynamic-schema")]
41mod dynamic {
42 use std::collections::HashMap;
43 use std::path::{Path, PathBuf};
44 use std::sync::atomic::{AtomicBool, Ordering};
45 use std::sync::{Mutex, OnceLock};
46
47 use cimschema::family::{self, Family};
48 use cimschema::model::{CimAttribute, CimSpecification};
49
50 use crate::base::{AttrDef, AttrKind, ClassDef, EnumDef, EnumValueDef, Schema};
51
52 pub const RDFS_DIR_ENV: &str = "CIMOXIDE_RDFS_DIR";
55
56 #[derive(Debug)]
57 pub enum SchemaError {
58 TooLate,
60 AlreadyLoaded,
62 UnknownFamily(String),
64 Parse(String),
65 }
66
67 impl std::fmt::Display for SchemaError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::TooLate => write!(
71 f,
72 "the type registry is already built; load the schema before decoding"
73 ),
74 Self::AlreadyLoaded => write!(f, "a schema is already loaded for this family"),
75 Self::UnknownFamily(id) => write!(f, "unknown family: {id}"),
76 Self::Parse(e) => write!(f, "{e}"),
77 }
78 }
79 }
80
81 impl std::error::Error for SchemaError {}
82
83 fn explicit() -> &'static Mutex<HashMap<&'static str, &'static Schema>> {
84 static E: OnceLock<Mutex<HashMap<&'static str, &'static Schema>>> = OnceLock::new();
85 E.get_or_init(|| Mutex::new(HashMap::new()))
86 }
87
88 static RESOLVED: AtomicBool = AtomicBool::new(false);
91
92 pub fn load_from(family_id: &str, dir: &Path) -> Result<(), SchemaError> {
98 let family = family::by_id(family_id)
99
100 .ok_or_else(|| SchemaError::UnknownFamily(family_id.to_string()))?;
101 if RESOLVED.load(Ordering::SeqCst) {
102 return Err(SchemaError::TooLate);
103 }
104 let classes = load_table(family.id, dir)?;
105 let mut map = explicit().lock().unwrap();
106 if map.contains_key(family.id) {
107 return Err(SchemaError::AlreadyLoaded);
108 }
109 map.insert(family.id, classes);
110 Ok(())
111 }
112
113 pub(super) fn resolve(
114 family_id: &'static str,
115 generated: &'static Schema,
116 ) -> &'static Schema {
117 RESOLVED.store(true, Ordering::SeqCst);
118
119 if let Some(classes) = explicit().lock().unwrap().get(family_id) {
120 return classes;
121 }
122
123 let Some(dirs) = env_dirs() else {
124 return generated;
125 };
126 let Some(family) = family::by_id(family_id) else {
127 return generated;
128 };
129 let Some(dir) = dirs.iter().find(|d| family_of_dir(d).is_some_and(|f| f.id == family.id)) else {
130 return generated;
131 };
132 match load_family(family, dir) {
133 Ok(classes) => classes,
134 Err(e) => {
135 eprintln!(
136 "warning: {RDFS_DIR_ENV} set but the {family_id} schema could not be \
137 loaded ({e}); falling back to the generated class table"
138 );
139 generated
140 }
141 }
142 }
143
144 fn env_dirs() -> Option<&'static [PathBuf]> {
147 static DIRS: OnceLock<Option<Vec<PathBuf>>> = OnceLock::new();
148 DIRS.get_or_init(|| {
149 let value = std::env::var_os(RDFS_DIR_ENV)?;
150 let dirs: Vec<PathBuf> = std::env::split_paths(&value).collect();
151 for d in &dirs {
152 if family_of_dir(d).is_none() {
153 eprintln!(
154 "warning: {RDFS_DIR_ENV} lists {}, which holds no CGMES or NC RDFS \
155 vocabularies; ignoring it",
156 d.display()
157 );
158 }
159 }
160 Some(dirs)
161 })
162 .as_deref()
163 }
164
165 pub fn family_of_dir(dir: &Path) -> Option<&'static Family> {
169 let names: Vec<String> = std::fs::read_dir(dir)
170 .ok()?
171 .filter_map(|e| e.ok())
172 .map(|e| e.file_name().to_string_lossy().into_owned())
173 .collect();
174 let matches = |f: &Family| {
175 let pattern = Path::new(f.default_schema)
176 .file_name()
177 .map(|n| n.to_string_lossy().into_owned())
178 .unwrap_or_default();
179 let (prefix, suffix) = pattern.split_once('*').unwrap_or((&pattern, ""));
180 names.iter().any(|n| n.starts_with(prefix) && n.ends_with(suffix))
181 };
182 [&family::CGMES, &family::NC].into_iter().find(|f| matches(f))
183 }
184
185 pub fn load_table(family_id: &str, dir: &Path) -> Result<&'static Schema, SchemaError> {
190 let family = family::by_id(family_id)
191
192 .ok_or_else(|| SchemaError::UnknownFamily(family_id.to_string()))?;
193 load_family(family, dir)
194 }
195
196 fn load_family(
197 family: &'static Family,
198 dir: &Path,
199 ) -> Result<&'static Schema, SchemaError> {
200 let file_pattern = Path::new(family.default_schema)
203 .file_name()
204 .map(|n| n.to_string_lossy().into_owned())
205 .unwrap_or_else(|| "*.rdf".to_string());
206 let pattern = dir.join(file_pattern);
207
208 let spec = cimschema::import::import_schema_files(&pattern.to_string_lossy(), family, false)
209 .map_err(|e| SchemaError::Parse(e.to_string()))?;
210 Ok(build(&spec))
211 }
212
213 #[derive(Default)]
217 struct Interner {
218 seen: HashMap<String, &'static str>,
219 }
220
221 impl Interner {
222 fn intern(&mut self, s: &str) -> &'static str {
223 if let Some(v) = self.seen.get(s) {
224 return v;
225 }
226 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
227 self.seen.insert(s.to_string(), leaked);
228 leaked
229 }
230
231 fn intern_all(&mut self, items: &[String]) -> &'static [&'static str] {
232 let v: Vec<&'static str> = items.iter().map(|s| self.intern(s)).collect();
233 Vec::leak(v)
234 }
235 }
236
237 fn attr_kind(a: &CimAttribute) -> AttrKind {
241 match cimschema::table::attr_kind(a) {
242 cimschema::table::Kind::Literal => AttrKind::Literal,
243 cimschema::table::Kind::Association => AttrKind::Association,
244 cimschema::table::Kind::Enum => AttrKind::Enum,
245 }
246 }
247
248 fn build(spec: &CimSpecification) -> &'static Schema {
249 let mut ids: Vec<&String> = spec.types.keys().collect();
252 ids.sort();
253 let index_of: HashMap<&str, usize> = ids
254 .iter()
255 .enumerate()
256 .map(|(i, id)| (id.as_str(), i))
257 .collect();
258
259 let mut interner = Interner::default();
260 let mut out: Vec<ClassDef> = Vec::with_capacity(ids.len());
261
262 for id in &ids {
263 let t = &spec.types[*id];
264 let attrs: Vec<AttrDef> = t
265 .attributes
266 .iter()
267 .map(|a| AttrDef {
268 id: interner.intern(&a.id),
269 ns: interner.intern(&a.namespace),
270 kind: attr_kind(a),
271 range: interner.intern(cimschema::table::attr_range(a)),
272 is_list: a.is_list,
273 used: a.is_association_used,
274 xsd: cimschema::table::xsd_type(a),
275 value_ns: interner.intern(cimschema::table::value_namespace(spec, a)),
276 origins: interner.intern_all(&a.origins),
277 comment: interner.intern(&a.comment),
278 })
279 .collect();
280
281 let qualified = format!("{}{}", spec.family.type_prefix, t.id);
282 out.push(ClassDef {
283 ns: interner.intern(&t.namespace),
284 local: interner.intern(&t.id),
285 qualified: interner.intern(&qualified),
286 super_class: index_of.get(t.super_type.as_str()).copied(),
287 concrete: !t.concrete_in.is_empty(),
288 attrs: Vec::leak(attrs),
289 origins: interner.intern_all(&t.origins),
290 comment: interner.intern(&t.comment),
291 });
292 }
293
294 let pairs = |interner: &mut Interner, v: Vec<(&str, &str)>| -> &'static [(&'static str, &'static str)] {
295 Vec::leak(v.into_iter().map(|(a, b)| (interner.intern(a), interner.intern(b))).collect())
296 };
297 let profiles = pairs(&mut interner, cimschema::table::profile_uris(spec));
298 let namespaces = pairs(&mut interner, cimschema::table::namespaces(spec));
299 let enums: Vec<EnumDef> = cimschema::table::enums(spec)
300 .into_iter()
301 .map(|e| EnumDef {
302 ns: interner.intern(&e.namespace),
303 local: interner.intern(&e.id),
304 comment: interner.intern(&e.comment),
305 values: Vec::leak(
306 e.values
307 .iter()
308 .map(|v| EnumValueDef { id: interner.intern(&v.id), comment: interner.intern(&v.comment) })
309 .collect(),
310 ),
311 })
312 .collect();
313 Box::leak(Box::new(Schema { classes: Vec::leak(out), profiles, namespaces, enums: Vec::leak(enums) }))
314 }
315}