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 let mut v = bag::validate_profile(dataset, profile, nc_shapes(), cfg);
71 if nc_profile_index().1.contains(&profile) {
74 v.extend(sparql::nc::validate_local(dataset));
75 }
76 v
77}
78
79pub fn validate_nc_merged(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
83 let mut v = bag::validate_profile(dataset, MERGED_PROFILE, nc_shapes(), cfg);
84 v.extend(sparql::nc::validate_merged(dataset));
85 v
86}
87
88pub const MERGED_PROFILE: &str = "ALL";
93
94
95pub mod cgmes_shapes;
103pub mod cgmes_profiles;
104
105pub fn cgmes_shapes() -> &'static [shapes::ShapeDef] {
108 static R: std::sync::OnceLock<&'static [shapes::ShapeDef]> = std::sync::OnceLock::new();
109 R.get_or_init(|| shape_source::resolve("cgmes", cgmes_shapes::SHAPES))
110}
111
112fn cgmes_tagged(tag: &str) -> &'static [&'static shapes::ShapeDef] {
114 type Index = std::collections::HashMap<&'static str, Vec<&'static shapes::ShapeDef>>;
115 static IDX: std::sync::OnceLock<Index> = std::sync::OnceLock::new();
116 let idx = IDX.get_or_init(|| {
117 let mut idx = Index::new();
118 for shape in cgmes_shapes() {
119 for tag in shape.profiles {
120 idx.entry(*tag).or_default().push(shape);
121 }
122 }
123 idx
124 });
125 idx.get(tag).map_or(&[], Vec::as_slice)
126}
127
128fn run_cgmes(dataset: &cimmodel::CimDataset, active: &[&shapes::ShapeDef]) -> Vec<Violation> {
129 bag::validate_shapes(dataset, bag::Source::Cgmes, active)
130}
131
132pub fn validate_header(dataset: &cimmodel::CimDataset, _cfg: &Config) -> Vec<Violation> {
136 run_cgmes(dataset, cgmes_tagged("HDR"))
137}
138
139pub fn validate_profile_shacl(dataset: &cimmodel::CimDataset, profile: &str, _cfg: &Config) -> Vec<Violation> {
143 run_cgmes(dataset, cgmes_tagged(profile))
144}
145
146pub fn validate_crossprofile_shacl(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
152 let has = |p: &str| cfg.profiles.is_empty() || cfg.profiles.iter().any(|x| x == p);
153 let mut active: Vec<&shapes::ShapeDef> = cimschema_cross_profiles()
154 .iter()
155 .filter(|p| has(p))
156 .flat_map(|p| cgmes_tagged(&format!("X:{p}")).iter().copied())
157 .collect();
158 if cfg.not_solved {
159 for p in cgmes_profile_index().1.iter().filter(|p| has(p)) {
160 active.extend_from_slice(cgmes_tagged(&format!("{p}!NS")));
161 }
162 }
163 if cfg.common {
164 active.extend_from_slice(cgmes_tagged("COMMON"));
165 }
166 run_cgmes(dataset, &active)
167}
168
169pub fn cgmes_profile_index() -> ProfileIndex {
173 static R: std::sync::OnceLock<ProfileIndex> = std::sync::OnceLock::new();
174 *R.get_or_init(|| {
175 shape_source::resolve_profiles("cgmes", cgmes_profiles::PROFILE_IRIS, cgmes_profiles::PROFILES)
176 })
177}
178
179fn cimschema_cross_profiles() -> &'static [&'static str] {
183 &["DL", "DY", "GL", "OP", "SV", "TP"]
184}
185
186
187pub fn validate_profile_local(dataset: &cimmodel::CimDataset, profile: &str, cfg: &Config) -> Vec<Violation> {
195 if !cfg.profiles.is_empty() && !cfg.profiles.iter().any(|p| p == profile) {
196 return Vec::new();
197 }
198 if !cgmes_profile_index().1.contains(&profile) && nc_profile_index().1.contains(&profile) {
203 return validate_nc_profile(dataset, profile, cfg);
204 }
205
206 let mut v = validate_profile_shacl(dataset, profile, cfg);
207 v.extend(sparql::validate_profile_local(dataset, profile, cfg));
208 v
209}
210
211
212pub fn validate_crossprofile(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
217 let (mut v, sparql) = std::thread::scope(|s| {
218 let sparql = s.spawn(|| sparql::validate_crossprofile(dataset, cfg));
219 let shacl = validate_crossprofile_shacl(dataset, cfg);
220 (shacl, sparql.join().expect("validation thread panicked"))
221 });
222 v.extend(sparql);
223 let cgmes = cgmes_profile_index().1;
226 if cfg.profiles.iter().any(|p| !cgmes.contains(&p.as_str()) && nc_profile_index().1.contains(&p.as_str())) {
227 v.extend(validate_nc_merged(dataset, cfg));
228 }
229 v
230}
231
232pub fn combined_config(
235 per_file: &[cimmodel::CimDataset],
236 profiles: Option<Vec<String>>,
237 solved: Option<bool>,
238 common: bool,
239 quality: bool,
240 silenced_rules: Vec<String>,
241) -> Config {
242 let mut cfg = Config::default();
243 for ds in per_file {
244 let c = detect_config(ds);
245 for p in c.profiles.into_iter().chain(detect_nc_profiles(ds)) {
246 if !cfg.profiles.contains(&p) {
247 cfg.profiles.push(p);
248 }
249 }
250 cfg.solved |= c.solved;
251 }
252 cfg.not_solved = !cfg.solved;
253 if let Some(p) = profiles { cfg.profiles = p; }
254 if let Some(s) = solved { cfg.solved = s; cfg.not_solved = !s; }
255 cfg.common = common;
256 cfg.quality = quality;
257 cfg.silenced_rules = silenced_rules;
258 cfg
259}
260
261pub fn validate_files(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<Violation> {
266 validate_files_traced(per_file, cfg).into_iter().map(|(_, v)| v).collect()
267}
268
269pub fn validate_files_traced(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<(Option<usize>, Violation)> {
274 let mut violations: Vec<(Option<usize>, Violation)> = std::thread::scope(|s| {
275 per_file
276 .iter()
277 .map(|ds| {
278 s.spawn(move || {
279 let mut v = validate_header(ds, cfg);
280 let mut profiles = detect_config(ds).profiles;
281 profiles.extend(detect_nc_profiles(ds));
282 for profile in &profiles {
283 v.extend(validate_profile_local(ds, profile, cfg));
284 }
285 v
286 })
287 })
288 .collect::<Vec<_>>()
289 .into_iter()
290 .enumerate()
291 .flat_map(|(i, h)| h.join().expect("validation thread panicked").into_iter().map(move |v| (Some(i), v)))
292 .collect()
293 });
294
295 let mut merged = cimmodel::CimDataset::new();
296 for ds in per_file {
297 merged.merge(ds);
298 }
299 violations.extend(validate_crossprofile(&merged, cfg).into_iter().map(|v| (None, v)));
300
301 if !cfg.silenced_rules.is_empty() {
302 let silenced: HashSet<&str> = cfg.silenced_rules.iter().map(String::as_str).collect();
303 violations.retain(|(_, v)| !silenced.contains(v.rule_id.as_str()));
304 }
305 violations
306}