Skip to content

Commit

Permalink
Add derive tests and doc support
Browse files Browse the repository at this point in the history
Signed-off-by: Danil-Grigorev <[email protected]>
  • Loading branch information
Danil-Grigorev committed Dec 2, 2024
1 parent 41b4157 commit c87fceb
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 9 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions kube-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
71 changes: 65 additions & 6 deletions kube-derive/src/custom_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,12 +699,13 @@ pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream {
let struct_name = ident.to_string();
let struct_rules: Vec<TokenStream> = 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();

Expand All @@ -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();

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
}
19 changes: 16 additions & 3 deletions kube-derive/tests/crd_schema_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -50,6 +53,7 @@ struct FooSpec {
timestamp: DateTime<Utc>,

/// 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
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
}
},
Expand Down

0 comments on commit c87fceb

Please sign in to comment.