mod generator;
mod schema;
mod shacl;
use std::path::Path;
const DEFAULT_SCHEMA: &str =
"application-profiles-library/CGMES/RDFS/61970-600-2_*-AP-Voc-RDFS2020.rdf";
const DEFAULT_OUTPUT: &str = "cimstructs/src";
const DEFAULT_SHACL: &str =
"application-profiles-library/CGMES/SHACL/*.ttl";
const DEFAULT_SHACL_OUTPUT: &str = "cimvalidation/src";
const DEFAULT_SPARQL_DIR: &str = "cimvalidation/src/sparql";
const DEFAULT_PYTHON_STUBS_OUTPUT: &str = "cimoxide-py/python/cimoxide";
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut schema = DEFAULT_SCHEMA.to_string();
let mut output = DEFAULT_OUTPUT.to_string();
let mut shacl_glob: Option<String> = Some(DEFAULT_SHACL.to_string());
let mut shacl_output: Option<String> = Some(DEFAULT_SHACL_OUTPUT.to_string());
let mut python_stubs_output: Option<String> = Some(DEFAULT_PYTHON_STUBS_OUTPUT.to_string());
let mut verbose = false;
let mut skip_report = false;
let mut rule_report = false;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--schema" => {
i += 1;
schema = args.get(i).cloned().unwrap_or_default();
}
"--output" => {
i += 1;
output = args.get(i).cloned().unwrap_or_default();
}
"--shacl" => {
i += 1;
shacl_glob = args.get(i).cloned();
}
"--shacl-output" => {
i += 1;
shacl_output = args.get(i).cloned();
}
"--python-stubs-output" => {
i += 1;
python_stubs_output = args.get(i).cloned();
}
"--verbose" | "-v" => verbose = true,
"--skip-report" => skip_report = true,
"--rule-report" => rule_report = true,
other => {
eprintln!("unknown argument: {other}");
std::process::exit(1);
}
}
i += 1;
}
if verbose {
eprintln!("schema pattern : {schema}");
eprintln!("output dir : {output}");
}
let mut spec = match schema::import::import_schema_files(&schema, verbose) {
Ok(s) => s,
Err(e) => {
eprintln!("error importing schema: {e}");
std::process::exit(1);
}
};
if verbose {
eprintln!(
"parsed {} types, {} enums, {} datatypes",
spec.types.len(),
spec.enums.len(),
spec.cim_datatypes.len()
);
}
if let Err(e) = generator::rust_gen::generate_rust(&mut spec, Path::new(&output)) {
eprintln!("error generating code: {e}");
std::process::exit(1);
}
eprintln!(
"generated {} structs, {} enums into {output}",
spec.types.len(),
spec.enums.len()
);
if let (Some(glob), Some(out_dir)) = (shacl_glob, shacl_output) {
run_shacl(&spec, &glob, &out_dir, verbose, skip_report, rule_report);
}
if let Some(out_dir) = python_stubs_output {
if let Err(e) =
generator::python_stubs_gen::generate_python_stubs(&spec, Path::new(&out_dir))
{
eprintln!("error generating Python stubs: {e}");
std::process::exit(1);
}
eprintln!("python stubs: types.pyi → {out_dir}");
}
}
fn run_shacl(
spec: &schema::model::CimSpecification,
glob: &str,
out_dir: &str,
verbose: bool,
skip_report: bool,
rule_report: bool,
) {
let pattern = glob::Pattern::new(glob).unwrap_or_else(|e| {
eprintln!("invalid shacl glob pattern: {e}");
std::process::exit(1);
});
let ttl_dir = std::path::Path::new(glob)
.parent()
.unwrap_or(std::path::Path::new("."));
let entries = std::fs::read_dir(ttl_dir).unwrap_or_else(|e| {
eprintln!("cannot read shacl directory {}: {e}", ttl_dir.display());
std::process::exit(1);
});
let mut ttl_paths: Vec<std::path::PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().and_then(|x| x.to_str()) == Some("ttl")
&& pattern.matches_path(p)
})
.collect();
ttl_paths.sort();
if verbose {
eprintln!("found {} SHACL TTL files", ttl_paths.len());
}
let mut results: Vec<shacl::model::FileResults> = Vec::new();
for path in &ttl_paths {
match shacl::ttl_import::import_ttl_file(path) {
Ok(fr) => results.push(fr),
Err(e) => {
eprintln!("warning: skipping {}: {e}", path.display());
}
}
}
let simplify_skips = shacl::simplify::simplify(&mut results);
let (total_checks, mut file_skips) = match shacl::codegen::generate_validation(&results, spec, Path::new(out_dir)) {
Ok(r) => r,
Err(e) => {
eprintln!("error generating SHACL validation code: {e}");
std::process::exit(1);
}
};
for (file_name, s_skips) in simplify_skips {
if s_skips.is_empty() { continue; }
if let Some(fi) = file_skips.iter_mut().find(|f| f.file_name == file_name) {
fi.skips.extend(s_skips);
} else {
file_skips.push(shacl::skip::FileSkipInfo { file_name, check_count: 0, skips: s_skips });
}
}
let total_skipped: usize = file_skips.iter().map(|f| f.skips.len()).sum();
if !rule_report {
eprintln!(
"shacl codegen: {} files, {} checks, {} skipped → {out_dir}",
results.len(), total_checks, total_skipped
);
}
if skip_report {
for fi in &file_skips {
eprintln!("PERFILE\t{}\t{}\t{}", fi.file_name, fi.check_count, fi.skips.len());
}
let mut global_counts: std::collections::HashMap<&'static str, usize> =
std::collections::HashMap::new();
for fi in &file_skips {
for e in &fi.skips {
eprintln!("{}\t{}", fi.file_name, e);
}
shacl::skip::print_file_summary(&fi.file_name, fi.check_count, &fi.skips);
shacl::skip::accumulate_counts(&mut global_counts, &fi.skips);
}
shacl::skip::print_global_summary(&global_counts);
}
if rule_report {
let mut global_counts: std::collections::HashMap<&'static str, usize> =
std::collections::HashMap::new();
for fi in &file_skips {
shacl::skip::accumulate_counts(&mut global_counts, &fi.skips);
}
eprintln!("\n########## README rule-count report ##########");
shacl::skip::print_global_summary(&global_counts);
let mut group_checks: std::collections::HashMap<&'static str, usize> = std::collections::HashMap::new();
let mut group_skipped: std::collections::HashMap<&'static str, usize> = std::collections::HashMap::new();
for fi in &file_skips {
let group = shacl::ttl_report::ttl_group_label(&fi.file_name);
*group_checks.entry(group).or_insert(0) += fi.check_count;
*group_skipped.entry(group).or_insert(0) += fi.skips.len();
}
eprintln!("\n=== Generated SHACL Rules by Profile ===");
eprintln!(" {:32} {:>9} {:>7} {:>6}", "Profile Group", "Generated", "Skipped", "Total");
let (mut gen_total, mut skip_total) = (0usize, 0usize);
for g in shacl::ttl_report::TTL_GROUP_LABEL_ORDER {
let checks = group_checks.get(g).copied().unwrap_or(0);
let skipped = group_skipped.get(g).copied().unwrap_or(0);
gen_total += checks;
skip_total += skipped;
eprintln!(" {:32} {:9} {:7} {:6}", g, checks, skipped, checks + skipped);
}
eprintln!(" -----");
eprintln!(" {:32} {:9} {:7} {:6}", "Total", gen_total, skip_total, gen_total + skip_total);
eprintln!("\n=== Per-File Rule Counts (grep PERFILE to diff against cimgo) ===");
let mut per_file: Vec<&shacl::skip::FileSkipInfo> = file_skips.iter().collect();
per_file.sort_by(|a, b| a.file_name.cmp(&b.file_name));
for fi in &per_file {
eprintln!(
"PERFILE\t{}\t{}\t{}\t{}",
fi.file_name,
fi.check_count,
fi.skips.len(),
fi.check_count + fi.skips.len()
);
}
let groups = shacl::sparql_report::report(std::path::Path::new(DEFAULT_SPARQL_DIR));
let ttl = shacl::ttl_report::ttl_sparql_names(&results);
let rows = shacl::ttl_report::combine_coverage(&groups, &ttl);
eprintln!("\n=== SPARQL Check Coverage (cimvalidation/src/sparql vs {glob}) ===");
eprintln!(" {:32} {:>11} {:>9} {:>8}", "Profile Group", "Implemented", "TTL Total", "Coverage");
let (mut total_impl, mut total_ttl) = (0usize, 0usize);
for r in &rows {
match r.ttl_total {
Some(ttl_total) => {
total_impl += r.implemented;
total_ttl += ttl_total;
let coverage = 100.0 * r.implemented as f64 / ttl_total as f64;
eprintln!(" {:32} {:11} {:9} {:7.1}%", r.label, r.implemented, ttl_total, coverage);
}
None => {
eprintln!(" {:32} {:11} {:>9} {:>8}", r.label, r.implemented, "n/a", "n/a");
}
}
}
eprintln!(" -----");
if total_ttl > 0 {
let coverage = 100.0 * total_impl as f64 / total_ttl as f64;
eprintln!(" {:32} {:11} {:9} {:7.1}%", "Total", total_impl, total_ttl, coverage);
}
for r in &rows {
if r.missing.is_empty() { continue; }
eprintln!("\n Not yet implemented in {}:", r.label);
for m in &r.missing {
eprintln!(" {m}");
}
}
if verbose {
eprintln!("\n=== Implemented names (cimvalidation/src/sparql) ===");
for g in &groups {
eprintln!(" {} ({} names)", g.label, g.names.len());
for n in &g.names {
eprintln!(" {n}");
}
}
}
}
}