From 2d236c431ccd2214163826490133cb4136cb56da Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 22 Sep 2025 23:17:22 +0100 Subject: [PATCH 1/3] Deduplicate and disambiguate containers by PartialEq Signed-off-by: clux --- src/analyzer.rs | 85 ++++++++++++++++++++++++++++--------------------- src/main.rs | 5 +-- src/output.rs | 54 ++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 39 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index 9af1ddb..f5c5c30 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -21,10 +21,10 @@ pub struct Config { /// /// All found output structs will have its names prefixed by the kind it is for pub fn analyze(schema: JSONSchemaProps, kind: &str, cfg: Config) -> Result { - let mut res = vec![]; + let mut res = Output::default(); analyze_(&schema, "", kind, 0, &mut res, &cfg)?; - Ok(Output(res)) + Ok(res) } /// Scan a schema for structs and members, and recurse to find all structs @@ -39,7 +39,7 @@ fn analyze_( current: &str, stack: &str, level: u8, - results: &mut Vec, + results: &mut Output, cfg: &Config, ) -> Result<()> { let props = schema.properties.clone().unwrap_or_default(); @@ -69,7 +69,7 @@ fn analyze_( schema, cfg, )?; - results.push(c); + results.insert(c); // deduplicated insert } else if dict_type == "object" { // recurse to see if we eventually find properties debug!( @@ -97,7 +97,7 @@ fn analyze_( schema, cfg, )?; - results.push(c); + results.insert(c); // deduplicated insert } } //trace!("full schema here: {}", serde_yaml::to_string(&schema).unwrap()); @@ -146,9 +146,9 @@ fn find_containers( level: u8, schema: &JSONSchemaProps, cfg: &Config, -) -> Result> { +) -> Result { //trace!("finding containers in: {}", serde_yaml::to_string(&props)?); - let mut results = vec![]; + let mut results = Output::default(); for (key, value) in props { if level == 0 && IGNORED_KEYS.contains(&(key.as_ref())) { debug!("not recursing into ignored {}", key); // handled elsewhere @@ -217,7 +217,7 @@ fn find_containers( // ....although this makes it impossible for us to handle enums at the top level // TODO: move this to the top level let new_result = analyze_enum_properties(en, &next_stack, level, schema)?; - results.push(new_result); + results.insert(new_result); // deduplicated insert } else { debug!("..not recursing into {} ('{}' is not a container)", key, x) } @@ -644,7 +644,7 @@ mod test { let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let structs = analyze(schema, "Agent", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Agent", Cfg::default()).unwrap().output(); //println!("{:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Agent"); @@ -688,7 +688,7 @@ mod test { let structs = analyze(schema, "ClusterStatusTopology", Cfg::default()) .unwrap() - .0; + .output(); //println!("{:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ClusterStatusTopology"); @@ -714,8 +714,9 @@ mod test { "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "AliasRoutingConfig", Cfg::default()).unwrap().0; - //println!("{:?}", structs); + let structs = analyze(schema, "AliasRoutingConfig", Cfg::default()) + .unwrap() + .output(); //println!("{:?}", structs); let root = &structs[0]; assert_eq!(root.name, "AliasRoutingConfig"); assert_eq!(root.level, 0); @@ -748,7 +749,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let structs = analyze(schema, "Server", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Server", Cfg::default()).unwrap().output(); //println!("{:#?}", structs); let root = &structs[0]; @@ -780,7 +781,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); println!("got {schema:?}"); - let structs = analyze(schema, "Spec", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Spec", Cfg::default()).unwrap().output(); println!("got: {structs:?}"); let root = &structs[0]; assert_eq!(root.name, "Spec"); @@ -824,7 +825,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); // println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let structs = analyze(schema, "Variables", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Variables", Cfg::default()).unwrap().output(); // println!("{:#?}", structs); let root = &structs[0]; @@ -858,7 +859,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Server", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Server", Cfg::default()).unwrap().output(); let root = &structs[0]; assert_eq!(root.name, "Server"); // should have an IntOrString member: @@ -892,7 +893,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Host", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Host", Cfg::default()).unwrap().output(); let root = &structs[0]; assert_eq!(root.name, "Host"); @@ -921,7 +922,7 @@ type: object type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Options", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Options", Cfg::default()).unwrap().output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Options"); @@ -948,7 +949,9 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "MatchExpressions", Cfg::default()).unwrap().0; + let structs = analyze(schema, "MatchExpressions", Cfg::default()) + .unwrap() + .output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "MatchExpressions"); @@ -1001,7 +1004,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Endpoint", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Endpoint", Cfg::default()).unwrap().output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Endpoint"); @@ -1085,7 +1088,7 @@ type: object type: object"#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "ServerSpec", Cfg::default()).unwrap().0; + let structs = analyze(schema, "ServerSpec", Cfg::default()).unwrap().output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServerSpec"); @@ -1156,7 +1159,9 @@ type: object type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "ServiceMonitor", Cfg::default()).unwrap().0; + let structs = analyze(schema, "ServiceMonitor", Cfg::default()) + .unwrap() + .output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServiceMonitor"); @@ -1219,7 +1224,9 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let structs = analyze(schema, "DestinationRule", Cfg::default()).unwrap().0; + let structs = analyze(schema, "DestinationRule", Cfg::default()) + .unwrap() + .output(); //println!("{:#?}", structs); // this should produce the root struct struct @@ -1254,7 +1261,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); println!("got schema {}", serde_yaml::to_string(&schema).unwrap()); - let structs = analyze(schema, "StatusCode", Cfg::default()).unwrap().0; + let structs = analyze(schema, "StatusCode", Cfg::default()).unwrap().output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "StatusCode"); @@ -1280,7 +1287,9 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "KustomizationSpec", Cfg::default()).unwrap().0; + let structs = analyze(schema, "KustomizationSpec", Cfg::default()) + .unwrap() + .output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "KustomizationSpec"); @@ -1304,7 +1313,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Schema", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Schema", Cfg::default()).unwrap().output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Schema"); @@ -1343,7 +1352,9 @@ type: object type: object type: object"#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "AppProjectStatus", Cfg::default()).unwrap().0; + let structs = analyze(schema, "AppProjectStatus", Cfg::default()) + .unwrap() + .output(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "AppProjectStatus"); @@ -1377,7 +1388,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Agent", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Agent", Cfg::default()).unwrap().output(); let root = &structs[0]; assert_eq!(root.name, "Agent"); @@ -1405,7 +1416,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "ArgoCDExport", Cfg::default()).unwrap().0; + let structs = analyze(schema, "ArgoCDExport", Cfg::default()).unwrap().output(); let root = &structs[0]; assert_eq!(root.name, "ArgoCdExport"); @@ -1435,7 +1446,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Geoip", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Geoip", Cfg::default()).unwrap().output(); assert_eq!(structs.len(), 1); assert_eq!(structs[0].members.len(), 1); @@ -1473,7 +1484,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Gateway", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Gateway", Cfg::default()).unwrap().output(); assert_eq!(structs.len(), 1); assert_eq!(structs[0].members.len(), 1); assert_eq!(structs[0].members[0].type_, "Option>"); @@ -1506,7 +1517,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Reference", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Reference", Cfg::default()).unwrap().output(); assert_eq!(structs[0].members[0].type_, "Option"); } @@ -1540,7 +1551,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Reference", Cfg::default()).unwrap().0; + let structs = analyze(schema, "Reference", Cfg::default()).unwrap().output(); assert_eq!(structs[0].members[0].type_, "Option>"); } @@ -1559,7 +1570,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "postgresql", Cfg::default()).unwrap().0; + let structs = analyze(schema, "postgresql", Cfg::default()).unwrap().output(); assert_eq!(structs[0].members[0].type_, "PostgresqlProp"); } @@ -1598,7 +1609,9 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "CephClusterSpec", Cfg::default()).unwrap().0; + let structs = analyze(schema, "CephClusterSpec", Cfg::default()) + .unwrap() + .output(); // debug!("got: {:#?}", structs); @@ -1628,7 +1641,7 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "KubeadmConfig", Cfg::default()).unwrap().0; + let structs = analyze(schema, "KubeadmConfig", Cfg::default()).unwrap().output(); let root = &structs[0]; assert_eq!(root.name, "KubeadmConfig"); diff --git a/src/main.rs b/src/main.rs index 7799727..53904f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, str::FromStr}; -#[macro_use] extern crate log; +#[macro_use] +extern crate log; use anyhow::{anyhow, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use heck::ToUpperCamelCase; @@ -245,7 +246,7 @@ impl Kopium { let structs = analyze(schema, kind, cfg)? .rename() .builder_fields(self.builders) - .0; + .output(); if !self.hide_prelude { self.print_prelude(&structs); diff --git a/src/output.rs b/src/output.rs index 449524f..e476177 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,7 +4,59 @@ use heck::{ToPascalCase, ToSnakeCase}; use regex::{Regex, RegexBuilder}; /// All found containers -pub struct Output(pub Vec); +#[derive(Default, Debug)] +pub struct Output(Vec); + +impl Output { + /// Safe container inserter + /// + /// Using this to insert, and encapulating the underlying vector ensures that we; + /// + /// 1. repeat struct with pathological names are distinguished https://github.com/kube-rs/kopium/issues/66 + /// 2. identical structs with different names are deduplicated https://github.com/kube-rs/kopium/issues/298 + pub fn insert(&mut self, mut value: Container) -> bool { + let mut name_clashes = 0; + for c in &self.0 { + if c == &value { + return false; // no new value inserted + } + if &c.name == &value.name { + // there is a struct with the same name + name_clashes += 1; + } + } + // keep adding X suffixes to the struct/enum name until we are past the number of clashes + // this setup is not foolproof, but should be good enough for a POC + while name_clashes > 0 { + value.name = format!("{}X", value.name); + name_clashes -= 1; + } + // push the struct/enum (possibly with a new unique name) + self.0.push(value); + true // new value inserted + } + + /// Consume self and return the final container vec + pub fn output(self) -> Vec { + self.0 + } + + /// Extend the inner vector with another Output instance + pub fn extend(&mut self, extras: Output) { + self.0.extend(extras.0); + } +} + +impl PartialEq for Container { + fn eq(&self, other: &Self) -> bool { + (&self.name, &self.members, &self.is_enum) == (&other.name, &other.members, &other.is_enum) + } +} +impl PartialEq for Member { + fn eq(&self, other: &Self) -> bool { + (&self.name, &self.type_, &self.serde_annot) == (&other.name, &other.type_, &other.serde_annot) + } +} /// Output container found by analyzer #[derive(Default, Debug)] From daf7faa7ad312228369da0f7c088c1a38654a6cd Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 22 Sep 2025 23:18:44 +0100 Subject: [PATCH 2/3] fmt Signed-off-by: clux --- src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 53904f7..316db55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use std::{path::PathBuf, str::FromStr}; -#[macro_use] -extern crate log; +#[macro_use] extern crate log; use anyhow::{anyhow, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use heck::ToUpperCamelCase; From fdd2b3299538165220a821daac2334ea0d75b14d Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 22 Sep 2025 23:21:45 +0100 Subject: [PATCH 3/3] clippy Signed-off-by: clux --- src/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index e476177..9047fbc 100644 --- a/src/output.rs +++ b/src/output.rs @@ -20,7 +20,7 @@ impl Output { if c == &value { return false; // no new value inserted } - if &c.name == &value.name { + if c.name == value.name { // there is a struct with the same name name_clashes += 1; }