From eace6ac1a8b03bcbedb64fb8fc9200ee659966fb Mon Sep 17 00:00:00 2001 From: clux Date: Tue, 3 May 2022 08:01:28 +0100 Subject: [PATCH 1/5] Simpler analyze entry-point for tests and main for #70 Signed-off-by: clux --- src/analyzer.rs | 47 ++++++++++++++++++++++++----------------------- src/lib.rs | 40 ++-------------------------------------- src/main.rs | 3 +-- src/output.rs | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 63 deletions(-) create mode 100644 src/output.rs diff --git a/src/analyzer.rs b/src/analyzer.rs index c9dda8b..3fa1dbc 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -8,6 +8,16 @@ use std::collections::{BTreeMap, HashMap}; const IGNORED_KEYS: [&str; 3] = ["metadata", "apiVersion", "kind"]; +/// Scan a schema for structs and members, and recurse to find all structs +/// +/// All found output structs will have its names prefixed by the kind it is for +pub fn analyze(schema: JSONSchemaProps, kind: &str) -> Result> { + let mut res = vec![]; + analyze_(schema, "", kind, 0, &mut res)?; + Ok(res) +} + + /// Scan a schema for structs and members, and recurse to find all structs /// /// schema: root schema / sub schema @@ -15,7 +25,7 @@ const IGNORED_KEYS: [&str; 3] = ["metadata", "apiVersion", "kind"]; /// stack: stacked concat of kind + current_{n-1} + ... + current (used to create dedup names/types) /// level: recursion level (start at 0) /// results: multable list of generated structs (not deduplicated) -pub fn analyze( +fn analyze_( schema: JSONSchemaProps, current: &str, stack: &str, @@ -74,7 +84,7 @@ pub fn analyze( if dict_type == "array" { // unpack the inner object from the array wrap if let Some(JSONSchemaPropsOrArray::Schema(items)) = &s.as_ref().items { - analyze(*items.clone(), &next_key, &next_stack, level + 1, results)?; + analyze_(*items.clone(), &next_key, &next_stack, level + 1, results)?; handled_inner = true; } } @@ -82,13 +92,13 @@ pub fn analyze( //if let Some(extra_props) = &s.properties { // for (_key, value) in extra_props { // debug!("nested recurse into {} {} - key: {}", next_key, next_stack, _key); - // analyze(value.clone(), &next_key, &next_stack, level +1, results)?; + // analyze_(value.clone(), &next_key, &next_stack, level +1, results)?; // } //} } if !handled_inner { // normal object recurse - analyze(value, &next_key, &next_stack, level + 1, results)?; + analyze_(value, &next_key, &next_stack, level + 1, results)?; } } "array" => { @@ -108,7 +118,7 @@ pub fn analyze( bail!("could not recurse into vec"); } } - analyze(inner, &next_key, &next_stack, level + 1, results)?; + analyze_(inner, &next_key, &next_stack, level + 1, results)?; } } "" => { @@ -480,8 +490,7 @@ mod test { let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let mut structs = vec![]; - analyze(schema, "ValidationsInfo", "Agent", 0, &mut structs).unwrap(); + let structs = analyze(schema, "Agent").unwrap(); //println!("{:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Agent"); @@ -525,8 +534,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let mut structs = vec![]; - analyze(schema, "Selector", "Server", 0, &mut structs).unwrap(); + let structs = analyze(schema, "Server").unwrap(); //println!("{:#?}", structs); let root = &structs[0]; @@ -557,16 +565,15 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let mut structs = vec![]; - analyze(schema, "ServerSpec", "Server", 0, &mut structs).unwrap(); + let structs = analyze(schema, "Server").unwrap(); let root = &structs[0]; assert_eq!(root.name, "Server"); - assert_eq!(root.level, 0); // should have an IntOrString member: let member = &root.members[0]; assert_eq!(member.name, "port"); assert_eq!(member.type_, "IntOrString"); assert!(root.uses_int_or_string()); + // TODO: check that anyOf: [type: integer, type: string] also works } #[test] @@ -587,8 +594,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let mut structs = vec![]; - analyze(schema, "", "MatchExpressions", 0, &mut structs).unwrap(); + let structs = analyze(schema, "MatchExpressions").unwrap(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "MatchExpressions"); @@ -641,8 +647,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let mut structs = vec![]; - analyze(schema, "", "Endpoint", 0, &mut structs).unwrap(); + let structs = analyze(schema, "Endpoint").unwrap(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Endpoint"); @@ -726,8 +731,7 @@ type: object type: object"#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let mut structs = vec![]; - analyze(schema, "", "ServerSpec", 0, &mut structs).unwrap(); + let structs = analyze(schema, "ServerSpec").unwrap(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServerSpec"); @@ -799,8 +803,7 @@ type: object type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let mut structs = vec![]; - analyze(schema, "Endpoints", "ServiceMonitor", 0, &mut structs).unwrap(); + let structs = analyze(schema, "ServiceMonitor").unwrap(); println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServiceMonitor"); @@ -864,14 +867,12 @@ type: object let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); //println!("schema: {}", serde_json::to_string_pretty(&schema).unwrap()); - let mut structs = vec![]; - analyze(schema, "LocalityLbSetting", "DestinationRule", 1, &mut structs).unwrap(); + let structs = analyze(schema, "DestinationRule").unwrap(); //println!("{:#?}", structs); // this should produce the root struct struct let root = &structs[0]; assert_eq!(root.name, "DestinationRule"); - assert_eq!(root.level, 1); // which contains the distribute member: let distmember = &root.members[0]; assert_eq!(distmember.name, "distribute"); diff --git a/src/lib.rs b/src/lib.rs index a514a24..1b5e39a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,42 +1,6 @@ #[macro_use] extern crate log; -/// Output struct from analysis -#[derive(Default, Debug)] -pub struct OutputStruct { - // The short name of the struct (kind + capitalized suffix) - pub name: String, - pub level: u8, - pub members: Vec, - pub docs: Option, - pub is_enum: bool, -} - -/// Output member belonging to an OutputStruct -#[derive(Default, Debug)] -pub struct OutputMember { - pub name: String, - pub type_: String, - pub serde_annot: Vec, - pub docs: Option, -} - -impl OutputStruct { - pub fn uses_btreemaps(&self) -> bool { - self.members.iter().any(|m| m.type_.contains("BTreeMap")) - } - - pub fn uses_datetime(&self) -> bool { - self.members.iter().any(|m| m.type_.contains("DateTime")) - } - - pub fn uses_date(&self) -> bool { - self.members.iter().any(|m| m.type_.contains("NaiveDate")) - } - - pub fn uses_int_or_string(&self) -> bool { - self.members.iter().any(|m| m.type_.contains("IntOrString")) - } -} - mod analyzer; pub use analyzer::analyze; +mod output; +pub use output::{OutputMember, OutputStruct}; diff --git a/src/main.rs b/src/main.rs index 605fc83..dc9577b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,9 +238,8 @@ impl Kopium { let scope = &crd.spec.scope; if let Some(schema) = data { - let mut structs = vec![]; log::debug!("schema: {}", serde_json::to_string_pretty(&schema)?); - analyze(schema, "", &kind, 0, &mut structs)?; + let structs = analyze(schema, &kind)?; if !self.hide_prelude { self.print_prelude(&structs); diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..c3cc790 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,37 @@ +/// Output struct from analysis +#[derive(Default, Debug)] +pub struct OutputStruct { + // The short name of the struct (kind + capitalized suffix) + pub name: String, + pub level: u8, + pub members: Vec, + pub docs: Option, + pub is_enum: bool, +} + +/// Output member belonging to an OutputStruct +#[derive(Default, Debug)] +pub struct OutputMember { + pub name: String, + pub type_: String, + pub serde_annot: Vec, + pub docs: Option, +} + +impl OutputStruct { + pub fn uses_btreemaps(&self) -> bool { + self.members.iter().any(|m| m.type_.contains("BTreeMap")) + } + + pub fn uses_datetime(&self) -> bool { + self.members.iter().any(|m| m.type_.contains("DateTime")) + } + + pub fn uses_date(&self) -> bool { + self.members.iter().any(|m| m.type_.contains("NaiveDate")) + } + + pub fn uses_int_or_string(&self) -> bool { + self.members.iter().any(|m| m.type_.contains("IntOrString")) + } +} From 406576c83c266d569a84f03426699f27908479a5 Mon Sep 17 00:00:00 2001 From: clux Date: Tue, 3 May 2022 08:39:56 +0100 Subject: [PATCH 2/5] refactor struct renames and fix enum naming Signed-off-by: clux --- src/analyzer.rs | 22 +++++------ src/lib.rs | 58 ++++++++++++++++++++++++++++- src/main.rs | 98 ++++++++++--------------------------------------- src/output.rs | 59 ++++++++++++++++++++++++++++- 4 files changed, 144 insertions(+), 93 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index 3fa1dbc..ce9bb33 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,5 +1,5 @@ //! Deals entirely with schema analysis for the purpose of creating output structs + members -use crate::{OutputMember, OutputStruct}; +use crate::{OutputMember, OutputStruct, Output}; use anyhow::{bail, Result}; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{ JSONSchemaProps, JSONSchemaPropsOrArray, JSONSchemaPropsOrBool, JSON, @@ -11,10 +11,10 @@ const IGNORED_KEYS: [&str; 3] = ["metadata", "apiVersion", "kind"]; /// Scan a schema for structs and members, and recurse to find all structs /// /// All found output structs will have its names prefixed by the kind it is for -pub fn analyze(schema: JSONSchemaProps, kind: &str) -> Result> { +pub fn analyze(schema: JSONSchemaProps, kind: &str) -> Result { let mut res = vec![]; analyze_(schema, "", kind, 0, &mut res)?; - Ok(res) + Ok(Output(res)) } @@ -490,7 +490,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").unwrap(); + let structs = analyze(schema, "Agent").unwrap().0; //println!("{:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Agent"); @@ -534,7 +534,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").unwrap(); + let structs = analyze(schema, "Server").unwrap().0; //println!("{:#?}", structs); let root = &structs[0]; @@ -565,7 +565,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Server").unwrap(); + let structs = analyze(schema, "Server").unwrap().0; let root = &structs[0]; assert_eq!(root.name, "Server"); // should have an IntOrString member: @@ -594,7 +594,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "MatchExpressions").unwrap(); + let structs = analyze(schema, "MatchExpressions").unwrap().0; println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "MatchExpressions"); @@ -647,7 +647,7 @@ type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "Endpoint").unwrap(); + let structs = analyze(schema, "Endpoint").unwrap().0; println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "Endpoint"); @@ -731,7 +731,7 @@ type: object type: object"#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "ServerSpec").unwrap(); + let structs = analyze(schema, "ServerSpec").unwrap().0; println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServerSpec"); @@ -803,7 +803,7 @@ type: object type: object "#; let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap(); - let structs = analyze(schema, "ServiceMonitor").unwrap(); + let structs = analyze(schema, "ServiceMonitor").unwrap().0; println!("got {:?}", structs); let root = &structs[0]; assert_eq!(root.name, "ServiceMonitor"); @@ -867,7 +867,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, "DestinationRule").unwrap(); + let structs = analyze(schema, "DestinationRule").unwrap().0; //println!("{:#?}", structs); // this should produce the root struct struct diff --git a/src/lib.rs b/src/lib.rs index 1b5e39a..445090c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,60 @@ mod analyzer; pub use analyzer::analyze; mod output; -pub use output::{OutputMember, OutputStruct}; +pub use output::{OutputMember, OutputStruct, Output}; + +// synced from https://doc.rust-lang.org/reference/keywords.html feb 2022 +pub const KEYWORDS: [&str; 52] = [ + "as", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + "async", + "await", + "dyn", + "abstract", + "become", + "box", + "do", + "final", + "macro", + "override", + "priv", + "typeof", + "unsized", + "virtual", + "yield", + "try", + "macro_rules", +]; diff --git a/src/main.rs b/src/main.rs index dc9577b..665b11c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,13 @@ use anyhow::{anyhow, Context, Result}; -use heck::ToSnakeCase; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{ CustomResourceDefinition, CustomResourceDefinitionVersion, CustomResourceSubresources, }; -use kopium::{analyze, OutputStruct}; +use kopium::{analyze, OutputStruct, KEYWORDS}; use kube::{api, core::Version, Api, Client, ResourceExt}; use quote::format_ident; use std::path::PathBuf; use structopt::{clap, StructOpt}; -// synced from https://doc.rust-lang.org/reference/keywords.html feb 2022 -const KEYWORDS: [&str; 52] = [ - "as", - "break", - "const", - "continue", - "crate", - "else", - "enum", - "extern", - "false", - "fn", - "for", - "if", - "impl", - "in", - "let", - "loop", - "match", - "mod", - "move", - "mut", - "pub", - "ref", - "return", - "self", - "Self", - "static", - "struct", - "super", - "trait", - "true", - "type", - "unsafe", - "use", - "where", - "while", - "async", - "await", - "dyn", - "abstract", - "become", - "box", - "do", - "final", - "macro", - "override", - "priv", - "typeof", - "unsized", - "virtual", - "yield", - "try", - "macro_rules", -]; - #[derive(StructOpt)] #[structopt( version = clap::crate_version!(), @@ -136,15 +79,17 @@ struct Kopium { #[structopt(subcommand)] command: Option, - /// Convert struct members to snake_case + /// Convert container members to rust casing conventions /// - /// This will run all members through heck::ToSnakeCase, and if different, + /// This will run all struct members through heck::ToSnakeCase, and if different, /// produce a #[serde(rename = "originalName")] attribute on the member. /// + /// For enum members, heck::ToPascalCase is performed instead. + /// /// This operation is safe because names are preserved through attributes. /// However, while not needing the #![allow(non_snake_case)] inner attribute; your code will be longer. #[structopt(long, short = "z")] - snake_case: bool, + rust_case: bool, /// Enable all automatation features /// @@ -174,7 +119,7 @@ async fn main() -> Result<()> { let mut args = Kopium::from_args(); if args.auto { args.docs = true; - args.snake_case = true; + args.rust_case = true; args.schema = "derived".into(); } if args.schema == "derived" && !args.derive.contains(&"JsonSchema".to_string()) { @@ -239,7 +184,11 @@ impl Kopium { if let Some(schema) = data { log::debug!("schema: {}", serde_json::to_string_pretty(&schema)?); - let structs = analyze(schema, &kind)?; + let mut output = analyze(schema, &kind)?; + if self.rust_case { + output = output.rename(); + } + let structs = output.0; if !self.hide_prelude { self.print_prelude(&structs); @@ -286,23 +235,13 @@ impl Kopium { } for m in s.members { self.print_docstr(m.docs, " "); - let mut serda = m.serde_annot; - let name = if self.snake_case && !s.is_enum { - let converted = m.name.to_snake_case(); - if converted != m.name { - serda.push(format!("rename = \"{}\"", m.name)); - } - converted - } else { - m.name - }; - if !serda.is_empty() { - println!(" #[serde({})]", serda.join(", ")); + if !m.serde_annot.is_empty() { + println!(" #[serde({})]", m.serde_annot.join(", ")); } - let safe_name = if KEYWORDS.contains(&name.as_ref()) { - format_ident!("r#{}", name) + let safe_name = if KEYWORDS.contains(&m.name.as_ref()) { + format_ident!("r#{}", m.name) } else { - format_ident!("{}", name) + format_ident!("{}", m.name) }; let spec_trimmed_type = m.type_.as_str().replace(&format!("{}Spec", kind), &kind); if self.builders { @@ -380,8 +319,9 @@ impl Kopium { } fn print_prelude(&self, results: &[OutputStruct]) { - if !self.snake_case && !self.hide_inner_attr { + if !self.rust_case && !self.hide_inner_attr { println!("#![allow(non_snake_case)]"); + // NB: we cannot allow warnings for bad enum names see #69 println!(); } if !self.hide_kube { diff --git a/src/output.rs b/src/output.rs index c3cc790..a0eb2cd 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,19 +1,40 @@ -/// Output struct from analysis +use heck::{ToSnakeCase, ToPascalCase}; + +/// All found containers +pub struct Output(pub Vec); + +/// Output container found by analyzer #[derive(Default, Debug)] pub struct OutputStruct { - // The short name of the struct (kind + capitalized suffix) + /// The short name of the struct (kind + capitalized suffix) pub name: String, + /// The nestedness level the container was found in pub level: u8, + /// Members or enum members of the container pub members: Vec, + /// Documentation properties extracted for the container pub docs: Option, + /// Whether this container is an enum pub is_enum: bool, } /// Output member belonging to an OutputStruct #[derive(Default, Debug)] pub struct OutputMember { + /// The raw, unsanitized name of the member + /// + /// This must be sanitized against KEYWORDS before it can be printed pub name: String, + /// The stringified name of the type such as BTreeMap` pub type_: String, + /// Serde annotations that should prefix the type + /// + /// This will be zero or more of: + /// - default (if the type has a default, or is an option) + /// - skip_serializing_if = "Option::is_none" (if the type is an Option) + /// - rename = "orig_name" (if the type does not match rust casing conventions) + /// + /// The `rename` attribute is only set if `OutputStruct::rename` is called. pub serde_annot: Vec, pub docs: Option, } @@ -35,3 +56,37 @@ impl OutputStruct { self.members.iter().any(|m| m.type_.contains("IntOrString")) } } + +impl OutputStruct { + /// Rename all struct members to rust conventions + pub fn rename(&mut self) { + for m in &mut self.members { + if self.is_enum { + let pascald = m.name.to_pascal_case(); + if pascald != m.name { + m.serde_annot.push(format!("rename = \"{}\"", m.name)); + } + m.name = pascald; + } else { // regular container + let snaked = m.name.to_snake_case(); + if snaked != m.name { + m.serde_annot.push(format!("rename = \"{}\"", m.name)); + } + m.name = snaked; + } + } + } +} + +impl Output { + /// Rename all structs and all all their members to rust conventions + /// + /// Converts [*].members[*].name to snake_case for structs, PascalCase for enums, + /// and adds a serde(rename = "orig_name") annotation to `serde_annot`. + pub fn rename(mut self) -> Self { + for c in &mut self.0 { + c.rename() + } + self + } +} From c6639dc0f2d4406cfeae22eb2ef4b76be5a9e3bc Mon Sep 17 00:00:00 2001 From: clux Date: Tue, 3 May 2022 08:43:01 +0100 Subject: [PATCH 3/5] rename outputs to something more appropriate outputstruct can be an enum now, so rename to container containers contain members, and a new Output wrapper contains all containers. does main refactoring in #70 Signed-off-by: clux --- src/analyzer.rs | 18 +++++++++--------- src/lib.rs | 2 +- src/main.rs | 4 ++-- src/output.rs | 21 +++++++++++---------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index ce9bb33..e487fa9 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,5 +1,5 @@ //! Deals entirely with schema analysis for the purpose of creating output structs + members -use crate::{OutputMember, OutputStruct, Output}; +use crate::{Container, Member, Output}; use anyhow::{bail, Result}; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{ JSONSchemaProps, JSONSchemaPropsOrArray, JSONSchemaPropsOrBool, JSON, @@ -30,7 +30,7 @@ fn analyze_( current: &str, stack: &str, level: u8, - results: &mut Vec, + results: &mut Vec, ) -> Result<()> { let props = schema.properties.clone().unwrap_or_default(); let mut array_recurse_level: HashMap = Default::default(); @@ -148,7 +148,7 @@ fn analyze_enum_properties( stack: &str, level: u8, schema: &JSONSchemaProps, -) -> Result { +) -> Result { let mut members = vec![]; debug!("analyzing enum {}", serde_json::to_string(&schema).unwrap()); for en in items { @@ -161,14 +161,14 @@ fn analyze_enum_properties( // Create member and wrap types correctly let member_doc = None; debug!("with enum member {} of type {}", name, rust_type); - members.push(OutputMember { + members.push(Member { type_: rust_type, name: name.to_string(), serde_annot: vec![], docs: member_doc, }) } - Ok(OutputStruct { + Ok(Container { name: stack.to_string(), members, level, @@ -185,7 +185,7 @@ fn analyze_object_properties( array_recurse_level: &mut HashMap, level: u8, schema: &JSONSchemaProps, -) -> Result, anyhow::Error> { +) -> Result, anyhow::Error> { let mut results = vec![]; let mut members = vec![]; //debug!("analyzing object {}", serde_json::to_string(&schema).unwrap()); @@ -304,7 +304,7 @@ fn analyze_object_properties( let member_doc = value.description.clone(); if reqs.contains(key) { debug!("with required member {} of type {}", key, &rust_type); - members.push(OutputMember { + members.push(Member { type_: rust_type, name: key.to_string(), serde_annot: vec![], @@ -313,7 +313,7 @@ fn analyze_object_properties( } else { // option wrapping needed if not required debug!("with optional member {} of type {}", key, rust_type); - members.push(OutputMember { + members.push(Member { type_: format!("Option<{}>", rust_type), name: key.to_string(), serde_annot: vec![ @@ -328,7 +328,7 @@ fn analyze_object_properties( // probably better to do impl Default to avoid having to make custom fns } } - results.push(OutputStruct { + results.push(Container { name: stack.to_string(), members, level, diff --git a/src/lib.rs b/src/lib.rs index 445090c..e3bf120 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ mod analyzer; pub use analyzer::analyze; mod output; -pub use output::{OutputMember, OutputStruct, Output}; +pub use output::{Container, Member, Output}; // synced from https://doc.rust-lang.org/reference/keywords.html feb 2022 pub const KEYWORDS: [&str; 52] = [ diff --git a/src/main.rs b/src/main.rs index 665b11c..409bd4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Context, Result}; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{ CustomResourceDefinition, CustomResourceDefinitionVersion, CustomResourceSubresources, }; -use kopium::{analyze, OutputStruct, KEYWORDS}; +use kopium::{analyze, Container, KEYWORDS}; use kube::{api, core::Version, Api, Client, ResourceExt}; use quote::format_ident; use std::path::PathBuf; @@ -318,7 +318,7 @@ impl Kopium { println!("#[derive({})]", derives.join(", ")); } - fn print_prelude(&self, results: &[OutputStruct]) { + fn print_prelude(&self, results: &[Container]) { if !self.rust_case && !self.hide_inner_attr { println!("#![allow(non_snake_case)]"); // NB: we cannot allow warnings for bad enum names see #69 diff --git a/src/output.rs b/src/output.rs index a0eb2cd..920c411 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,26 +1,26 @@ -use heck::{ToSnakeCase, ToPascalCase}; +use heck::{ToPascalCase, ToSnakeCase}; /// All found containers -pub struct Output(pub Vec); +pub struct Output(pub Vec); /// Output container found by analyzer #[derive(Default, Debug)] -pub struct OutputStruct { +pub struct Container { /// The short name of the struct (kind + capitalized suffix) pub name: String, /// The nestedness level the container was found in pub level: u8, /// Members or enum members of the container - pub members: Vec, + pub members: Vec, /// Documentation properties extracted for the container pub docs: Option, /// Whether this container is an enum pub is_enum: bool, } -/// Output member belonging to an OutputStruct +/// Output member belonging to an Container #[derive(Default, Debug)] -pub struct OutputMember { +pub struct Member { /// The raw, unsanitized name of the member /// /// This must be sanitized against KEYWORDS before it can be printed @@ -34,12 +34,12 @@ pub struct OutputMember { /// - skip_serializing_if = "Option::is_none" (if the type is an Option) /// - rename = "orig_name" (if the type does not match rust casing conventions) /// - /// The `rename` attribute is only set if `OutputStruct::rename` is called. + /// The `rename` attribute is only set if `Container::rename` is called. pub serde_annot: Vec, pub docs: Option, } -impl OutputStruct { +impl Container { pub fn uses_btreemaps(&self) -> bool { self.members.iter().any(|m| m.type_.contains("BTreeMap")) } @@ -57,7 +57,7 @@ impl OutputStruct { } } -impl OutputStruct { +impl Container { /// Rename all struct members to rust conventions pub fn rename(&mut self) { for m in &mut self.members { @@ -67,7 +67,8 @@ impl OutputStruct { m.serde_annot.push(format!("rename = \"{}\"", m.name)); } m.name = pascald; - } else { // regular container + } else { + // regular container let snaked = m.name.to_snake_case(); if snaked != m.name { m.serde_annot.push(format!("rename = \"{}\"", m.name)); From 726664e4ce3d9ab23e38753aebb25216a3311ab8 Mon Sep 17 00:00:00 2001 From: clux Date: Tue, 3 May 2022 08:47:42 +0100 Subject: [PATCH 4/5] update --all Signed-off-by: clux --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 409bd4f..cfcf667 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,7 +97,7 @@ struct Kopium { /// /// It contains an unstable set of of features and may get expanded in the future. /// - /// Setting --auto enables: --schema=derived --derive=JsonSchema --snake-case --docs + /// Setting --auto enables: --schema=derived --derive=JsonSchema --rust-case --docs #[structopt(long, short = "A")] auto: bool, } From 68203cee30191a7aa0b72db56b52741e48d78b76 Mon Sep 17 00:00:00 2001 From: clux Date: Tue, 3 May 2022 09:18:35 +0100 Subject: [PATCH 5/5] factor out builders into Output::builder_fields Signed-off-by: clux --- src/analyzer.rs | 3 +++ src/main.rs | 21 +++++++-------------- src/output.rs | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index e487fa9..71d89fe 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -165,6 +165,7 @@ fn analyze_enum_properties( type_: rust_type, name: name.to_string(), serde_annot: vec![], + extra_annot: vec![], docs: member_doc, }) } @@ -308,6 +309,7 @@ fn analyze_object_properties( type_: rust_type, name: key.to_string(), serde_annot: vec![], + extra_annot: vec![], docs: member_doc, }) } else { @@ -320,6 +322,7 @@ fn analyze_object_properties( "default".into(), "skip_serializing_if = \"Option::is_none\"".into(), ], + extra_annot: vec![], docs: member_doc, }) // TODO: must capture `default` key here instead of blindly using serde default diff --git a/src/main.rs b/src/main.rs index cfcf667..03283f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,11 +184,10 @@ impl Kopium { if let Some(schema) = data { log::debug!("schema: {}", serde_json::to_string_pretty(&schema)?); - let mut output = analyze(schema, &kind)?; - if self.rust_case { - output = output.rename(); - } - let structs = output.0; + let structs = analyze(schema, &kind)? + .rename(self.rust_case) + .builder_fields(self.builders) + .0; if !self.hide_prelude { self.print_prelude(&structs); @@ -243,16 +242,10 @@ impl Kopium { } else { format_ident!("{}", m.name) }; - let spec_trimmed_type = m.type_.as_str().replace(&format!("{}Spec", kind), &kind); - if self.builders { - if spec_trimmed_type.starts_with("Option<") { - println!("#[builder(default, setter(strip_option))]"); - } else if spec_trimmed_type.starts_with("Vec<") - || spec_trimmed_type.starts_with("BTreeMap<") - { - println!("#[builder(default)]"); - } + for annot in m.extra_annot { + println!(" {}", annot); } + let spec_trimmed_type = m.type_.as_str().replace(&format!("{}Spec", kind), &kind); if s.is_enum { // NB: only supporting plain enumerations atm, not oneOf println!(" {},", safe_name); diff --git a/src/output.rs b/src/output.rs index 920c411..c4550b5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -36,6 +36,11 @@ pub struct Member { /// /// The `rename` attribute is only set if `Container::rename` is called. pub serde_annot: Vec, + /// Additional field level annotations + /// + /// This is currently used by optional builders. + pub extra_annot: Vec, + /// Documentation properties extracted from the property pub docs: Option, } @@ -77,6 +82,18 @@ impl Container { } } } + + /// Add builder annotations + pub fn builder_fields(&mut self) { + for m in &mut self.members { + if m.type_.starts_with("Option<") { + m.extra_annot + .push("#[builder(default, setter(strip_option))]".to_string()); + } else if m.type_.starts_with("Vec<") || m.type_.starts_with("BTreeMap<") { + m.extra_annot.push("#[builder(default)]".to_string()); + } + } + } } impl Output { @@ -84,9 +101,24 @@ impl Output { /// /// Converts [*].members[*].name to snake_case for structs, PascalCase for enums, /// and adds a serde(rename = "orig_name") annotation to `serde_annot`. - pub fn rename(mut self) -> Self { - for c in &mut self.0 { - c.rename() + pub fn rename(mut self, rust_case: bool) -> Self { + if rust_case { + for c in &mut self.0 { + c.rename() + } + } + self + } + + /// Add builders to all output members + /// + /// Adds #[builder(default, setter(strip_option))] to all option types. + /// Adds #[builder(default)] to required vec and btreemaps. + pub fn builder_fields(mut self, builders: bool) -> Self { + if builders { + for c in &mut self.0 { + c.builder_fields() + } } self }