From 4e7691f0e1b237d0c26f39ee8c03baf4e9be69f6 Mon Sep 17 00:00:00 2001 From: Danil-Grigorev Date: Sun, 1 Dec 2024 16:36:49 +0100 Subject: [PATCH] Implement as a JsonSchema generator via derive(ValidateSchema) Signed-off-by: Danil-Grigorev --- examples/crd_derive_schema.rs | 90 ++++++++++----- kube-core/src/cel.rs | 95 ++++++++++++++- kube-core/src/lib.rs | 3 +- kube-derive/src/custom_resource.rs | 179 +++++++++++++---------------- kube-derive/src/lib.rs | 56 ++------- kube/src/lib.rs | 2 +- 6 files changed, 241 insertions(+), 184 deletions(-) diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index e83e12134..06b237160 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -6,11 +6,9 @@ use kube::{ Api, ApiResource, DeleteParams, DynamicObject, GroupVersionKind, Patch, PatchParams, PostParams, WatchEvent, WatchParams, }, - cel_validate, runtime::wait::{await_condition, conditions}, - CELValidate, Client, CustomResource, CustomResourceExt, + Client, CustomResource, CustomResourceExt, ValidateSchema, }; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; // This example shows how the generated schema affects defaulting and validation. @@ -20,9 +18,7 @@ use serde::{Deserialize, Serialize}; // - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#defaulting // - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#defaulting-and-nullable -#[derive( - CustomResource, CELValidate, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, JsonSchema, -)] +#[derive(CustomResource, ValidateSchema, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] #[kube( group = "clux.dev", version = "v1", @@ -31,6 +27,8 @@ use serde::{Deserialize, Serialize}; derive = "PartialEq", derive = "Default" )] +#[serde(rename_all = "camelCase")] +#[cel_validate(rule = Rule::new("self.nonNullable == oldSelf.nonNullable"))] pub struct FooSpec { // Non-nullable without default is required. // @@ -92,18 +90,16 @@ pub struct FooSpec { // Field with CEL validation #[serde(default = "default_legal")] #[cel_validate( - method = cel_validate, - rule = Rule{rule: "self != 'illegal'".into(), message: Some(Message::Expression("'string cannot be illegal'".into())), reason: Some(Reason::FieldValueForbidden), ..Default::default()}, - rule = Rule{rule: "self != 'not legal'".into(), reason: Some(Reason::FieldValueInvalid), ..Default::default()} + rule = Rule::new("self != 'illegal'").message(Message::Expression("'string cannot be illegal'".into())).reason(Reason::FieldValueForbidden), + rule = Rule::new("self != 'not legal'").reason(Reason::FieldValueInvalid), )] - #[schemars(schema_with = "cel_validate")] cel_validated: Option, + #[cel_validate(rule = Rule::new("self == oldSelf").message("is immutable"))] foo_sub_spec: Option, } -#[cel_validate] -#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, JsonSchema)] +#[derive(ValidateSchema, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] pub struct FooSubSpec { #[cel_validate(rule = "self != 'not legal'".into())] field: String, @@ -192,23 +188,23 @@ async fn main() -> Result<()> { // Test defaulting of `non_nullable_with_default` field let data = DynamicObject::new("baz", &api_resource).data(serde_json::json!({ "spec": { - "non_nullable": "a required field", + "nonNullable": "a required field", // `non_nullable_with_default` field is missing // listable values to patch later to verify merge strategies - "default_listable": vec![2], - "set_listable": vec![2], + "defaultListable": vec![2], + "setListable": vec![2], } })); let val = dynapi.create(&PostParams::default(), &data).await?.data; println!("{:?}", val["spec"]); // Defaulting happened for non-nullable field - assert_eq!(val["spec"]["non_nullable_with_default"], default_value()); + assert_eq!(val["spec"]["nonNullableWithDefault"], default_value()); // Listables - assert_eq!(serde_json::to_string(&val["spec"]["default_listable"])?, "[2]"); - assert_eq!(serde_json::to_string(&val["spec"]["set_listable"])?, "[2]"); - assert_eq!(serde_json::to_string(&val["spec"]["cel_validated"])?, "\"legal\""); + assert_eq!(serde_json::to_string(&val["spec"]["defaultListable"])?, "[2]"); + assert_eq!(serde_json::to_string(&val["spec"]["setListable"])?, "[2]"); + assert_eq!(serde_json::to_string(&val["spec"]["celValidated"])?, "\"legal\""); // Missing required field (non-nullable without default) is an error let data = DynamicObject::new("qux", &api_resource).data(serde_json::json!({ @@ -222,7 +218,7 @@ async fn main() -> Result<()> { assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); assert!(err.message.contains("clux.dev \"qux\" is invalid")); - assert!(err.message.contains("spec.non_nullable: Required value")); + assert!(err.message.contains("spec.nonNullable: Required value")); } _ => panic!(), } @@ -233,8 +229,8 @@ async fn main() -> Result<()> { "apiVersion": "clux.dev/v1", "kind": "Foo", "spec": { - "default_listable": vec![3], - "set_listable": vec![3] + "defaultListable": vec![3], + "setListable": vec![3] } }); let pres = foos.patch("baz", &ssapply, &Patch::Apply(patch)).await?; @@ -247,7 +243,7 @@ async fn main() -> Result<()> { "apiVersion": "clux.dev/v1", "kind": "Foo", "spec": { - "cel_validated": Some("illegal") + "celValidated": Some("illegal") } }); let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; @@ -258,7 +254,7 @@ async fn main() -> Result<()> { assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); - assert!(err.message.contains("spec.cel_validated: Forbidden")); + assert!(err.message.contains("spec.celValidated: Forbidden")); assert!(err.message.contains("string cannot be illegal")); } _ => panic!(), @@ -269,7 +265,7 @@ async fn main() -> Result<()> { "apiVersion": "clux.dev/v1", "kind": "Foo", "spec": { - "cel_validated": Some("not legal") + "celValidated": Some("not legal") } }); let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; @@ -280,7 +276,7 @@ async fn main() -> Result<()> { assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); - assert!(err.message.contains("spec.cel_validated: Invalid value")); + assert!(err.message.contains("spec.celValidated: Invalid value")); assert!(err.message.contains("failed rule: self != 'not legal'")); } _ => panic!(), @@ -290,7 +286,7 @@ async fn main() -> Result<()> { "apiVersion": "clux.dev/v1", "kind": "Foo", "spec": { - "foo_sub_spec": { + "fooSubSpec": { "field": Some("not legal"), } } @@ -303,18 +299,54 @@ async fn main() -> Result<()> { assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); - assert!(err.message.contains("spec.foo_sub_spec.field: Invalid value")); + assert!(err.message.contains("spec.fooSubSpec.field: Invalid value")); assert!(err.message.contains("failed rule: self != 'not legal'")); } _ => panic!(), } + let cel_patch = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "fooSubSpec": { + "field": Some("legal"), + } + } + }); + let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; + assert!(cel_res.is_ok()); + + let cel_patch = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "fooSubSpec": { + "field": Some("legal"), + "other": "different", + } + } + }); + let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; + assert!(cel_res.is_err()); + match cel_res.err() { + Some(kube::Error::Api(err)) => { + assert_eq!(err.code, 422); + assert_eq!(err.reason, "Invalid"); + assert_eq!(err.status, "Failure"); + assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); + assert!(err.message.contains("spec.fooSubSpec: Invalid value")); + assert!(err.message.contains("Invalid value: \"object\": is immutable")); + } + _ => panic!(), + } + // cel validation happy: let cel_patch_ok = serde_json::json!({ "apiVersion": "clux.dev/v1", "kind": "Foo", "spec": { - "cel_validated": Some("legal") + "celValidated": Some("legal") } }); foos.patch("baz", &ssapply, &Patch::Apply(cel_patch_ok)).await?; diff --git a/kube-core/src/cel.rs b/kube-core/src/cel.rs index 4105b517c..5816e2004 100644 --- a/kube-core/src/cel.rs +++ b/kube-core/src/cel.rs @@ -19,6 +19,7 @@ pub struct Rule { pub message: Option, /// fieldPath represents the field path returned when the validation fails. /// It must be a relative JSON path, scoped to the location of the field in the schema + #[serde(skip_serializing_if = "Option::is_none")] pub field_path: Option, /// reason is a machine-readable value providing more detail about why a field failed the validation. #[serde(skip_serializing_if = "Option::is_none")] @@ -162,7 +163,7 @@ impl FromStr for Reason { } /// Validate takes schema and applies a set of validation rules to it. The rules are stored -/// under the "x-kubernetes-validations". +/// on the top level under the "x-kubernetes-validations". /// /// ```rust /// use schemars::schema::Schema; @@ -175,7 +176,7 @@ impl FromStr for Reason { /// field_path: Some("spec.host".into()), /// ..Default::default() /// }]; -/// let schema = validate(&mut schema, rules)?; +/// validate(&mut schema, rules)?; /// assert_eq!( /// serde_json::to_string(&schema).unwrap(), /// r#"{"x-kubernetes-validations":[{"fieldPath":"spec.host","message":"must be a URL with the host matching spec.host","rule":"self.spec.host == self.url.host"}]}"#, @@ -184,16 +185,98 @@ impl FromStr for Reason { ///``` #[cfg(feature = "schema")] #[cfg_attr(docsrs, doc(cfg(feature = "schema")))] -pub fn validate(s: &mut Schema, rules: Vec) -> Result { - let rules = serde_json::to_value(rules)?; +pub fn validate(s: &mut Schema, rules: Vec) -> Result<(), serde_json::Error> { match s { Schema::Bool(_) => (), Schema::Object(schema_object) => { schema_object .extensions - .insert("x-kubernetes-validations".into(), rules); + .insert("x-kubernetes-validations".into(), serde_json::to_value(rules)?); + } + }; + Ok(()) +} + +/// Validate property mutates property under property_index of the schema +/// with the provided set of validation rules. +/// +/// ```rust +/// use schemars::JsonSchema; +/// use kube::core::{Rule, validate_property}; +/// +/// #[derive(JsonSchema)] +/// struct MyStruct { +/// field: Option, +/// } +/// +/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator(); +/// let mut schema = MyStruct::json_schema(gen); +/// let rules = vec![Rule::new("self != oldSelf")]; +/// validate_property(&mut schema, 0, rules)?; +/// assert_eq!( +/// serde_json::to_string(&schema).unwrap(), +/// r#"{"type":"object","properties":{"field":{"type":"string","nullable":true,"x-kubernetes-validations":[{"rule":"self != oldSelf"}]}}}"# +/// ); +/// # Ok::<(), serde_json::Error>(()) +///``` +#[cfg(feature = "schema")] +#[cfg_attr(docsrs, doc(cfg(feature = "schema")))] +pub fn validate_property( + s: &mut Schema, + property_index: usize, + rules: Vec, +) -> Result<(), serde_json::Error> { + match s { + Schema::Bool(_) => (), + Schema::Object(schema_object) => { + let obj = schema_object.object(); + for (n, (_, schema)) in obj.properties.iter_mut().enumerate() { + if n == property_index { + return validate(schema, rules); + } + } } }; - Ok(s.clone()) + Ok(()) +} + +/// Merge schema properties in order to pass overrides or extension properties from the other schema. +/// +/// ```rust +/// use schemars::JsonSchema; +/// use kube::core::{Rule, merge_properties}; +/// +/// #[derive(JsonSchema)] +/// struct MyStruct { +/// a: Option, +/// } +/// +/// #[derive(JsonSchema)] +/// struct MySecondStruct { +/// a: bool, +/// b: Option, +/// } +/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator(); +/// let mut first = MyStruct::json_schema(gen); +/// let mut second = MySecondStruct::json_schema(gen); +/// merge_properties(&mut first, &mut second); +/// +/// assert_eq!( +/// serde_json::to_string(&first).unwrap(), +/// r#"{"type":"object","properties":{"a":{"type":"boolean"},"b":{"type":"boolean","nullable":true}}}"# +/// ); +/// # Ok::<(), serde_json::Error>(()) +#[cfg(feature = "schema")] +#[cfg_attr(docsrs, doc(cfg(feature = "schema")))] +pub fn merge_properties(s: &mut Schema, merge: &mut Schema) { + match s { + schemars::schema::Schema::Bool(_) => (), + schemars::schema::Schema::Object(schema_object) => { + let obj = schema_object.object(); + for (k, v) in &merge.clone().into_object().object().properties { + obj.properties.insert(k.clone(), v.clone()); + } + } + } } diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index 9f366d4d1..6ba9f81b6 100644 --- a/kube-core/src/lib.rs +++ b/kube-core/src/lib.rs @@ -28,7 +28,8 @@ pub use crd::CustomResourceExt; pub mod cel; pub use cel::{Message, Reason, Rule}; -#[cfg(feature = "schema")] pub use cel::validate; +#[cfg(feature = "schema")] +pub use cel::{merge_properties, validate, validate_property}; pub mod gvk; pub use gvk::{GroupVersion, GroupVersionKind, GroupVersionResource}; diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 80462a6a5..474368d3f 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -3,13 +3,13 @@ use darling::{ ast::{self, NestedMeta}, util::{self, path_to_string, IdentString}, - FromDeriveInput, FromField, FromMeta, + FromAttributes, FromDeriveInput, FromField, FromMeta, }; use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{ToTokens, TokenStreamExt as _}; use syn::{ - parse::Parser as _, parse_quote, spanned::Spanned, Attribute, Data, DeriveInput, Expr, Path, Type, - Visibility, + parse::Parser as _, parse_quote, spanned::Spanned, Attribute, Data, DataStruct, DeriveInput, Expr, + FieldsNamed, Path, Type, Visibility, }; /// Values we can parse from #[kube(attrs)] @@ -107,8 +107,6 @@ fn default_served_arg() -> bool { #[derive(Debug, FromMeta)] struct Crates { - #[darling(default = "Self::default_kube")] - kube: Path, #[darling(default = "Self::default_kube_core")] kube_core: Path, #[darling(default = "Self::default_k8s_openapi")] @@ -136,10 +134,6 @@ impl Crates { parse_quote! { ::kube::core } // by default must work well with people using facade crate } - fn default_kube() -> Path { - parse_quote! { ::kube } - } - fn default_k8s_openapi() -> Path { parse_quote! { ::k8s_openapi } } @@ -646,132 +640,122 @@ fn generate_hasspec(spec_ident: &Ident, root_ident: &Ident, kube_core: &Path) -> #[derive(FromField)] #[darling(attributes(cel_validate))] struct Rule { - ident: Option, - ty: Type, - method: Option, #[darling(multiple, rename = "rule")] rules: Vec, } #[derive(FromDeriveInput)] #[darling(attributes(cel_validate), supports(struct_named))] -struct CELValidation { +struct ValidateSchema { #[darling(default)] crates: Crates, - data: ast::Data, + ident: Ident, + #[darling(multiple, rename = "rule")] + rules: Vec, } -pub(crate) fn derive_cel_validate(input: TokenStream) -> TokenStream { - let ast: DeriveInput = match syn::parse2(input) { +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 CELValidation { - crates: Crates { - kube_core, schemars, .. - }, - data, - .. - } = match CELValidation::from_derive_input(&ast) { + let ValidateSchema { + crates: + Crates { + kube_core, + schemars, + serde, + .. + }, + ident, + rules, + } = match ValidateSchema::from_derive_input(&ast) { Err(err) => return err.write_errors(), Ok(attrs) => attrs, }; - let mut validations: TokenStream = TokenStream::new(); - - let fields = data.take_struct().map(|f| f.fields).unwrap_or_default(); - for rule in fields.iter().filter(|r| !r.rules.is_empty()) { - let Rule { - rules, - ident, - ty, - method, - } = rule; - let rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - let method = match method { - Some(method) => method.to_token_stream(), - None => match ident { - Some(ident) => IdentString::new(ident.clone()).to_token_stream(), - None => continue, - }, - }; - - validations.append_all(quote! { - fn #method(gen: &mut #schemars::gen::SchemaGenerator) -> #schemars::schema::Schema { - use #kube_core::{Rule, Message, Reason}; - #kube_core::validate(&mut gen.subschema_for::<#ty>(), [#(#rules)*].to_vec()).unwrap() - } - }); - } + // Collect global structure validation rules + let struct_name = ident.to_string(); + let struct_rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - validations -} - -#[derive(FromDeriveInput)] -#[darling(attributes(cel_validate), supports(struct_named))] -struct CELCompletion { - #[darling(default)] - crates: Crates, -} - -pub(crate) fn cel_validate(_args: TokenStream, input: TokenStream) -> TokenStream { - let mut ast: DeriveInput = match syn::parse2(input) { - Err(err) => return err.to_compile_error(), - Ok(di) => di, - }; - - let CELCompletion { - crates: Crates { kube, .. }, - } = match CELCompletion::from_derive_input(&ast) { - Err(err) => return err.write_errors(), - Ok(attrs) => attrs, - }; + // Remove all non-serde, non-schemars attributes + // Has to happen on the original definition at all times, as we don't have #[derive] stanzes. + ast.attrs = ast + .attrs + .iter() + .filter(|attr| attr.path().is_ident("serde") || attr.path().is_ident("schemars")) + .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, method, .. } = match Rule::from_field(field) { - Ok(rule) if rule.rules.is_empty() => continue, + let Rule { rules, .. } = match Rule::from_field(field) { Ok(rule) => rule, Err(err) => return err.write_errors(), }; - // Remove original attributes + // Remove all non-serde, non-schemars 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| !attr.path().is_ident("cel_validate")) + .filter(|attr| attr.path().is_ident("serde") || attr.path().is_ident("schemars")) .cloned() .collect(); - let rules: Vec = rules.iter().map(|r| quote! {rule = #r,}).collect(); - let validator = field.ident.as_ref().map(|i| i.to_string()).unwrap_or_default(); - let validator = method.as_ref().map(path_to_string).unwrap_or(validator); - let method = method.map(|m| quote! {method = #m,}); + if rules.is_empty() { + continue; + } - // Prepare updated definition with shcemars injection - let new_serde_attr = quote! { - #[cel_validate(#method #(#rules)*)] - #[schemars(schema_with = #validator)] - }; + let rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - // Modify directly in tree - let parser = Attribute::parse_outer; - match parser.parse2(new_serde_attr) { - Ok(ref mut parsed) => field.attrs.append(parsed), - Err(e) => return e.to_compile_error(), - }; + // 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)* + 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! { - #[derive(#kube::CELValidate)] - #ast + 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)] + #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() + } + } } } @@ -904,19 +888,14 @@ mod tests { #[test] fn test_derive_validated() { let input = quote! { - #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema, CELValidated)] + #[derive(CustomResource, ValidateSchema, Serialize, Deserialize, Debug, PartialEq, Clone)] #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] struct FooSpec { - #[cel_validate(rule = "self != ''".into())] + #[cel_validate("self != ''".into())] foo: String } }; let input = syn::parse2(input).unwrap(); - let validation = CELValidation::from_derive_input(&input).unwrap(); - let data = validation.data.take_struct(); - assert!(data.is_some()); - let data = data.unwrap(); - assert_eq!(data.len(), 1); - assert_eq!(data.fields[0].rules.len(), 1); + ValidateSchema::from_derive_input(&input).unwrap(); } } diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 97cf9604c..106c5d8a9 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -327,57 +327,21 @@ pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::Tok custom_resource::derive(proc_macro2::TokenStream::from(input)).into() } -/// Generates a JsonSchema patch with a set of CEL expression validation rules applied on the CRD. -/// -/// # Example -/// -/// ```rust -/// use kube::CELValidate; -/// use kube::CustomResource; -/// use serde::Deserialize; -/// use serde::Serialize; -/// use schemars::JsonSchema; -/// use kube::core::crd::CustomResourceExt; -/// -/// #[derive(CustomResource, CELValidate, Serialize, Deserialize, Clone, Debug, JsonSchema)] -/// #[kube(group = "kube.rs", version = "v1", kind = "Struct")] -/// struct MyStruct { -/// #[serde(default = "default")] -/// #[cel_validate(rule = Rule{rule: "self != ''".into(), message: Some("failure message".into()), ..Default::default()})] -/// #[schemars(schema_with = "field")] -/// field: String, -/// } -/// -/// fn default() -> String { -/// "value".into() -/// } -/// -/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains("x-kubernetes-validations")); -/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self != ''""#)); -/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""message":"failure message""#)); -/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""default":"value""#)); -/// ``` -#[proc_macro_derive(CELValidate, attributes(cel_validate))] -pub fn derive_validated(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - custom_resource::derive_cel_validate(input.into()).into() -} - -/// Injects schemars overrides and the derive keyword for provided CEL rules via the [`CELValidate`]. +/// Generates a JsonSchema implementation a set of CEL validation rules applied on the CRD. /// /// ```rust -/// use kube::cel_validate; +/// use kube::ValidateSchema; /// use kube::CustomResource; /// use serde::Deserialize; /// use serde::Serialize; -/// use schemars::JsonSchema; /// use kube::core::crd::CustomResourceExt; /// -/// #[cel_validate] -/// #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)] +/// #[derive(CustomResource, ValidateSchema, Serialize, Deserialize, Clone, Debug)] /// #[kube(group = "kube.rs", version = "v1", kind = "Struct")] +/// #[cel_validate(rule = Rule::new("self == oldSelf"))] /// struct MyStruct { /// #[serde(default = "default")] -/// #[cel_validate(rule = Rule{rule: "self != ''".into(), message: Some("failure message".into()), ..Default::default()})] +/// #[cel_validate(rule = Rule::new("self != ''").message("failure message"))] /// field: String, /// } /// @@ -386,16 +350,14 @@ pub fn derive_validated(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// } /// /// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains("x-kubernetes-validations")); +/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self == oldSelf""#)); /// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self != ''""#)); /// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""message":"failure message""#)); /// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""default":"value""#)); /// ``` -#[proc_macro_attribute] -pub fn cel_validate( - args: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - custom_resource::cel_validate(args.into(), input.into()).into() +#[proc_macro_derive(ValidateSchema, attributes(cel_validate, schemars))] +pub fn derive_schema_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + custom_resource::derive_validated_schema(input.into()).into() } /// A custom derive for inheriting Resource impl for the type. diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 4360be6d5..bec8ba4d0 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -170,7 +170,7 @@ pub use kube_derive::Resource; #[cfg(feature = "derive")] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] -pub use kube_derive::{cel_validate, CELValidate}; +pub use kube_derive::ValidateSchema; #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]