Skip to main content

cimvalidation/
lib.rs

1pub mod violation;
2pub use violation::Violation;
3
4pub mod sparql;
5pub mod detect;
6pub use detect::detect_config;
7
8use std::collections::HashSet;
9
10#[derive(Debug, Default, Clone)]
11pub struct Config {
12    /// Profile short names to validate (e.g. "EQ", "SSH"). Empty = all detected.
13    pub profiles: Vec<String>,
14    /// True if SV profile (power-flow results) is present.
15    pub solved: bool,
16    /// True if SV profile is absent.
17    pub not_solved: bool,
18    /// Run common cross-profile checks.
19    pub common: bool,
20    /// Run CIMdesk modeling quality checks.
21    pub quality: bool,
22    /// Rule IDs to suppress in the output.
23    pub silenced_rules: Vec<String>,
24    /// When non-empty, enables the EQBD2 base voltage check.
25    pub eqbd_base_voltage_ids: Option<HashSet<String>>,
26}
27
28pub mod helpers;
29pub mod generated_p61968_13_geographicallocation_ap_con_complex_shacl;
30pub mod generated_p61970_301_diagramlayout_ap_con_complex_notsolvedmas_shacl;
31pub mod generated_p61970_301_diagramlayout_ap_con_complex_shacl;
32pub mod generated_p61970_301_equipment_ap_con_complex_shacl;
33pub mod generated_p61970_301_equipmentboundary_ap_con_complex_shacl;
34pub mod generated_p61970_301_operation_ap_con_complex_shacl;
35pub mod generated_p61970_301_shortcircuit_ap_con_complex_shacl;
36pub mod generated_p61970_301_statevariables_ap_con_complex_shacl;
37pub mod generated_p61970_301_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl;
38pub mod generated_p61970_301_steadystatehypothesis_ap_con_complex_shacl;
39pub mod generated_p61970_302_dynamics_ap_con_complex_shacl;
40pub mod generated_p61970_452_equipment_ap_con_complex_shacl;
41pub mod generated_p61970_452_operation_ap_con_complex_shacl;
42pub mod generated_p61970_453_diagramlayout_ap_con_complex_implicit_crossprofile_shacl;
43pub mod generated_p61970_453_diagramlayout_ap_con_complex_shacl;
44pub mod generated_p61970_456_statevariables_ap_con_complex_explicit_crossprofile_shacl;
45pub mod generated_p61970_456_statevariables_ap_con_complex_implicit_crossprofile_shacl;
46pub mod generated_p61970_456_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl;
47pub mod generated_p61970_456_steadystatehypothesis_ap_con_complex_shacl;
48pub mod generated_p61970_456_topology_ap_con_complex_implicit_crossprofile_shacl;
49pub mod generated_p61970_456_topology_ap_con_complex_shacl;
50pub mod generated_p61970_457_dynamics_ap_con_complex_implicit_crossprofile_shacl;
51pub mod generated_p61970_552_header_ap_con_simple_shacl;
52pub mod generated_p61970_600_1_equipment_ap_con_complex_shacl;
53pub mod generated_p61970_600_2_diagramlayout_ap_con_simple_shacl;
54pub mod generated_p61970_600_2_dynamics_ap_con_simple_shacl;
55pub mod generated_p61970_600_2_equipment_ap_con_complex_shacl;
56pub mod generated_p61970_600_2_equipment_ap_con_simple_shacl;
57pub mod generated_p61970_600_2_equipmentboundary_ap_con_simple_shacl;
58pub mod generated_p61970_600_2_geographicallocation_ap_con_complex_implicit_crossprofile_shacl;
59pub mod generated_p61970_600_2_geographicallocation_ap_con_simple_shacl;
60pub mod generated_p61970_600_2_operation_ap_con_complex_implicit_crossprofile_shacl;
61pub mod generated_p61970_600_2_operation_ap_con_simple_shacl;
62pub mod generated_p61970_600_2_shortcircuit_ap_con_simple_shacl;
63pub mod generated_p61970_600_2_statevariables_ap_con_simple_shacl;
64pub mod generated_p61970_600_2_steadystatehypothesis_ap_con_simple_shacl;
65pub mod generated_p61970_600_2_topology_ap_con_simple_shacl;
66
67// ── per-profile local rules ────────────────────────────────────────────────
68
69fn validate_dl_local(dataset: &cimdecoder::CimDataset, cfg: &Config) -> Vec<Violation> {
70    let mut v = Vec::new();
71    if cfg.not_solved {
72        v.extend(generated_p61970_301_diagramlayout_ap_con_complex_notsolvedmas_shacl::validate_p61970_301_diagramlayout_ap_con_complex_notsolvedmas_shacl(dataset));
73    }
74    v.extend(generated_p61970_301_diagramlayout_ap_con_complex_shacl::validate_p61970_301_diagramlayout_ap_con_complex_shacl(dataset));
75    v.extend(generated_p61970_453_diagramlayout_ap_con_complex_shacl::validate_p61970_453_diagramlayout_ap_con_complex_shacl(dataset));
76    v.extend(generated_p61970_600_2_diagramlayout_ap_con_simple_shacl::validate_p61970_600_2_diagramlayout_ap_con_simple_shacl(dataset));
77    v
78}
79
80fn validate_dl_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
81    generated_p61970_453_diagramlayout_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_453_diagramlayout_ap_con_complex_implicit_crossprofile_shacl(dataset)
82}
83
84fn validate_dy_local(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
85    let mut v = Vec::new();
86    v.extend(generated_p61970_302_dynamics_ap_con_complex_shacl::validate_p61970_302_dynamics_ap_con_complex_shacl(dataset));
87    v.extend(generated_p61970_600_2_dynamics_ap_con_simple_shacl::validate_p61970_600_2_dynamics_ap_con_simple_shacl(dataset));
88    v
89}
90
91fn validate_dy_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
92    generated_p61970_457_dynamics_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_457_dynamics_ap_con_complex_implicit_crossprofile_shacl(dataset)
93}
94
95pub fn validate_eq(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
96    let mut v = Vec::new();
97    v.extend(generated_p61970_301_equipment_ap_con_complex_shacl::validate_p61970_301_equipment_ap_con_complex_shacl(dataset));
98    v.extend(generated_p61970_452_equipment_ap_con_complex_shacl::validate_p61970_452_equipment_ap_con_complex_shacl(dataset));
99    v.extend(generated_p61970_600_1_equipment_ap_con_complex_shacl::validate_p61970_600_1_equipment_ap_con_complex_shacl(dataset));
100    v.extend(generated_p61970_600_2_equipment_ap_con_complex_shacl::validate_p61970_600_2_equipment_ap_con_complex_shacl(dataset));
101    v.extend(generated_p61970_600_2_equipment_ap_con_simple_shacl::validate_p61970_600_2_equipment_ap_con_simple_shacl(dataset));
102    v
103}
104
105pub fn validate_eqbd(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
106    let mut v = Vec::new();
107    v.extend(generated_p61970_301_equipmentboundary_ap_con_complex_shacl::validate_p61970_301_equipmentboundary_ap_con_complex_shacl(dataset));
108    v.extend(generated_p61970_600_2_equipmentboundary_ap_con_simple_shacl::validate_p61970_600_2_equipmentboundary_ap_con_simple_shacl(dataset));
109    v
110}
111
112fn validate_gl_local(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
113    let mut v = Vec::new();
114    v.extend(generated_p61968_13_geographicallocation_ap_con_complex_shacl::validate_p61968_13_geographicallocation_ap_con_complex_shacl(dataset));
115    v.extend(generated_p61970_600_2_geographicallocation_ap_con_simple_shacl::validate_p61970_600_2_geographicallocation_ap_con_simple_shacl(dataset));
116    v
117}
118
119fn validate_gl_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
120    generated_p61970_600_2_geographicallocation_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_600_2_geographicallocation_ap_con_complex_implicit_crossprofile_shacl(dataset)
121}
122
123fn validate_op_local(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
124    let mut v = Vec::new();
125    v.extend(generated_p61970_301_operation_ap_con_complex_shacl::validate_p61970_301_operation_ap_con_complex_shacl(dataset));
126    v.extend(generated_p61970_452_operation_ap_con_complex_shacl::validate_p61970_452_operation_ap_con_complex_shacl(dataset));
127    v.extend(generated_p61970_600_2_operation_ap_con_simple_shacl::validate_p61970_600_2_operation_ap_con_simple_shacl(dataset));
128    v
129}
130
131fn validate_op_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
132    generated_p61970_600_2_operation_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_600_2_operation_ap_con_complex_implicit_crossprofile_shacl(dataset)
133}
134
135pub fn validate_sc(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
136    let mut v = Vec::new();
137    v.extend(generated_p61970_301_shortcircuit_ap_con_complex_shacl::validate_p61970_301_shortcircuit_ap_con_complex_shacl(dataset));
138    v.extend(generated_p61970_600_2_shortcircuit_ap_con_simple_shacl::validate_p61970_600_2_shortcircuit_ap_con_simple_shacl(dataset));
139    v
140}
141
142pub fn validate_ssh(dataset: &cimdecoder::CimDataset, cfg: &Config) -> Vec<Violation> {
143    let mut v = Vec::new();
144    if cfg.not_solved {
145        v.extend(generated_p61970_301_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl::validate_p61970_301_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl(dataset));
146    }
147    v.extend(generated_p61970_301_steadystatehypothesis_ap_con_complex_shacl::validate_p61970_301_steadystatehypothesis_ap_con_complex_shacl(dataset));
148    if cfg.not_solved {
149        v.extend(generated_p61970_456_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl::validate_p61970_456_steadystatehypothesis_ap_con_complex_notsolvedmas_shacl(dataset));
150    }
151    v.extend(generated_p61970_456_steadystatehypothesis_ap_con_complex_shacl::validate_p61970_456_steadystatehypothesis_ap_con_complex_shacl(dataset));
152    v.extend(generated_p61970_600_2_steadystatehypothesis_ap_con_simple_shacl::validate_p61970_600_2_steadystatehypothesis_ap_con_simple_shacl(dataset));
153    v
154}
155
156fn validate_sv_local(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
157    let mut v = Vec::new();
158    v.extend(generated_p61970_301_statevariables_ap_con_complex_shacl::validate_p61970_301_statevariables_ap_con_complex_shacl(dataset));
159    v.extend(generated_p61970_600_2_statevariables_ap_con_simple_shacl::validate_p61970_600_2_statevariables_ap_con_simple_shacl(dataset));
160    v
161}
162
163fn validate_sv_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
164    let mut v = Vec::new();
165    v.extend(generated_p61970_456_statevariables_ap_con_complex_explicit_crossprofile_shacl::validate_p61970_456_statevariables_ap_con_complex_explicit_crossprofile_shacl(dataset));
166    v.extend(generated_p61970_456_statevariables_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_456_statevariables_ap_con_complex_implicit_crossprofile_shacl(dataset));
167    v
168}
169
170fn validate_tp_local(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
171    let mut v = Vec::new();
172    v.extend(generated_p61970_456_topology_ap_con_complex_shacl::validate_p61970_456_topology_ap_con_complex_shacl(dataset));
173    v.extend(generated_p61970_600_2_topology_ap_con_simple_shacl::validate_p61970_600_2_topology_ap_con_simple_shacl(dataset));
174    v
175}
176
177fn validate_tp_crossprofile(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
178    generated_p61970_456_topology_ap_con_complex_implicit_crossprofile_shacl::validate_p61970_456_topology_ap_con_complex_implicit_crossprofile_shacl(dataset)
179}
180
181// ── public two-phase API ───────────────────────────────────────────────────
182
183/// Phase 1 — header: validate FullModel/DifferenceModel rules for a single file's dataset.
184pub fn validate_header(dataset: &cimdecoder::CimDataset, _cfg: &Config) -> Vec<Violation> {
185    generated_p61970_552_header_ap_con_simple_shacl::validate_p61970_552_header_ap_con_simple_shacl(dataset)
186}
187
188/// Phase 1 — per-profile: run local (non-crossprofile) generated SHACL rules and SPARQL
189/// rules for one profile.
190///
191/// Pass the single-file dataset for the profile and the combined config (solved/not_solved
192/// must reflect the full set of files, not just this file). If `cfg.profiles` is non-empty
193/// and does not include `profile`, returns an empty vec — this filter applies to both the
194/// SHACL and SPARQL checks.
195pub fn validate_profile_local(dataset: &cimdecoder::CimDataset, profile: &str, cfg: &Config) -> Vec<Violation> {
196    if !cfg.profiles.is_empty() && !cfg.profiles.iter().any(|p| p == profile) {
197        return Vec::new();
198    }
199    let mut v = match profile {
200        "DL"   => validate_dl_local(dataset, cfg),
201        "DY"   => validate_dy_local(dataset, cfg),
202        "EQ"   => validate_eq(dataset, cfg),
203        "EQBD" => validate_eqbd(dataset, cfg),
204        "GL"   => validate_gl_local(dataset, cfg),
205        "OP"   => validate_op_local(dataset, cfg),
206        "SC"   => validate_sc(dataset, cfg),
207        "SSH"  => validate_ssh(dataset, cfg),
208        "SV"   => validate_sv_local(dataset, cfg),
209        "TP"   => validate_tp_local(dataset, cfg),
210        _      => Vec::new(),
211    };
212    v.extend(sparql::validate_profile_local(dataset, profile, cfg));
213    v
214}
215
216/// Phase 2 — crossprofile: run crossprofile SHACL + cross-profile SPARQL on the merged dataset.
217pub fn validate_crossprofile(dataset: &cimdecoder::CimDataset, cfg: &Config) -> Vec<Violation> {
218    let has = |p: &str| cfg.profiles.is_empty() || cfg.profiles.iter().any(|x| x == p);
219
220    let mut v = Vec::new();
221    if has("DL") { v.extend(validate_dl_crossprofile(dataset, cfg)); }
222    if has("DY") { v.extend(validate_dy_crossprofile(dataset, cfg)); }
223    if has("GL") { v.extend(validate_gl_crossprofile(dataset, cfg)); }
224    if has("OP") { v.extend(validate_op_crossprofile(dataset, cfg)); }
225    if has("SV") { v.extend(validate_sv_crossprofile(dataset, cfg)); }
226    if has("TP") { v.extend(validate_tp_crossprofile(dataset, cfg)); }
227
228    v.extend(sparql::validate_crossprofile(dataset, cfg));
229    v
230}
231
232/// Build a combined `Config` by auto-detecting profiles/solved-state across all files,
233/// then applying explicit overrides (each `None`/default leaves the detected value).
234pub fn combined_config(
235    per_file: &[cimdecoder::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 {
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
261/// Run full two-phase validation: per-file local checks (header + per-profile SHACL/SPARQL)
262/// in parallel, then crossprofile checks on the merged dataset, then apply rule silencing.
263///
264/// Consumes `per_file` since it merges them into one dataset for phase 2.
265pub fn validate_files(per_file: Vec<cimdecoder::CimDataset>, cfg: &Config) -> Vec<Violation> {
266    let mut violations: Vec<Violation> = std::thread::scope(|s| {
267        per_file
268            .iter()
269            .map(|ds| {
270                s.spawn(move || {
271                    let mut v = validate_header(ds, cfg);
272                    let file_cfg = detect_config(ds);
273                    for profile in &file_cfg.profiles {
274                        v.extend(validate_profile_local(ds, profile, cfg));
275                    }
276                    v
277                })
278            })
279            .collect::<Vec<_>>()
280            .into_iter()
281            .flat_map(|h| h.join().expect("validation thread panicked"))
282            .collect()
283    });
284
285    let mut merged = cimdecoder::CimDataset::new();
286    for ds in per_file {
287        merged.merge(ds);
288    }
289    violations.extend(validate_crossprofile(&merged, cfg));
290
291    if !cfg.silenced_rules.is_empty() {
292        let silenced: HashSet<&str> = cfg.silenced_rules.iter().map(String::as_str).collect();
293        violations.retain(|v| !silenced.contains(v.rule_id.as_str()));
294    }
295    violations
296}