use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use std::fs;
use std::path::Path;
use crate::schema::model::*;
pub fn generate_python_stubs(
spec: &CimSpecification,
output_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
fs::create_dir_all(output_dir)?;
let stubs = render_stubs(spec);
fs::write(output_dir.join("types.pyi"), stubs)?;
Ok(())
}
fn render_stubs(spec: &CimSpecification) -> String {
let mut s = String::new();
writeln!(s, "# Generated by cimgen — do not edit by hand.").unwrap();
writeln!(s, "from __future__ import annotations").unwrap();
writeln!(s, "from typing import TypedDict").unwrap();
writeln!(s).unwrap();
let mut enum_ids: Vec<&String> = spec.enums.keys().collect();
enum_ids.sort();
for id in &enum_ids {
let e = &spec.enums[*id];
if e.values.is_empty() {
continue;
}
render_enum_stub(&mut s, e);
}
let sorted = topological_sort(&spec.types);
for id in &sorted {
if let Some(t) = spec.types.get(id) {
render_type_stub(&mut s, t, spec);
}
}
s
}
fn render_enum_stub(s: &mut String, e: &CimEnum) {
let values: Vec<String> = e
.values
.iter()
.map(|v| format!("\"{}\"", v.label.replace('"', "\\\"")))
.collect();
writeln!(s, "{} = {}", e.id, values.join(" | ")).unwrap();
writeln!(s).unwrap();
}
fn render_type_stub(s: &mut String, t: &CimType, spec: &CimSpecification) {
let base = if t.super_type.is_empty() || !spec.types.contains_key(&t.super_type) {
"TypedDict".to_string()
} else {
t.super_type.clone()
};
if !t.comment.is_empty() {
writeln!(s, "# {}", t.comment).unwrap();
}
writeln!(s, "class {}({}, total=False):", t.id, base).unwrap();
let fields: Vec<&CimAttribute> = t
.attributes
.iter()
.filter(|a| a.is_association_used)
.collect();
if t.super_type.is_empty() {
writeln!(s, " id: str").unwrap();
}
if fields.is_empty() && t.super_type.is_empty() {
} else if fields.is_empty() {
writeln!(s, " pass").unwrap();
} else {
for attr in fields {
let fname = to_snake_case(&attr.label);
let ftype = py_field_type(attr);
writeln!(s, " {fname}: {ftype}").unwrap();
}
}
writeln!(s).unwrap();
}
fn py_field_type(attr: &CimAttribute) -> &'static str {
if attr.is_primitive || attr.is_cim_datatype {
if attr.is_list {
if attr.lang_type == "Vec<f64>" {
"list[float]"
} else if attr.lang_type == "Vec<i64>" {
"list[int]"
} else if attr.lang_type == "Vec<bool>" {
"list[bool]"
} else {
"list[str]"
}
} else {
match attr.lang_type.as_str() {
"f64" => "float | None",
"i64" => "int | None",
"bool" => "bool | None",
_ => "str",
}
}
} else if attr.is_enum_value {
"str | None"
} else if attr.is_list {
"list[str]"
} else {
"str | None"
}
}
fn topological_sort(types: &HashMap<String, CimType>) -> Vec<String> {
let mut sorted = Vec::new();
let mut visited = HashSet::new();
let mut ids: Vec<String> = types.keys().cloned().collect();
ids.sort();
for id in ids {
topo_visit(&id, types, &mut visited, &mut sorted);
}
sorted
}
fn topo_visit(
id: &str,
types: &HashMap<String, CimType>,
visited: &mut HashSet<String>,
sorted: &mut Vec<String>,
) {
if visited.contains(id) {
return;
}
visited.insert(id.to_string());
if let Some(t) = types.get(id) {
if !t.super_type.is_empty() && types.contains_key(&t.super_type) {
topo_visit(&t.super_type, types, visited, sorted);
}
}
sorted.push(id.to_string());
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() {
if i > 0 && !chars[i - 1].is_uppercase() {
result.push('_');
} else if i > 0
&& chars[i - 1].is_uppercase()
&& i + 1 < chars.len()
&& chars[i + 1].is_lowercase()
{
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
result
}