1pub mod violation;
2pub use violation::Violation;
3
4pub mod sparql;
5pub mod detect;
6pub use detect::{detect_config, detect_nc_profiles};
7
8use std::collections::HashSet;
9
10#[derive(Debug, Default, Clone)]
11pub struct Config {
12 pub profiles: Vec<String>,
14 pub solved: bool,
16 pub not_solved: bool,
18 pub common: bool,
20 pub quality: bool,
22 pub silenced_rules: Vec<String>,
24 pub eqbd_base_voltage_ids: Option<HashSet<String>>,
26}
27
28pub mod helpers;
29mod par;
30
31pub mod bag;
36pub mod shapes;
37pub mod shape_source;
38pub mod nc_shapes;
39pub mod nc_profiles;
40
41pub fn nc_shapes() -> &'static [shapes::ShapeDef] {
44 static R: std::sync::OnceLock<&'static [shapes::ShapeDef]> = std::sync::OnceLock::new();
45 R.get_or_init(|| shape_source::resolve("nc", nc_shapes::SHAPES))
46}
47
48pub type ProfileIndex = (&'static [(&'static str, &'static str)], &'static [&'static str]);
50
51pub fn nc_profile_index() -> ProfileIndex {
54 static R: std::sync::OnceLock<ProfileIndex> = std::sync::OnceLock::new();
55 *R.get_or_init(|| {
56 shape_source::resolve_profiles("nc", nc_profiles::PROFILE_IRIS, nc_profiles::PROFILES)
57 })
58}
59
60pub fn validate_nc_profile(
66 dataset: &cimmodel::CimDataset,
67 profile: &str,
68 cfg: &Config,
69) -> Vec<Violation> {
70 validate_nc_profile_in(dataset, &[], profile, cfg)
71}
72
73pub fn validate_nc_profile_in(
75 dataset: &cimmodel::CimDataset,
76 set: &[cimmodel::CimDataset],
77 profile: &str,
78 cfg: &Config,
79) -> Vec<Violation> {
80 let mut v = bag::validate_profile_in(dataset, set, profile, nc_shapes(), cfg);
81 if nc_profile_index().1.contains(&profile) {
84 v.extend(sparql::nc::validate_local(dataset));
85 }
86 v
87}
88
89pub fn validate_nc_merged(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
93 let mut v = bag::validate_profile(dataset, MERGED_PROFILE, nc_shapes(), cfg);
94 v.extend(sparql::nc::validate_merged(dataset));
95 v
96}
97
98pub const MERGED_PROFILE: &str = "ALL";
103
104
105pub mod cgmes_shapes;
113pub mod cgmes_profiles;
114
115pub fn cgmes_shapes() -> &'static [shapes::ShapeDef] {
118 static R: std::sync::OnceLock<&'static [shapes::ShapeDef]> = std::sync::OnceLock::new();
119 R.get_or_init(|| shape_source::resolve("cgmes", cgmes_shapes::SHAPES))
120}
121
122fn cgmes_tagged(tag: &str) -> &'static [&'static shapes::ShapeDef] {
124 type Index = std::collections::HashMap<&'static str, Vec<&'static shapes::ShapeDef>>;
125 static IDX: std::sync::OnceLock<Index> = std::sync::OnceLock::new();
126 let idx = IDX.get_or_init(|| {
127 let mut idx = Index::new();
128 for shape in cgmes_shapes() {
129 for tag in shape.profiles {
130 idx.entry(*tag).or_default().push(shape);
131 }
132 }
133 idx
134 });
135 idx.get(tag).map_or(&[], Vec::as_slice)
136}
137
138fn run_cgmes(dataset: &cimmodel::CimDataset, active: &[&shapes::ShapeDef]) -> Vec<Violation> {
139 bag::validate_shapes(dataset, bag::Source::Cgmes, active)
140}
141
142fn run_cgmes_in(
143 dataset: &cimmodel::CimDataset,
144 set: &[cimmodel::CimDataset],
145 active: &[&shapes::ShapeDef],
146) -> Vec<Violation> {
147 bag::validate_shapes_in(dataset, set, bag::Source::Cgmes, active)
148}
149
150pub fn validate_header(dataset: &cimmodel::CimDataset, _cfg: &Config) -> Vec<Violation> {
154 run_cgmes(dataset, cgmes_tagged("HDR"))
155}
156
157pub fn validate_profile_shacl(dataset: &cimmodel::CimDataset, profile: &str, _cfg: &Config) -> Vec<Violation> {
161 run_cgmes(dataset, cgmes_tagged(profile))
162}
163
164pub fn validate_crossprofile_shacl(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
170 let has = |p: &str| cfg.profiles.is_empty() || cfg.profiles.iter().any(|x| x == p);
171 let mut active: Vec<&shapes::ShapeDef> = cimschema_cross_profiles()
172 .iter()
173 .filter(|p| has(p))
174 .flat_map(|p| cgmes_tagged(&format!("X:{p}")).iter().copied())
175 .collect();
176 if cfg.not_solved {
177 for p in cgmes_profile_index().1.iter().filter(|p| has(p)) {
178 active.extend_from_slice(cgmes_tagged(&format!("{p}!NS")));
179 }
180 }
181 if cfg.common {
182 active.extend_from_slice(cgmes_tagged("COMMON"));
183 }
184 run_cgmes(dataset, &active)
185}
186
187pub fn cgmes_profile_index() -> ProfileIndex {
191 static R: std::sync::OnceLock<ProfileIndex> = std::sync::OnceLock::new();
192 *R.get_or_init(|| {
193 shape_source::resolve_profiles("cgmes", cgmes_profiles::PROFILE_IRIS, cgmes_profiles::PROFILES)
194 })
195}
196
197fn cimschema_cross_profiles() -> &'static [&'static str] {
201 &["DL", "DY", "GL", "OP", "SV", "TP"]
202}
203
204
205pub fn validate_profile_local(dataset: &cimmodel::CimDataset, profile: &str, cfg: &Config) -> Vec<Violation> {
213 validate_profile_local_in(dataset, &[], profile, cfg)
214}
215
216pub fn validate_profile_local_in(
225 dataset: &cimmodel::CimDataset,
226 set: &[cimmodel::CimDataset],
227 profile: &str,
228 cfg: &Config,
229) -> Vec<Violation> {
230 if !cfg.profiles.is_empty() && !cfg.profiles.iter().any(|p| p == profile) {
231 return Vec::new();
232 }
233 if !cgmes_profile_index().1.contains(&profile) && nc_profile_index().1.contains(&profile) {
238 return validate_nc_profile_in(dataset, set, profile, cfg);
239 }
240
241 let mut v = run_cgmes_in(dataset, set, cgmes_tagged(profile));
242 v.extend(sparql::validate_profile_local(dataset, profile, cfg));
243 v
244}
245
246
247pub fn validate_crossprofile(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
252 let (mut v, sparql) = std::thread::scope(|s| {
253 let sparql = s.spawn(|| sparql::validate_crossprofile(dataset, cfg));
254 let shacl = validate_crossprofile_shacl(dataset, cfg);
255 (shacl, sparql.join().expect("validation thread panicked"))
256 });
257 v.extend(sparql);
258 let cgmes = cgmes_profile_index().1;
261 if cfg.profiles.iter().any(|p| !cgmes.contains(&p.as_str()) && nc_profile_index().1.contains(&p.as_str())) {
262 v.extend(validate_nc_merged(dataset, cfg));
263 }
264 v
265}
266
267pub fn combined_config(
270 per_file: &[cimmodel::CimDataset],
271 profiles: Option<Vec<String>>,
272 solved: Option<bool>,
273 common: bool,
274 quality: bool,
275 silenced_rules: Vec<String>,
276) -> Config {
277 let mut cfg = Config::default();
278 for ds in per_file {
279 let c = detect_config(ds);
280 for p in c.profiles.into_iter().chain(detect_nc_profiles(ds)) {
281 if !cfg.profiles.contains(&p) {
282 cfg.profiles.push(p);
283 }
284 }
285 cfg.solved |= c.solved;
286 }
287 cfg.not_solved = !cfg.solved;
288 if let Some(p) = profiles { cfg.profiles = p; }
289 if let Some(s) = solved { cfg.solved = s; cfg.not_solved = !s; }
290 cfg.common = common;
291 cfg.quality = quality;
292 cfg.silenced_rules = silenced_rules;
293 cfg
294}
295
296pub fn validate_files(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<Violation> {
301 validate_files_traced(per_file, cfg).into_iter().map(|(_, v)| v).collect()
302}
303
304pub fn validate_files_traced(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<(Option<usize>, Violation)> {
309 let set = per_file.as_slice();
310 let mut violations: Vec<(Option<usize>, Violation)> = std::thread::scope(|s| {
311 set
312 .iter()
313 .map(|ds| {
314 s.spawn(move || {
315 let mut v = validate_header(ds, cfg);
316 let mut profiles = detect_config(ds).profiles;
317 profiles.extend(detect_nc_profiles(ds));
318 for profile in &profiles {
319 v.extend(validate_profile_local_in(ds, set, profile, cfg));
320 }
321 v
322 })
323 })
324 .collect::<Vec<_>>()
325 .into_iter()
326 .enumerate()
327 .flat_map(|(i, h)| h.join().expect("validation thread panicked").into_iter().map(move |v| (Some(i), v)))
328 .collect()
329 });
330
331 let mut merged = cimmodel::CimDataset::new();
332 for ds in per_file {
333 merged.merge(ds);
334 }
335 violations.extend(validate_crossprofile(&merged, cfg).into_iter().map(|v| (None, v)));
336
337 if !cfg.silenced_rules.is_empty() {
338 let silenced: HashSet<&str> = cfg.silenced_rules.iter().map(String::as_str).collect();
339 violations.retain(|(_, v)| !silenced.contains(v.rule_id.as_str()));
340 }
341 violations
342}