cimoxide-gen 0.6.2

Code generator turning ENTSO-E CGMES RDF/SHACL schemas into typed Rust structs and SHACL validators
//! Emits the class table for a family represented as property bags.
//!
//! A bag family contributes data rows rather than Rust types, so adding one
//! costs a table instead of ~700 struct files, and two families declaring the
//! same class name is a non-event: the rows are keyed by namespace.

use std::fmt::Write;

use crate::schema::model::{CimAttribute, CimSpecification, CimType};
use crate::schema::table::{self, Kind};

/// Class ids in emission order; the index of each is its `ClassId`.
fn ordered_ids(spec: &CimSpecification) -> Vec<&String> {
    let mut ids: Vec<&String> = spec.types.keys().collect();
    ids.sort();
    ids
}

fn attr_kind(a: &CimAttribute) -> &'static str {
    match table::attr_kind(a) {
        Kind::Enum => "AttrKind::Enum",
        Kind::Literal => "AttrKind::Literal",
        Kind::Association => "AttrKind::Association",
    }
}

fn pairs(name: &str, doc: &str, v: &[(&str, &str)]) -> String {
    let mut s = format!("/// {doc}\npub static {name}: &[(&str, &str)] = &[\n");
    for (a, b) in v {
        s.push_str(&format!("    (\"{a}\", \"{b}\"),\n"));
    }
    s.push_str("];\n");
    s
}

fn str_list(items: &[String]) -> String {
    let inner: Vec<String> = items.iter().map(|o| format!("\"{o}\"")).collect();
    format!("&[{}]", inner.join(", "))
}

pub fn render_classes(spec: &CimSpecification) -> String {
    let family = spec.family;
    let ids = ordered_ids(spec);
    let index_of: std::collections::HashMap<&str, usize> = ids
        .iter()
        .enumerate()
        .map(|(i, id)| (id.as_str(), i))
        .collect();

    let mut s = String::new();
    writeln!(s, "// Generated by cimgen — do not edit by hand.").unwrap();
    writeln!(s, "#![allow(clippy::all, dead_code)]").unwrap();
    writeln!(s).unwrap();
    writeln!(s, "use crate::base::{{AttrDef, AttrKind, ClassDef, EnumDef, EnumValueDef, Schema}};").unwrap();
    writeln!(s).unwrap();
    writeln!(
        s,
        "/// Every class of the `{}` family. The index of a row is its class id.",
        family.id
    )
    .unwrap();
    writeln!(s, "pub static CLASSES: &[ClassDef] = &[").unwrap();

    for id in &ids {
        let t: &CimType = &spec.types[*id];
        let super_class = index_of
            .get(t.super_type.as_str())
            .map(|i| format!("Some({i})"))
            .unwrap_or_else(|| "None".to_string());
        writeln!(s, "    ClassDef {{").unwrap();
        writeln!(s, "        ns: \"{}\",", t.namespace).unwrap();
        writeln!(s, "        local: \"{}\",", t.id).unwrap();
        writeln!(
            s,
            "        qualified: \"{}{}\",",
            family.type_prefix, t.id
        )
        .unwrap();
        writeln!(s, "        super_class: {super_class},").unwrap();
        writeln!(s, "        concrete: {},", !t.concrete_in.is_empty()).unwrap();
        writeln!(s, "        origins: {},", str_list(&t.origins)).unwrap();
        writeln!(s, "        comment: {:?},", t.comment).unwrap();

        let attrs: Vec<&CimAttribute> = t.attributes.iter().collect();
        if attrs.is_empty() {
            writeln!(s, "        attrs: &[],").unwrap();
        } else {
            writeln!(s, "        attrs: &[").unwrap();
            for a in attrs {
                writeln!(
                    s,
                    "            AttrDef {{ id: \"{id}\", ns: \"{ns}\", kind: {kind}, range: \"{range}\", is_list: {is_list}, used: {used}, xsd: \"{xsd}\", value_ns: \"{value_ns}\", origins: {origins}, comment: {comment:?} }},",
                    id = a.id,
                    ns = a.namespace,
                    kind = attr_kind(a),
                    range = table::attr_range(a),
                    is_list = a.is_list,
                    used = a.is_association_used,
                    xsd = table::xsd_type(a),
                    value_ns = table::value_namespace(spec, a),
                    origins = str_list(&a.origins),
                    comment = a.comment,
                )
                .unwrap();
            }
            writeln!(s, "        ],").unwrap();
        }
        writeln!(s, "    }},").unwrap();
    }

    writeln!(s, "];").unwrap();
    writeln!(s).unwrap();
    s.push_str(&pairs("PROFILES", "Profile code → profile URI, sorted by code.", &table::profile_uris(spec)));
    writeln!(s).unwrap();
    s.push_str(&pairs("NAMESPACES", "XML prefix → namespace IRI, sorted by prefix.", &table::namespaces(spec)));
    writeln!(s).unwrap();
    writeln!(s, "/// Every enumeration of the `{}` family, sorted by name.", family.id).unwrap();
    writeln!(s, "pub static ENUMS: &[EnumDef] = &[").unwrap();
    for e in table::enums(spec) {
        writeln!(s, "    EnumDef {{").unwrap();
        writeln!(s, "        ns: \"{}\",", e.namespace).unwrap();
        writeln!(s, "        local: \"{}\",", e.id).unwrap();
        writeln!(s, "        comment: {:?},", e.comment).unwrap();
        writeln!(s, "        values: &[").unwrap();
        for v in &e.values {
            writeln!(s, "            EnumValueDef {{ id: \"{}\", comment: {:?} }},", v.id, v.comment).unwrap();
        }
        writeln!(s, "        ],").unwrap();
        writeln!(s, "    }},").unwrap();
    }
    writeln!(s, "];").unwrap();
    writeln!(s).unwrap();
    writeln!(s, "/// The `{}` family's schema.", family.id).unwrap();
    writeln!(s, "pub static SCHEMA: Schema = Schema {{ classes: CLASSES, profiles: PROFILES, namespaces: NAMESPACES, enums: ENUMS }};").unwrap();
    s
}