From 5026bdca2bd7d3caf93ac9b9bac9d389ee0b959c Mon Sep 17 00:00:00 2001 From: Danil-Grigorev Date: Wed, 18 Dec 2024 17:33:42 +0100 Subject: [PATCH] Move to a separate package Signed-off-by: Danil-Grigorev --- kube-derive/src/cel_schema.rs | 238 +++++++++++++++++++++++++++++ kube-derive/src/custom_resource.rs | 201 +----------------------- kube-derive/src/lib.rs | 3 +- 3 files changed, 241 insertions(+), 201 deletions(-) create mode 100644 kube-derive/src/cel_schema.rs diff --git a/kube-derive/src/cel_schema.rs b/kube-derive/src/cel_schema.rs new file mode 100644 index 000000000..a85c9514e --- /dev/null +++ b/kube-derive/src/cel_schema.rs @@ -0,0 +1,238 @@ +use darling::{FromDeriveInput, FromField, FromMeta}; +use proc_macro2::TokenStream; +use syn::{parse_quote, DeriveInput, Expr, Ident, Path}; + +#[derive(FromField)] +#[darling(attributes(cel_validate))] +struct Rule { + #[darling(multiple, rename = "rule")] + rules: Vec, +} + +#[derive(FromDeriveInput)] +#[darling(attributes(cel_validate), supports(struct_named))] +struct CELSchema { + #[darling(default)] + crates: Crates, + ident: Ident, + #[darling(multiple, rename = "rule")] + rules: Vec, +} + +#[derive(Debug, FromMeta)] +struct Crates { + #[darling(default = "Self::default_kube_core")] + kube_core: Path, + #[darling(default = "Self::default_schemars")] + schemars: Path, + #[darling(default = "Self::default_serde")] + serde: Path, +} + +// Default is required when the subattribute isn't mentioned at all +// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses +impl Default for Crates { + fn default() -> Self { + Self::from_list(&[]).unwrap() + } +} + +impl Crates { + fn default_kube_core() -> Path { + parse_quote! { ::kube::core } // by default must work well with people using facade crate + } + + fn default_schemars() -> Path { + parse_quote! { ::schemars } + } + + fn default_serde() -> Path { + parse_quote! { ::serde } + } +} + +pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream { + let mut ast: DeriveInput = match syn::parse2(input) { + Err(err) => return err.to_compile_error(), + Ok(di) => di, + }; + + let CELSchema { + crates: Crates { + kube_core, + schemars, + serde, + }, + ident, + rules, + } = match CELSchema::from_derive_input(&ast) { + Err(err) => return err.write_errors(), + Ok(attrs) => attrs, + }; + + // Collect global structure validation rules + let struct_name = ident.to_string(); + let struct_rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); + + // Remove all unknown attributes + // Has to happen on the original definition at all times, as we don't have #[derive] stanzes. + let attribute_whitelist = ["serde", "schemars", "doc"]; + ast.attrs = ast + .attrs + .iter() + .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) + .cloned() + .collect(); + + let struct_data = match ast.data { + syn::Data::Struct(ref mut struct_data) => struct_data, + _ => return quote! {}, + }; + + // Preserve all serde attributes, to allow #[serde(rename_all = "camelCase")] or similar + let struct_attrs: Vec = ast.attrs.iter().map(|attr| quote! {#attr}).collect(); + let mut property_modifications = vec![]; + if let syn::Fields::Named(fields) = &mut struct_data.fields { + for field in &mut fields.named { + let Rule { rules, .. } = match Rule::from_field(field) { + Ok(rule) => rule, + Err(err) => return err.write_errors(), + }; + + // Remove all unknown attributes + // Has to happen on the original definition at all times, as we don't have #[derive] stanzes. + field.attrs = field + .attrs + .iter() + .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) + .cloned() + .collect(); + + if rules.is_empty() { + continue; + } + + let rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); + + // We need to prepend derive macros, as they were consumed by this macro processing, being a derive by itself. + property_modifications.push(quote! { + { + #[derive(#serde::Serialize, #schemars::JsonSchema)] + #(#struct_attrs)* + #[automatically_derived] + #[allow(missing_docs)] + struct Validated { + #field + } + + let merge = &mut Validated::json_schema(gen); + #kube_core::validate_property(merge, 0, [#(#rules)*].to_vec()).unwrap(); + #kube_core::merge_properties(s, merge); + } + }); + } + } + + quote! { + impl #schemars::JsonSchema for #ident { + fn is_referenceable() -> bool { + false + } + + fn schema_name() -> String { + #struct_name.to_string() + "_kube_validation".into() + } + + fn json_schema(gen: &mut #schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + #[derive(#serde::Serialize, #schemars::JsonSchema)] + #[automatically_derived] + #[allow(missing_docs)] + #ast + + use #kube_core::{Rule, Message, Reason}; + let s = &mut #ident::json_schema(gen); + #kube_core::validate(s, [#(#struct_rules)*].to_vec()).unwrap(); + #(#property_modifications)* + s.clone() + } + } + } +} + +#[test] +fn test_derive_validated() { + let input = quote! { + #[derive(CustomResource, CELSchema, Serialize, Deserialize, Debug, PartialEq, Clone)] + #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] + #[cel_validate(rule = "self != ''".into())] + struct FooSpec { + #[cel_validate(rule = "self != ''".into())] + foo: String + } + }; + let input = syn::parse2(input).unwrap(); + let v = CELSchema::from_derive_input(&input).unwrap(); + assert_eq!(v.rules.len(), 1); +} + +#[cfg(test)] +mod tests { + use std::{env, fs}; + + use prettyplease::unparse; + use syn::parse::{Parse as _, Parser as _}; + + use super::*; + #[test] + fn test_derive_validated_full() { + let input = quote! { + #[derive(CELSchema)] + #[cel_validate(rule = "true".into())] + struct FooSpec { + #[cel_validate(rule = "true".into())] + foo: String + } + }; + + let expected = quote! { + impl ::schemars::JsonSchema for FooSpec { + fn is_referenceable() -> bool { + false + } + fn schema_name() -> String { + "FooSpec".to_string() + "_kube_validation".into() + } + fn json_schema( + gen: &mut ::schemars::gen::SchemaGenerator, + ) -> schemars::schema::Schema { + #[derive(::serde::Serialize, ::schemars::JsonSchema)] + #[automatically_derived] + #[allow(missing_docs)] + struct FooSpec { + foo: String, + } + use ::kube::core::{Rule, Message, Reason}; + let s = &mut FooSpec::json_schema(gen); + ::kube::core::validate(s, ["true".into()].to_vec()).unwrap(); + { + #[derive(::serde::Serialize, ::schemars::JsonSchema)] + #[automatically_derived] + #[allow(missing_docs)] + struct Validated { + foo: String, + } + let merge = &mut Validated::json_schema(gen); + ::kube::core::validate_property(merge, 0, ["true".into()].to_vec()).unwrap(); + ::kube::core::merge_properties(s, merge); + } + s.clone() + } + } + }; + + let output = derive_validated_schema(input); + let output = unparse(&syn::File::parse.parse2(output).unwrap()); + let expected = unparse(&syn::File::parse.parse2(expected).unwrap()); + assert_eq!(output, expected); + } +} diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index c74e26c07..e12d559a6 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -1,6 +1,6 @@ // Generated by darling macros, out of our control #![allow(clippy::manual_unwrap_or_default)] -use darling::{FromDeriveInput, FromField, FromMeta}; +use darling::{FromDeriveInput, FromMeta}; use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{ToTokens, TokenStreamExt as _}; use syn::{parse_quote, Data, DeriveInput, Expr, Path, Visibility}; @@ -642,133 +642,6 @@ fn generate_hasspec(spec_ident: &Ident, root_ident: &Ident, kube_core: &Path) -> } } -#[derive(FromField)] -#[darling(attributes(cel_validate))] -struct Rule { - #[darling(multiple, rename = "rule")] - rules: Vec, -} - -#[derive(FromDeriveInput)] -#[darling(attributes(cel_validate), supports(struct_named))] -struct CELSchema { - #[darling(default)] - crates: Crates, - ident: Ident, - #[darling(multiple, rename = "rule")] - rules: Vec, -} - -pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream { - let mut ast: DeriveInput = match syn::parse2(input) { - Err(err) => return err.to_compile_error(), - Ok(di) => di, - }; - - let CELSchema { - crates: - Crates { - kube_core, - schemars, - serde, - .. - }, - ident, - rules, - } = match CELSchema::from_derive_input(&ast) { - Err(err) => return err.write_errors(), - Ok(attrs) => attrs, - }; - - // Collect global structure validation rules - let struct_name = ident.to_string(); - let struct_rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - - // Remove all unknown attributes - // Has to happen on the original definition at all times, as we don't have #[derive] stanzes. - let attribute_whitelist = ["serde", "schemars", "doc"]; - ast.attrs = ast - .attrs - .iter() - .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) - .cloned() - .collect(); - - let struct_data = match ast.data { - syn::Data::Struct(ref mut struct_data) => struct_data, - _ => return quote! {}, - }; - - // Preserve all serde attributes, to allow #[serde(rename_all = "camelCase")] or similar - let struct_attrs: Vec = ast.attrs.iter().map(|attr| quote! {#attr}).collect(); - let mut property_modifications = vec![]; - if let syn::Fields::Named(fields) = &mut struct_data.fields { - for field in &mut fields.named { - let Rule { rules, .. } = match Rule::from_field(field) { - Ok(rule) => rule, - Err(err) => return err.write_errors(), - }; - - // Remove all unknown attributes - // Has to happen on the original definition at all times, as we don't have #[derive] stanzes. - field.attrs = field - .attrs - .iter() - .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) - .cloned() - .collect(); - - if rules.is_empty() { - continue; - } - - let rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - - // We need to prepend derive macros, as they were consumed by this macro processing, being a derive by itself. - property_modifications.push(quote! { - { - #[derive(#serde::Serialize, #schemars::JsonSchema)] - #(#struct_attrs)* - #[automatically_derived] - #[allow(missing_docs)] - struct Validated { - #field - } - - let merge = &mut Validated::json_schema(gen); - #kube_core::validate_property(merge, 0, [#(#rules)*].to_vec()).unwrap(); - #kube_core::merge_properties(s, merge); - } - }); - } - } - - quote! { - impl #schemars::JsonSchema for #ident { - fn is_referenceable() -> bool { - false - } - - fn schema_name() -> String { - #struct_name.to_string() + "_kube_validation".into() - } - - fn json_schema(gen: &mut #schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - #[derive(#serde::Serialize, #schemars::JsonSchema)] - #[automatically_derived] - #[allow(missing_docs)] - #ast - - use #kube_core::{Rule, Message, Reason}; - let s = &mut #ident::json_schema(gen); - #kube_core::validate(s, [#(#struct_rules)*].to_vec()).unwrap(); - #(#property_modifications)* - s.clone() - } - } - } -} - struct StatusInformation { /// The code to be used for the field in the main struct field: TokenStream, @@ -864,9 +737,6 @@ fn to_plural(word: &str) -> String { mod tests { use std::{env, fs}; - use prettyplease::unparse; - use syn::parse::{Parse as _, Parser as _}; - use super::*; #[test] @@ -897,73 +767,4 @@ mod tests { let file = fs::File::open(path).unwrap(); runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap(); } - - #[test] - fn test_derive_validated() { - let input = quote! { - #[derive(CustomResource, CELSchema, Serialize, Deserialize, Debug, PartialEq, Clone)] - #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] - #[cel_validate(rule = "self != ''".into())] - struct FooSpec { - #[cel_validate(rule = "self != ''".into())] - foo: String - } - }; - let input = syn::parse2(input).unwrap(); - let v = CELSchema::from_derive_input(&input).unwrap(); - assert_eq!(v.rules.len(), 1); - } - - #[test] - fn test_derive_validated_full() { - let input = quote! { - #[derive(CELSchema)] - #[cel_validate(rule = "true".into())] - struct FooSpec { - #[cel_validate(rule = "true".into())] - foo: String - } - }; - - let expected = quote! { - impl ::schemars::JsonSchema for FooSpec { - fn is_referenceable() -> bool { - false - } - fn schema_name() -> String { - "FooSpec".to_string() + "_kube_validation".into() - } - fn json_schema( - gen: &mut ::schemars::gen::SchemaGenerator, - ) -> schemars::schema::Schema { - #[derive(::serde::Serialize, ::schemars::JsonSchema)] - #[automatically_derived] - #[allow(missing_docs)] - struct FooSpec { - foo: String, - } - use ::kube::core::{Rule, Message, Reason}; - let s = &mut FooSpec::json_schema(gen); - ::kube::core::validate(s, ["true".into()].to_vec()).unwrap(); - { - #[derive(::serde::Serialize, ::schemars::JsonSchema)] - #[automatically_derived] - #[allow(missing_docs)] - struct Validated { - foo: String, - } - let merge = &mut Validated::json_schema(gen); - ::kube::core::validate_property(merge, 0, ["true".into()].to_vec()).unwrap(); - ::kube::core::merge_properties(s, merge); - } - s.clone() - } - } - }; - - let output = derive_validated_schema(input); - let output = unparse(&syn::File::parse.parse2(output).unwrap()); - let expected = unparse(&syn::File::parse.parse2(expected).unwrap()); - assert_eq!(output, expected); - } } diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 4022a96af..257b1ca2c 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -4,6 +4,7 @@ extern crate proc_macro; #[macro_use] extern crate quote; mod custom_resource; +mod cel_schema; mod resource; /// A custom derive for kubernetes custom resource definitions. @@ -363,7 +364,7 @@ pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::Tok /// ``` #[proc_macro_derive(CELSchema, attributes(cel_validate, schemars))] pub fn derive_schema_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - custom_resource::derive_validated_schema(input.into()).into() + cel_schema::derive_validated_schema(input.into()).into() } /// A custom derive for inheriting Resource impl for the type.