diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index b38f09b66..eb5ef5abb 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -17,6 +17,7 @@ quote = "1.0.8" syn = { version = "1.0.57", features = ["extra-traits"] } Inflector = "0.11.4" serde_json = "1.0.61" +darling = "0.12.1" [lib] proc-macro = true diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index afacb8822..f431436b4 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -1,496 +1,324 @@ -use crate::{CustomDerive, ResultExt}; +use darling::FromDeriveInput; use inflector::string::pluralize::to_plural; use proc_macro2::{Ident, Span}; -use syn::{Data, DeriveInput, Path, Result, Visibility}; - -#[derive(Debug)] -pub(crate) struct CustomResource { - tokens: proc_macro2::TokenStream, - ident: proc_macro2::Ident, - visibility: Visibility, - kubeattrs: KubeAttrs, -} +use syn::{DeriveInput, Path, Result}; /// Values we can parse from #[kube(attrs)] -#[derive(Debug, Default)] -struct KubeAttrs { +#[derive(Debug, Default, FromDeriveInput)] +#[darling(attributes(kube))] +pub(crate) struct KubeAttrs { group: String, version: String, kind: String, - kind_struct: String, + #[darling(default, rename = "struct")] + kind_struct: Option, /// lowercase plural of kind (inferred if omitted) + #[darling(default)] plural: Option, + #[darling(default)] namespaced: bool, + #[darling(default = "default_apiext")] apiextensions: String, + #[darling(multiple, rename = "derive")] derives: Vec, + #[darling(default)] status: Option, + #[darling(multiple, rename = "shortname")] shortnames: Vec, + #[darling(multiple, rename = "printcolumn")] printcolums: Vec, + #[darling(default)] scale: Option, } -impl CustomDerive for CustomResource { - fn parse(input: DeriveInput, tokens: proc_macro2::TokenStream) -> Result { - let ident = input.ident; - let visibility = input.vis; +fn default_apiext() -> String { + "v1".to_owned() +} - // Limit derive to structs - let _s = match input.data { - Data::Struct(ref s) => s, - _ => return Err(r#"Enums or Unions can not #[derive(CustomResource)"#).spanning(ident), - }; +pub(crate) fn derive(input: DeriveInput, kube_attrs: KubeAttrs) -> Result { + let KubeAttrs { + group, + kind, + kind_struct, + version, + namespaced, + derives, + status, + plural, + shortnames, + printcolums, + apiextensions, + scale, + } = kube_attrs; - // Outputs - let mut ka = KubeAttrs { - apiextensions: "v1".to_owned(), // implicit stable crd version expected - ..Default::default() - }; - let (mut group, mut version, mut kind) = (None, None, None); // mandatory GVK - let mut kind_struct = None; + let struct_name = kind_struct.unwrap_or_else(|| kind.clone()); + if input.ident == struct_name { + return Err(syn::Error::new_spanned( + input.ident, + r#"#[derive(CustomResource)] `kind = "..."` must not equal the struct name (this is generated)"#, + )); + } + let visibility = input.vis; + let ident = input.ident; - // Arg parsing - for attr in &input.attrs { - if attr.style != syn::AttrStyle::Outer { - continue; - } - if !attr.path.is_ident("kube") { - continue; - } - let metas = match attr.parse_meta()? { - syn::Meta::List(meta) => meta.nested, - meta => return Err(r#"#[kube] expects a list of metas, like `#[kube(...)]`"#).spanning(meta), - }; + // 1. Create root object Foo and truncate name from FooSpec - for meta in metas { - let meta: &dyn quote::ToTokens = match &meta { - // key-value arguments - syn::NestedMeta::Meta(syn::Meta::NameValue(meta)) => { - if meta.path.is_ident("group") { - if let syn::Lit::Str(lit) = &meta.lit { - group = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(group = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("version") { - if let syn::Lit::Str(lit) = &meta.lit { - version = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(version = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("kind") { - if let syn::Lit::Str(lit) = &meta.lit { - kind = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(kind = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("struct") { - if let syn::Lit::Str(lit) = &meta.lit { - kind_struct = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(struct = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("plural") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.plural = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(plural = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("shortname") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.shortnames.push(lit.value()); - continue; - } else { - return Err(r#"#[kube(shortname = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("scale") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.scale = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(scale = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("status") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.status = Some(lit.value()); - continue; - } else { - return Err(r#"#[kube(status = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("apiextensions") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.apiextensions = lit.value(); - continue; - } else { - return Err( - r#"#[kube(apiextensions = "...")] expects a string literal value"#, - ) - .spanning(meta); - } - } else if meta.path.is_ident("printcolumn") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.printcolums.push(lit.value()); - continue; - } else { - return Err(r#"#[kube(printcolumn = "...")] expects a string literal value"#) - .spanning(meta); - } - } else if meta.path.is_ident("derive") { - if let syn::Lit::Str(lit) = &meta.lit { - ka.derives.push(lit.value()); - continue; - } else { - return Err(r#"#[kube(derive = "...")] expects a string literal value"#) - .spanning(meta); - } - } else { - //println!("Unknown arg {:?}", meta.path.get_ident()); - meta - } - } - // indicator arguments - syn::NestedMeta::Meta(syn::Meta::Path(path)) => { - if path.is_ident("namespaced") { - ka.namespaced = true; - continue; - } else { - &meta - } - } + // Default visibility is `pub(crate)` + // Default generics is no generics (makes little sense to re-use CRD kind?) + // We enforce metadata + spec's existence (always there) + // => No default impl + let rootident = Ident::new(&struct_name, Span::call_site()); - // unknown arg - meta => meta, - }; - // throw on unknown arg - return Err(r#"#[derive(CustomResource)] found unexpected meta"#).spanning(meta); - } - } - - // Unpack the mandatory GVK - let mkerror = |arg| { - format!( - r#"#[derive(CustomResource)] did not find a #[kube({} = "...")] attribute on the struct"#, - arg - ) + // if status set, also add that + let (statusq, statusdef) = if let Some(status_name) = &status { + let ident = format_ident!("{}", status_name); + let fst = quote! { + #[serde(skip_serializing_if = "Option::is_none")] + #visibility status: Option<#ident>, }; - ka.group = group.ok_or_else(|| mkerror("group")).spanning(&tokens)?; - ka.version = version.ok_or_else(|| mkerror("version")).spanning(&tokens)?; - ka.kind = kind.ok_or_else(|| mkerror("kind")).spanning(&tokens)?; - ka.kind_struct = kind_struct.unwrap_or_else(|| ka.kind.clone()); + let snd = quote! { status: None, }; + (fst, snd) + } else { + let fst = quote! {}; + let snd = quote! {}; + (fst, snd) + }; + let has_status = status.is_some(); + let mut has_default = false; - let struct_name = ident.to_string(); - if ka.kind_struct == struct_name { - return Err(r#"#[derive(CustomResource)] `kind = "..."` must not equal the struct name (this is generated)"#) - .spanning(ident); + let mut derive_paths: Vec = vec![]; + for d in ["::serde::Serialize", "::serde::Deserialize", "Clone", "Debug"].iter() { + derive_paths.push(syn::parse_str(*d)?); + } + for d in &derives { + if d == "Default" { + has_default = true; // overridden manually to avoid confusion + } else { + derive_paths.push(syn::parse_str(d)?); } - Ok(CustomResource { - kubeattrs: ka, - tokens, - ident, - visibility, - }) } - // Using parsed info, create code - fn emit(self) -> Result { - let CustomResource { - tokens, - ident, - visibility, - kubeattrs, - } = self; - - let KubeAttrs { - group, - kind, - kind_struct, - version, - namespaced, - derives, - status, - plural, - shortnames, - printcolums, - apiextensions, - scale, - } = kubeattrs; - - // 1. Create root object Foo and truncate name from FooSpec - - // Default visibility is `pub(crate)` - // Default generics is no generics (makes little sense to re-use CRD kind?) - // We enforce metadata + spec's existence (always there) - // => No default impl - let rootident = Ident::new(&kind_struct, Span::call_site()); - - // if status set, also add that - let (statusq, statusdef) = if let Some(status_name) = &status { - let ident = format_ident!("{}", status_name); - let fst = quote! { - #[serde(skip_serializing_if = "Option::is_none")] - #visibility status: Option<#ident>, - }; - let snd = quote! { status: None, }; - (fst, snd) - } else { - let fst = quote! {}; - let snd = quote! {}; - (fst, snd) - }; - let has_status = status.is_some(); - let mut has_default = false; + // Schema generation is always enabled for v1 because it's mandatory. + // TODO Enable schema generation for v1beta1 if the spec derives `JsonSchema`. + let schema_gen_enabled = apiextensions == "v1" && cfg!(feature = "schema"); + // We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because + // these are validated by the API server implicitly. Also, we can't generate the + // schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`. + let schemars_skip = if schema_gen_enabled { + quote! { #[schemars(skip)] } + } else { + quote! {} + }; + if schema_gen_enabled { + derive_paths.push(syn::parse_str("::schemars::JsonSchema")?); + } - let mut derive_paths: Vec = vec![]; - for d in ["::serde::Serialize", "::serde::Deserialize", "Clone", "Debug"].iter() { - derive_paths.push(syn::parse_str(*d)?); + let docstr = format!(" Auto-generated derived type for {} via `CustomResource`", ident); + let root_obj = quote! { + #[doc = #docstr] + #[derive(#(#derive_paths),*)] + #[serde(rename_all = "camelCase")] + #visibility struct #rootident { + #schemars_skip + #visibility api_version: String, + #schemars_skip + #visibility kind: String, + #schemars_skip + #visibility metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta, + #visibility spec: #ident, + #statusq } - for d in &derives { - if d == "Default" { - has_default = true; // overridden manually to avoid confusion - } else { - derive_paths.push(syn::parse_str(d)?); + impl #rootident { + pub fn new(name: &str, spec: #ident) -> Self { + Self { + api_version: <#rootident as k8s_openapi::Resource>::API_VERSION.to_string(), + kind: <#rootident as k8s_openapi::Resource>::KIND.to_string(), + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(name.to_string()), + ..Default::default() + }, + spec: spec, + #statusdef + } } } + }; - // Schema generation is always enabled for v1 because it's mandatory. - // TODO Enable schema generation for v1beta1 if the spec derives `JsonSchema`. - let schema_gen_enabled = apiextensions == "v1" && cfg!(feature = "schema"); - // We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because - // these are validated by the API server implicitly. Also, we can't generate the - // schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`. - let schemars_skip = if schema_gen_enabled { - quote! { #[schemars(skip)] } - } else { - quote! {} - }; - if schema_gen_enabled { - derive_paths.push(syn::parse_str("::schemars::JsonSchema")?); + // 2. Implement Resource trait for k8s_openapi + let api_ver = format!("{}/{}", group, version); + let impl_resource = quote! { + impl k8s_openapi::Resource for #rootident { + const API_VERSION: &'static str = #api_ver; + const GROUP: &'static str = #group; + const KIND: &'static str = #kind; + const VERSION: &'static str = #version; } + }; - let docstr = format!(" Auto-generated derived type for {} via `CustomResource`", ident); - let root_obj = quote! { - #[doc = #docstr] - #[derive(#(#derive_paths),*)] - #[serde(rename_all = "camelCase")] - #visibility struct #rootident { - #schemars_skip - #visibility api_version: String, - #schemars_skip - #visibility kind: String, - #schemars_skip - #visibility metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta, - #visibility spec: #ident, - #statusq + // 3. Implement Metadata trait for k8s_openapi + let impl_metadata = quote! { + impl k8s_openapi::Metadata for #rootident { + type Ty = k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; + fn metadata(&self) -> &Self::Ty { + &self.metadata } - impl #rootident { - pub fn new(name: &str, spec: #ident) -> Self { + fn metadata_mut(&mut self) -> &mut Self::Ty { + &mut self.metadata + } + } + }; + // 4. Implement Default if requested + let impl_default = if has_default { + quote! { + impl Default for #rootident { + fn default() -> Self { Self { api_version: <#rootident as k8s_openapi::Resource>::API_VERSION.to_string(), kind: <#rootident as k8s_openapi::Resource>::KIND.to_string(), - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { - name: Some(name.to_string()), - ..Default::default() - }, - spec: spec, + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta::default(), + spec: Default::default(), #statusdef } } } - }; - - // 2. Implement Resource trait for k8s_openapi - let api_ver = format!("{}/{}", group, version); - let impl_resource = quote! { - impl k8s_openapi::Resource for #rootident { - const API_VERSION: &'static str = #api_ver; - const GROUP: &'static str = #group; - const KIND: &'static str = #kind; - const VERSION: &'static str = #version; - } - }; - - // 3. Implement Metadata trait for k8s_openapi - let impl_metadata = quote! { - impl k8s_openapi::Metadata for #rootident { - type Ty = k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; - fn metadata(&self) -> &Self::Ty { - &self.metadata - } - fn metadata_mut(&mut self) -> &mut Self::Ty { - &mut self.metadata - } - } - }; - // 4. Implement Default if requested - let impl_default = if has_default { - quote! { - impl Default for #rootident { - fn default() -> Self { - Self { - api_version: <#rootident as k8s_openapi::Resource>::API_VERSION.to_string(), - kind: <#rootident as k8s_openapi::Resource>::KIND.to_string(), - metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta::default(), - spec: Default::default(), - #statusdef - } - } - } - } - } else { - quote! {} - }; + } + } else { + quote! {} + }; - // 5. Implement CustomResource - let name = kind.to_ascii_lowercase(); - let plural = plural.unwrap_or_else(|| to_plural(&name)); - let scope = if namespaced { "Namespaced" } else { "Cluster" }; + // 5. Implement CustomResource + let name = kind.to_ascii_lowercase(); + let plural = plural.unwrap_or_else(|| to_plural(&name)); + let scope = if namespaced { "Namespaced" } else { "Cluster" }; - // Compute a bunch of crd props - let mut printers = format!("[ {} ]", printcolums.join(",")); // hacksss - if apiextensions == "v1beta1" { - // only major api inconsistency.. - printers = printers.replace("jsonPath", "JSONPath"); - } - let scale_code = if let Some(s) = scale { s } else { "".to_string() }; + // Compute a bunch of crd props + let mut printers = format!("[ {} ]", printcolums.join(",")); // hacksss + if apiextensions == "v1beta1" { + // only major api inconsistency.. + printers = printers.replace("jsonPath", "JSONPath"); + } + let scale_code = if let Some(s) = scale { s } else { "".to_string() }; - // Ensure it generates for the correct CRD version - let v1ident = format_ident!("{}", apiextensions); - let apiext = quote! { - k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::#v1ident - }; + // Ensure it generates for the correct CRD version + let v1ident = format_ident!("{}", apiextensions); + let apiext = quote! { + k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::#v1ident + }; - let short_json = serde_json::to_string(&shortnames).unwrap(); - let crd_meta_name = format!("{}.{}", plural, group); - let crd_meta = quote! { { "name": #crd_meta_name } }; + let short_json = serde_json::to_string(&shortnames).unwrap(); + let crd_meta_name = format!("{}.{}", plural, group); + let crd_meta = quote! { { "name": #crd_meta_name } }; - let schemagen = if schema_gen_enabled { - quote! { - // Don't use definitions and don't include `$schema` because these are not allowed. - let gen = schemars::gen::SchemaSettings::openapi3().with(|s| { - s.inline_subschemas = true; - s.meta_schema = None; - }).into_generator(); - let schema = gen.into_root_schema_for::(); - } - } else { - // we could issue a compile time warning for this, but it would hit EVERY compile, which would be noisy - // eprintln!("warning: kube-derive configured with manual schema generation"); - // users must manually set a valid schema in crd.spec.versions[*].schema - see examples: crd_derive_no_schema - quote! { - let schema: Option = None; - } - }; + let schemagen = if schema_gen_enabled { + quote! { + // Don't use definitions and don't include `$schema` because these are not allowed. + let gen = schemars::gen::SchemaSettings::openapi3().with(|s| { + s.inline_subschemas = true; + s.meta_schema = None; + }).into_generator(); + let schema = gen.into_root_schema_for::(); + } + } else { + // we could issue a compile time warning for this, but it would hit EVERY compile, which would be noisy + // eprintln!("warning: kube-derive configured with manual schema generation"); + // users must manually set a valid schema in crd.spec.versions[*].schema - see examples: crd_derive_no_schema + quote! { + let schema: Option = None; + } + }; - let jsondata = if apiextensions == "v1" { - quote! { - #schemagen + let jsondata = if apiextensions == "v1" { + quote! { + #schemagen - let jsondata = serde_json::json!({ - "metadata": #crd_meta, - "spec": { - "group": #group, - "scope": #scope, - "names": { - "plural": #plural, - "singular": #name, - "kind": #kind, - "shortNames": shorts - }, - "versions": [{ - "name": #version, - "served": true, - "storage": true, - "schema": { - "openAPIV3Schema": schema, - }, - "additionalPrinterColumns": columns, - "subresources": subres, - }], - } - }); - } - } else { - // TODO Include schema if enabled - quote! { - let jsondata = serde_json::json!({ - "metadata": #crd_meta, - "spec": { - "group": #group, - "scope": #scope, - "names": { - "plural": #plural, - "singular": #name, - "kind": #kind, - "shortNames": shorts + let jsondata = serde_json::json!({ + "metadata": #crd_meta, + "spec": { + "group": #group, + "scope": #scope, + "names": { + "plural": #plural, + "singular": #name, + "kind": #kind, + "shortNames": shorts + }, + "versions": [{ + "name": #version, + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": schema, }, - // printer columns can't be on versions reliably in v1beta.. "additionalPrinterColumns": columns, - "versions": [{ - "name": #version, - "served": true, - "storage": true, - }], "subresources": subres, - } - }); - } - }; + }], + } + }); + } + } else { + // TODO Include schema if enabled + quote! { + let jsondata = serde_json::json!({ + "metadata": #crd_meta, + "spec": { + "group": #group, + "scope": #scope, + "names": { + "plural": #plural, + "singular": #name, + "kind": #kind, + "shortNames": shorts + }, + // printer columns can't be on versions reliably in v1beta.. + "additionalPrinterColumns": columns, + "versions": [{ + "name": #version, + "served": true, + "storage": true, + }], + "subresources": subres, + } + }); + } + }; - // TODO: should ::crd be from a trait? - let impl_crd = quote! { - impl #rootident { - pub fn crd() -> #apiext::CustomResourceDefinition { - let columns : Vec<#apiext::CustomResourceColumnDefinition> = serde_json::from_str(#printers).expect("valid printer column json"); - let scale: Option<#apiext::CustomResourceSubresourceScale> = if #scale_code.is_empty() { - None - } else { - serde_json::from_str(#scale_code).expect("valid scale subresource json") - }; - let shorts : Vec = serde_json::from_str(#short_json).expect("valid shortnames"); - let subres = if #has_status { - if let Some(s) = &scale { - serde_json::json!({ - "status": {}, - "scale": scale - }) - } else { - serde_json::json!({"status": {} }) - } + // TODO: should ::crd be from a trait? + let impl_crd = quote! { + impl #rootident { + pub fn crd() -> #apiext::CustomResourceDefinition { + let columns : Vec<#apiext::CustomResourceColumnDefinition> = serde_json::from_str(#printers).expect("valid printer column json"); + let scale: Option<#apiext::CustomResourceSubresourceScale> = if #scale_code.is_empty() { + None + } else { + serde_json::from_str(#scale_code).expect("valid scale subresource json") + }; + let shorts : Vec = serde_json::from_str(#short_json).expect("valid shortnames"); + let subres = if #has_status { + if let Some(s) = &scale { + serde_json::json!({ + "status": {}, + "scale": scale + }) } else { - serde_json::json!({}) - }; + serde_json::json!({"status": {} }) + } + } else { + serde_json::json!({}) + }; - #jsondata - serde_json::from_value(jsondata) - .expect("valid custom resource from #[kube(attrs..)]") - } + #jsondata + serde_json::from_value(jsondata) + .expect("valid custom resource from #[kube(attrs..)]") } - }; + } + }; - // Concat output - let output = quote! { - #root_obj - #impl_resource - #impl_metadata - #impl_default - #impl_crd - }; - // Try to convert to a TokenStream - let res = syn::parse(output.into()) - .map_err(|err| format!("#[derive(CustomResource)] failed: {:?}", err)) - .spanning(&tokens)?; - Ok(res) - } + // Concat output + Ok(quote! { + #root_obj + #impl_resource + #impl_metadata + #impl_default + #impl_crd + }) } diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 2b34dbe60..3e80245e5 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -4,46 +4,12 @@ #![recursion_limit = "1024"] extern crate proc_macro; #[macro_use] extern crate quote; -use proc_macro::TokenStream; -use syn::Result; -trait CustomDerive: Sized { - fn parse(input: syn::DeriveInput, tokens: proc_macro2::TokenStream) -> Result; - fn emit(self) -> Result; -} - -fn run_custom_derive(input: TokenStream) -> TokenStream -where - T: CustomDerive, -{ - let input: proc_macro2::TokenStream = input.into(); - let tokens = input.clone(); - let token_stream = match syn::parse2(input) - .and_then(|input| ::parse(input, tokens)) - .and_then(::emit) - { - Ok(token_stream) => token_stream, - Err(err) => err.to_compile_error(), - }; - token_stream.into() -} - -trait ResultExt { - fn spanning(self, spanned: impl quote::ToTokens) -> Result; -} - -impl ResultExt for std::result::Result -where - E: std::fmt::Display, -{ - fn spanning(self, spanned: impl quote::ToTokens) -> Result { - self.map_err(|err| syn::Error::new_spanned(spanned, err)) - } -} +use darling::FromDeriveInput; -// #[derive(CustomResource)] mod custom_resource; -use custom_resource::CustomResource; +use custom_resource::KubeAttrs; +use syn::{Data, DeriveInput}; /// A custom derive for kubernetes custom resource definitions. /// @@ -214,5 +180,40 @@ use custom_resource::CustomResource; /// [`k8s_openapi::Resource`]: https://docs.rs/k8s-openapi/*/k8s_openapi/trait.Resource.html #[proc_macro_derive(CustomResource, attributes(kube))] pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - run_custom_derive::(input) + let input = proc_macro2::TokenStream::from(input); + let tokens = input.clone(); + + let derive_input: DeriveInput = match syn::parse2(input) { + Err(err) => return err.to_compile_error().into(), + Ok(di) => di, + }; + // Limit derive to structs + match derive_input.data { + Data::Struct(_) => {} + _ => { + return syn::Error::new_spanned( + &derive_input.ident, + r#"Enums or Unions can not #[derive(CustomResource)"#, + ) + .to_compile_error() + .into() + } + } + + let kube_attrs = match KubeAttrs::from_derive_input(&derive_input) { + Err(err) => return err.write_errors().into(), + Ok(attrs) => attrs, + }; + + let output = match custom_resource::derive(derive_input, kube_attrs) { + Ok(out) => match syn::parse2(out) { + Ok(res) => res, + Err(err) => { + syn::Error::new_spanned(&tokens, format!("#[derive(CustomResource)] failed: {:?}", err)) + .to_compile_error() + } + }, + Err(err) => err.to_compile_error(), + }; + output.into() }