kube_derive/
cel_schema.rs

1use darling::{
2    util::{parse_expr, IdentString},
3    FromDeriveInput, FromField, FromMeta,
4};
5use proc_macro2::TokenStream;
6use syn::{parse_quote, Attribute, DeriveInput, Expr, Ident, Path};
7
8#[derive(FromField)]
9#[darling(attributes(x_kube))]
10struct XKube {
11    #[darling(multiple, rename = "validation", with = parse_expr::preserve_str_literal)]
12    validations: Vec<Expr>,
13    merge_strategy: Option<Expr>,
14}
15
16#[derive(FromDeriveInput)]
17#[darling(attributes(x_kube), supports(struct_named))]
18struct KubeSchema {
19    #[darling(default)]
20    crates: Crates,
21    ident: Ident,
22    #[darling(multiple, rename = "validation", with = parse_expr::preserve_str_literal)]
23    validations: Vec<Expr>,
24}
25
26#[derive(Debug, FromMeta)]
27struct Crates {
28    #[darling(default = "Self::default_kube_core")]
29    kube_core: Path,
30    #[darling(default = "Self::default_schemars")]
31    schemars: Path,
32    #[darling(default = "Self::default_serde")]
33    serde: Path,
34    #[darling(default = "Self::default_std")]
35    std: Path,
36}
37
38// Default is required when the subattribute isn't mentioned at all
39// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
40impl Default for Crates {
41    fn default() -> Self {
42        Self::from_list(&[]).unwrap()
43    }
44}
45
46impl Crates {
47    fn default_kube_core() -> Path {
48        parse_quote! { ::kube::core } // by default must work well with people using facade crate
49    }
50
51    fn default_schemars() -> Path {
52        parse_quote! { ::schemars }
53    }
54
55    fn default_serde() -> Path {
56        parse_quote! { ::serde }
57    }
58
59    fn default_std() -> Path {
60        parse_quote! { ::std }
61    }
62}
63
64pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream {
65    let mut ast: DeriveInput = match syn::parse2(input) {
66        Err(err) => return err.to_compile_error(),
67        Ok(di) => di,
68    };
69
70    let KubeSchema {
71        crates:
72            Crates {
73                kube_core,
74                schemars,
75                serde,
76                std,
77            },
78        ident,
79        validations,
80    } = match KubeSchema::from_derive_input(&ast) {
81        Err(err) => return err.write_errors(),
82        Ok(attrs) => attrs,
83    };
84
85    // Collect global structure validation rules
86    let struct_name = IdentString::new(ident.clone()).map(|ident| format!("{ident}Validated"));
87    let struct_rules: Vec<TokenStream> = validations
88        .iter()
89        .map(|rule| quote! {#kube_core::validate(s, #rule).unwrap();})
90        .collect();
91
92    // Modify generated struct name to avoid Struct::method conflicts in attributes
93    ast.ident = struct_name.as_ident().clone();
94
95    // Remove all unknown attributes from the original structure copy
96    // Has to happen on the original definition at all times, as we don't have #[derive] stanzes.
97    let attribute_whitelist = ["serde", "schemars", "doc", "validate"];
98    ast.attrs = remove_attributes(&ast.attrs, &attribute_whitelist);
99
100    let struct_data = match ast.data {
101        syn::Data::Struct(ref mut struct_data) => struct_data,
102        _ => return quote! {},
103    };
104
105    // Preserve all serde attributes, to allow #[serde(rename_all = "camelCase")] or similar
106    let struct_attrs: Vec<TokenStream> = ast.attrs.iter().map(|attr| quote! {#attr}).collect();
107    let mut property_modifications = vec![];
108    if let syn::Fields::Named(fields) = &mut struct_data.fields {
109        for field in &mut fields.named {
110            let XKube {
111                validations,
112                merge_strategy,
113                ..
114            } = match XKube::from_field(field) {
115                Ok(rule) => rule,
116                Err(err) => return err.write_errors(),
117            };
118
119            // Remove all unknown attributes from each field
120            // Has to happen on the original definition at all times, as we don't have #[derive] stanzes.
121            field.attrs = remove_attributes(&field.attrs, &attribute_whitelist);
122
123            if validations.is_empty() && merge_strategy.is_none() {
124                continue;
125            }
126
127            let rules: Vec<TokenStream> = validations
128                .iter()
129                .map(|rule| quote! {#kube_core::validate_property(merge, 0, #rule).unwrap();})
130                .collect();
131            let merge_strategy = merge_strategy
132                .map(|strategy| quote! {#kube_core::merge_strategy_property(merge, 0, #strategy).unwrap();});
133
134            // We need to prepend derive macros, as they were consumed by this macro processing, being a derive by itself.
135            property_modifications.push(quote! {
136                {
137                    #[derive(#serde::Serialize, #schemars::JsonSchema)]
138                    #(#struct_attrs)*
139                    #[automatically_derived]
140                    #[allow(missing_docs)]
141                    struct Validated {
142                        #field
143                    }
144
145                    let merge = &mut Validated::json_schema(generate);
146                    #(#rules)*
147                    #merge_strategy
148                    #kube_core::merge_properties(s, merge);
149                }
150            });
151        }
152    }
153
154    let schema_name = struct_name.as_str();
155    let generated_struct_name = struct_name.as_ident();
156
157    quote! {
158        impl #schemars::JsonSchema for #ident {
159            fn inline_schema() -> bool {
160                true
161            }
162
163            fn schema_name() -> #std::borrow::Cow<'static, str> {
164                #schema_name.into()
165            }
166
167            fn json_schema(generate: &mut #schemars::generate::SchemaGenerator) -> schemars::Schema {
168                #[derive(#serde::Serialize, #schemars::JsonSchema)]
169                #[automatically_derived]
170                #[allow(missing_docs)]
171                #ast
172
173                use #kube_core::{Rule, Message, Reason, ListMerge, MapMerge, StructMerge};
174                let s = &mut #generated_struct_name::json_schema(generate);
175                #(#struct_rules)*
176                #(#property_modifications)*
177                s.clone()
178            }
179        }
180    }
181}
182
183// Remove all unknown attributes from the list
184fn remove_attributes(attrs: &[Attribute], witelist: &[&str]) -> Vec<Attribute> {
185    attrs
186        .iter()
187        .filter(|attr| witelist.iter().any(|i| attr.path().is_ident(i)))
188        .cloned()
189        .collect()
190}
191
192#[test]
193fn test_derive_validated() {
194    let input = quote! {
195        #[derive(CustomResource, KubeSchema, Serialize, Deserialize, Debug, PartialEq, Clone)]
196        #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
197        #[x_kube(validation = "self != ''")]
198        struct FooSpec {
199            #[x_kube(validation = "self != ''")]
200            foo: String
201        }
202    };
203    let input = syn::parse2(input).unwrap();
204    let v = KubeSchema::from_derive_input(&input).unwrap();
205    assert_eq!(v.validations.len(), 1);
206}
207
208#[cfg(test)]
209mod tests {
210    use prettyplease::unparse;
211    use syn::parse::{Parse as _, Parser as _};
212
213    use super::*;
214    #[test]
215    fn test_derive_validated_full() {
216        let input = quote! {
217            #[derive(KubeSchema)]
218            #[x_kube(validation = "true", validation = "false")]
219            struct FooSpec {
220                #[x_kube(validation = "true", validation = Rule::new("false"))]
221                #[x_kube(merge_strategy = ListMerge::Atomic)]
222                foo: Vec<String>
223            }
224        };
225
226        let expected = quote! {
227            impl ::schemars::JsonSchema for FooSpec {
228                fn inline_schema() -> bool {
229                    true
230                }
231                fn schema_name() -> ::std::borrow::Cow<'static, str> {
232                    "FooSpecValidated".into()
233                }
234                fn json_schema(
235                    generate: &mut ::schemars::generate::SchemaGenerator,
236                ) -> schemars::Schema {
237                    #[derive(::serde::Serialize, ::schemars::JsonSchema)]
238                    #[automatically_derived]
239                    #[allow(missing_docs)]
240                    struct FooSpecValidated {
241                        foo: Vec<String>,
242                    }
243                    use ::kube::core::{Rule, Message, Reason, ListMerge, MapMerge, StructMerge};
244                    let s = &mut FooSpecValidated::json_schema(generate);
245                    ::kube::core::validate(s, "true").unwrap();
246                    ::kube::core::validate(s, "false").unwrap();
247                    {
248                        #[derive(::serde::Serialize, ::schemars::JsonSchema)]
249                        #[automatically_derived]
250                        #[allow(missing_docs)]
251                        struct Validated {
252                            foo: Vec<String>,
253                        }
254                        let merge = &mut Validated::json_schema(generate);
255                        ::kube::core::validate_property(merge, 0, "true").unwrap();
256                        ::kube::core::validate_property(merge, 0, Rule::new("false")).unwrap();
257                        ::kube::core::merge_strategy_property(merge, 0, ListMerge::Atomic).unwrap();
258                        ::kube::core::merge_properties(s, merge);
259                    }
260                    s.clone()
261                }
262            }
263        };
264
265        let output = derive_validated_schema(input);
266        let output = unparse(&syn::File::parse.parse2(output).unwrap());
267        let expected = unparse(&syn::File::parse.parse2(expected).unwrap());
268        assert_eq!(output, expected);
269    }
270}