pub mod violation;
pub use violation::Violation;
pub mod sparql;
pub mod detect;
pub use detect::{detect_config, detect_nc_profiles};
use std::collections::HashSet;
#[derive(Debug, Default, Clone)]
pub struct Config {
pub profiles: Vec<String>,
pub solved: bool,
pub not_solved: bool,
pub common: bool,
pub quality: bool,
pub silenced_rules: Vec<String>,
pub eqbd_base_voltage_ids: Option<HashSet<String>>,
}
pub mod helpers;
mod par;
pub mod bag;
pub mod shapes;
pub mod shape_source;
pub mod nc_shapes;
pub mod nc_profiles;
pub fn nc_shapes() -> &'static [shapes::ShapeDef] {
static R: std::sync::OnceLock<&'static [shapes::ShapeDef]> = std::sync::OnceLock::new();
R.get_or_init(|| shape_source::resolve("nc", nc_shapes::SHAPES))
}
pub type ProfileIndex = (&'static [(&'static str, &'static str)], &'static [&'static str]);
pub fn nc_profile_index() -> ProfileIndex {
static R: std::sync::OnceLock<ProfileIndex> = std::sync::OnceLock::new();
*R.get_or_init(|| {
shape_source::resolve_profiles("nc", nc_profiles::PROFILE_IRIS, nc_profiles::PROFILES)
})
}
pub fn validate_nc_profile(
dataset: &cimmodel::CimDataset,
profile: &str,
cfg: &Config,
) -> Vec<Violation> {
validate_nc_profile_in(dataset, &[], profile, cfg)
}
pub fn validate_nc_profile_in(
dataset: &cimmodel::CimDataset,
set: &[cimmodel::CimDataset],
profile: &str,
cfg: &Config,
) -> Vec<Violation> {
let mut v = bag::validate_profile_in(dataset, set, profile, nc_shapes(), cfg);
if nc_profile_index().1.contains(&profile) {
v.extend(sparql::nc::validate_local(dataset));
}
v
}
pub fn validate_nc_merged(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
let mut v = bag::validate_profile(dataset, MERGED_PROFILE, nc_shapes(), cfg);
v.extend(sparql::nc::validate_merged(dataset));
v
}
pub const MERGED_PROFILE: &str = "ALL";
pub mod cgmes_shapes;
pub mod cgmes_profiles;
pub fn cgmes_shapes() -> &'static [shapes::ShapeDef] {
static R: std::sync::OnceLock<&'static [shapes::ShapeDef]> = std::sync::OnceLock::new();
R.get_or_init(|| shape_source::resolve("cgmes", cgmes_shapes::SHAPES))
}
fn cgmes_tagged(tag: &str) -> &'static [&'static shapes::ShapeDef] {
type Index = std::collections::HashMap<&'static str, Vec<&'static shapes::ShapeDef>>;
static IDX: std::sync::OnceLock<Index> = std::sync::OnceLock::new();
let idx = IDX.get_or_init(|| {
let mut idx = Index::new();
for shape in cgmes_shapes() {
for tag in shape.profiles {
idx.entry(*tag).or_default().push(shape);
}
}
idx
});
idx.get(tag).map_or(&[], Vec::as_slice)
}
fn run_cgmes(dataset: &cimmodel::CimDataset, active: &[&shapes::ShapeDef]) -> Vec<Violation> {
bag::validate_shapes(dataset, bag::Source::Cgmes, active)
}
fn run_cgmes_in(
dataset: &cimmodel::CimDataset,
set: &[cimmodel::CimDataset],
active: &[&shapes::ShapeDef],
) -> Vec<Violation> {
bag::validate_shapes_in(dataset, set, bag::Source::Cgmes, active)
}
pub fn validate_header(dataset: &cimmodel::CimDataset, _cfg: &Config) -> Vec<Violation> {
run_cgmes(dataset, cgmes_tagged("HDR"))
}
pub fn validate_profile_shacl(dataset: &cimmodel::CimDataset, profile: &str, _cfg: &Config) -> Vec<Violation> {
run_cgmes(dataset, cgmes_tagged(profile))
}
pub fn validate_crossprofile_shacl(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
let has = |p: &str| cfg.profiles.is_empty() || cfg.profiles.iter().any(|x| x == p);
let mut active: Vec<&shapes::ShapeDef> = cimschema_cross_profiles()
.iter()
.filter(|p| has(p))
.flat_map(|p| cgmes_tagged(&format!("X:{p}")).iter().copied())
.collect();
if cfg.not_solved {
for p in cgmes_profile_index().1.iter().filter(|p| has(p)) {
active.extend_from_slice(cgmes_tagged(&format!("{p}!NS")));
}
}
if cfg.common {
active.extend_from_slice(cgmes_tagged("COMMON"));
}
run_cgmes(dataset, &active)
}
pub fn cgmes_profile_index() -> ProfileIndex {
static R: std::sync::OnceLock<ProfileIndex> = std::sync::OnceLock::new();
*R.get_or_init(|| {
shape_source::resolve_profiles("cgmes", cgmes_profiles::PROFILE_IRIS, cgmes_profiles::PROFILES)
})
}
fn cimschema_cross_profiles() -> &'static [&'static str] {
&["DL", "DY", "GL", "OP", "SV", "TP"]
}
pub fn validate_profile_local(dataset: &cimmodel::CimDataset, profile: &str, cfg: &Config) -> Vec<Violation> {
validate_profile_local_in(dataset, &[], profile, cfg)
}
pub fn validate_profile_local_in(
dataset: &cimmodel::CimDataset,
set: &[cimmodel::CimDataset],
profile: &str,
cfg: &Config,
) -> Vec<Violation> {
if !cfg.profiles.is_empty() && !cfg.profiles.iter().any(|p| p == profile) {
return Vec::new();
}
if !cgmes_profile_index().1.contains(&profile) && nc_profile_index().1.contains(&profile) {
return validate_nc_profile_in(dataset, set, profile, cfg);
}
let mut v = run_cgmes_in(dataset, set, cgmes_tagged(profile));
v.extend(sparql::validate_profile_local(dataset, profile, cfg));
v
}
pub fn validate_crossprofile(dataset: &cimmodel::CimDataset, cfg: &Config) -> Vec<Violation> {
let (sparql, mut v) = par::par_two(
|| sparql::validate_crossprofile(dataset, cfg),
|| validate_crossprofile_shacl(dataset, cfg),
);
v.extend(sparql);
let cgmes = cgmes_profile_index().1;
if cfg.profiles.iter().any(|p| !cgmes.contains(&p.as_str()) && nc_profile_index().1.contains(&p.as_str())) {
v.extend(validate_nc_merged(dataset, cfg));
}
v
}
pub fn combined_config(
per_file: &[cimmodel::CimDataset],
profiles: Option<Vec<String>>,
solved: Option<bool>,
common: bool,
quality: bool,
silenced_rules: Vec<String>,
) -> Config {
let mut cfg = Config::default();
for ds in per_file {
let c = detect_config(ds);
for p in c.profiles.into_iter().chain(detect_nc_profiles(ds)) {
if !cfg.profiles.contains(&p) {
cfg.profiles.push(p);
}
}
cfg.solved |= c.solved;
}
cfg.not_solved = !cfg.solved;
if let Some(p) = profiles { cfg.profiles = p; }
if let Some(s) = solved { cfg.solved = s; cfg.not_solved = !s; }
cfg.common = common;
cfg.quality = quality;
cfg.silenced_rules = silenced_rules;
cfg
}
pub fn validate_files(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<Violation> {
validate_files_traced(per_file, cfg).into_iter().map(|(_, v)| v).collect()
}
pub fn validate_files_traced(per_file: Vec<cimmodel::CimDataset>, cfg: &Config) -> Vec<(Option<usize>, Violation)> {
let set = per_file.as_slice();
let mut violations: Vec<(Option<usize>, Violation)> = par::par_each(set, |ds| {
let mut v = validate_header(ds, cfg);
let mut profiles = detect_config(ds).profiles;
profiles.extend(detect_nc_profiles(ds));
for profile in &profiles {
v.extend(validate_profile_local_in(ds, set, profile, cfg));
}
v
})
.into_iter()
.enumerate()
.flat_map(|(i, found)| found.into_iter().map(move |v| (Some(i), v)))
.collect();
let mut merged = cimmodel::CimDataset::new();
for ds in per_file {
merged.merge(ds);
}
violations.extend(validate_crossprofile(&merged, cfg).into_iter().map(|v| (None, v)));
if !cfg.silenced_rules.is_empty() {
let silenced: HashSet<&str> = cfg.silenced_rules.iter().map(String::as_str).collect();
violations.retain(|(_, v)| !silenced.contains(v.rule_id.as_str()));
}
violations
}