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
38impl 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 } }
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 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 ast.ident = struct_name.as_ident().clone();
94
95 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 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 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 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
183fn 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}