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 (sparql, mut v) = par::par_two(
253 || sparql::validate_crossprofile(dataset, cfg),
254 || validate_crossprofile_shacl(dataset, cfg),
255 );
256 v.extend(sparql);
257 let cgmes = cgmes_profile_index().1;
260 if cfg.profiles.iter().any(|p| !cgmes.contains(&p.as_str()) && nc_profile_index().1.contains(&p.as_str())) {
261 v.extend(validate_nc_merged(dataset, cfg));
262 }
263 v
264}
265
266pub fn combined_config(
269 per_file: &[cimmodel::CimDataset],
270 profiles: Option<Vec<String>>,
271 solved: Option<bool>,
272 common: bool,
273 quality: bool,
274 silenced_rules: Vec<String>,
275) -> Config {
276 let mut cfg = Config::default();
277 for ds in per_file {
278 let c = detect_config(ds);
279 for p in c.profiles.into_iter().chain(detect_nc_profiles(ds)) {
280 if !cfg.profiles.contains(&p) {
281 cfg.profiles.push(p);
282 }
283 }
284 cfg.solved |= c.solved;
285 }
286 cfg.not_solved = !cfg.solved;
287 if let Some(p) = profiles { cfg.profiles = p; }
288 if let Some(s) = solved { cfg.solved = s; cfg.not_solved = !s; }
289 cfg.common = common;
290 cfg.quality = quality;
291 cfg.silenced_rules = silenced_rules;
292 cfg
293}
294
295pub fn validate_files(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<Violation> {
300 validate_files_traced(per_file, cfg).into_iter().map(|(_, v)| v).collect()
301}
302
303pub fn validate_files_traced(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<(Option<usize>, Violation)> {
308 let set = per_file.as_slice();
309 let mut violations: Vec<(Option<usize>, Violation)> = par::par_each(set, |ds| {
310 let mut v = validate_header(ds, cfg);
311 let mut profiles = detect_config(ds).profiles;
312 profiles.extend(detect_nc_profiles(ds));
313 for profile in &profiles {
314 v.extend(validate_profile_local_in(ds, set, profile, cfg));
315 }
316 v
317 })
318 .into_iter()
319 .enumerate()
320 .flat_map(|(i, found)| found.into_iter().map(move |v| (Some(i), v)))
321 .collect();
322
323 let mut merged = cimmodel::CimDataset::new();
324 for ds in per_file {
325 merged.merge(ds);
326 }
327 violations.extend(validate_crossprofile(&merged, cfg).into_iter().map(|v| (None, v)));
328
329 if !cfg.silenced_rules.is_empty() {
330 let silenced: HashSet<&str> = cfg.silenced_rules.iter().map(String::as_str).collect();
331 violations.retain(|(_, v)| !silenced.contains(v.rule_id.as_str()));
332 }
333 violations
334}