From c87fceb5c1f5aab9b2c254af1301a592e7f83591 Mon Sep 17 00:00:00 2001 From: Danil-Grigorev Date: Mon, 2 Dec 2024 15:01:49 +0100 Subject: [PATCH] Add derive tests and doc support Signed-off-by: Danil-Grigorev --- Cargo.toml | 1 + kube-derive/Cargo.toml | 1 + kube-derive/src/custom_resource.rs | 71 +++++++++++++++++++++++++--- kube-derive/tests/crd_schema_test.rs | 19 ++++++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14fc4e283..10d2d4650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,3 +90,4 @@ tower-test = "0.4.0" tracing = "0.1.36" tracing-subscriber = "0.3.17" trybuild = "1.0.48" +prettyplease = "0.2.25" \ No newline at end of file diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 0b89ea2f0..01320eca3 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -34,3 +34,4 @@ chrono.workspace = true trybuild.workspace = true assert-json-diff.workspace = true runtime-macros = { git = "https://github.com/tyrone-wu/runtime-macros.git", rev = "e31f4de52e078d41aba4792a7ea30139606c1362" } +prettyplease.workspace = true diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index d3e9288c7..ac4d4ab61 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -699,12 +699,13 @@ pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream { let struct_name = ident.to_string(); let struct_rules: Vec = rules.iter().map(|r| quote! {#r,}).collect(); - // Remove all non-serde, non-schemars attributes + // 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| attr.path().is_ident("serde") || attr.path().is_ident("schemars")) + .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) .cloned() .collect(); @@ -723,12 +724,12 @@ pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream { Err(err) => return err.write_errors(), }; - // Remove all non-serde, non-schemars attributes + // 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| attr.path().is_ident("serde") || attr.path().is_ident("schemars")) + .filter(|attr| attribute_whitelist.iter().any(|i| attr.path().is_ident(i))) .cloned() .collect(); @@ -878,6 +879,9 @@ 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] @@ -914,12 +918,67 @@ mod tests { let input = quote! { #[derive(CustomResource, ValidateSchema, Serialize, Deserialize, Debug, PartialEq, Clone)] #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] + #[cel_validate(rule = "self != ''".into())] struct FooSpec { - #[cel_validate("self != ''".into())] + #[cel_validate(rule = "self != ''".into())] foo: String } }; let input = syn::parse2(input).unwrap(); - ValidateSchema::from_derive_input(&input).unwrap(); + let v = ValidateSchema::from_derive_input(&input).unwrap(); + assert_eq!(v.rules.len(), 1); + } + + #[test] + fn test_derive_validated_full() { + let input = quote! { + #[derive(ValidateSchema)] + #[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/tests/crd_schema_test.rs b/kube-derive/tests/crd_schema_test.rs index e975d8ff3..aba8e4873 100644 --- a/kube-derive/tests/crd_schema_test.rs +++ b/kube-derive/tests/crd_schema_test.rs @@ -2,13 +2,14 @@ use assert_json_diff::assert_json_eq; use chrono::{DateTime, Utc}; +use kube::ValidateSchema; use kube_derive::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; // See `crd_derive_schema` example for how the schema generated from this struct affects defaulting and validation. -#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] +#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, ValidateSchema)] #[kube( group = "clux.dev", version = "v1", @@ -26,8 +27,10 @@ use std::collections::{HashMap, HashSet}; annotation("clux.dev", "cluxingv1"), annotation("clux.dev/firewall", "enabled"), label("clux.dev", "cluxingv1"), - label("clux.dev/persistence", "disabled") + label("clux.dev/persistence", "disabled"), + rule = Rule::new("self.metadata.name == 'singleton'"), )] +#[cel_validate(rule = Rule::new("has(self.nonNullable)"))] #[serde(rename_all = "camelCase")] struct FooSpec { non_nullable: String, @@ -50,6 +53,7 @@ struct FooSpec { timestamp: DateTime, /// This is a complex enum with a description + #[cel_validate(rule = Rule::new("!has(self.variantOne) || self.variantOne.int > 22"))] complex_enum: ComplexEnum, /// This is a untagged enum with a description @@ -303,6 +307,9 @@ fn test_crd_schema_matches_expected() { "required": ["variantThree"] } ], + "x-kubernetes-validations": [{ + "rule": "!has(self.variantOne) || self.variantOne.int > 22", + }], "description": "This is a complex enum with a description" }, "untaggedEnumPerson": { @@ -347,13 +354,19 @@ fn test_crd_schema_matches_expected() { "timestamp", "untaggedEnumPerson" ], + "x-kubernetes-validations": [{ + "rule": "has(self.nonNullable)", + }], "type": "object" } }, "required": [ "spec" ], - "title": "Foo", + "x-kubernetes-validations": [{ + "rule": "self.metadata.name == 'singleton'", + }], + "title": "Foo_kube_validation", "type": "object" } },