Skip to main content

vld_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Expr, Fields, Lit, Meta};
4
5/// Derive macro that generates `vld_parse()`, `parse_value()`, `validate_fields()`,
6/// and `parse_lenient()` methods for a struct, plus implements the `VldParse` trait.
7///
8/// # Usage
9///
10/// ```ignore
11/// use vld::Validate;
12///
13/// #[derive(Debug, Validate)]
14/// struct User {
15///     #[vld(vld::string().min(2).max(50))]
16///     name: String,
17///     #[vld(vld::string().email())]
18///     email: String,
19///     #[vld(vld::number().int().gte(18).optional())]
20///     age: Option<i64>,
21/// }
22///
23/// let user = User::vld_parse(r#"{"name": "Alex", "email": "a@b.com"}"#).unwrap();
24/// ```
25///
26/// # Serde rename support
27///
28/// The derive macro respects `#[serde(rename = "...")]` on fields and
29/// `#[serde(rename_all = "...")]` on the struct:
30///
31/// ```ignore
32/// #[derive(Debug, serde::Serialize, Validate)]
33/// #[serde(rename_all = "camelCase")]
34/// struct ApiRequest {
35///     #[vld(vld::string().min(2))]
36///     first_name: String,
37///     #[vld(vld::string().email())]
38///     email_address: String,
39/// }
40/// // Parses from {"firstName": "...", "emailAddress": "..."}
41/// ```
42///
43/// Supported rename_all conventions: `camelCase`, `PascalCase`, `snake_case`,
44/// `SCREAMING_SNAKE_CASE`, `kebab-case`, `SCREAMING-KEBAB-CASE`.
45///
46/// The expression inside `#[vld(...)]` is used as-is in the generated code.
47/// Make sure the types are in scope (e.g., use `vld::string()` or import via prelude).
48#[proc_macro_derive(Validate, attributes(vld, into_params))]
49pub fn derive_validate(input: TokenStream) -> TokenStream {
50    let input = parse_macro_input!(input as DeriveInput);
51    let name = &input.ident;
52
53    // Check for #[serde(rename_all = "...")]
54    let rename_all = get_serde_rename_all(&input.attrs);
55    let parameter_in_expr = match get_into_params_parameter_in(&input.attrs).as_deref() {
56        Some("query") => quote! { Some("query") },
57        Some("path") => quote! { Some("path") },
58        Some("header") => quote! { Some("header") },
59        Some("cookie") => quote! { Some("cookie") },
60        _ => quote! { None },
61    };
62
63    let fields = match &input.data {
64        Data::Struct(data) => match &data.fields {
65            Fields::Named(fields) => &fields.named,
66            _ => panic!("Validate can only be derived for structs with named fields"),
67        },
68        _ => panic!("Validate can only be derived for structs"),
69    };
70
71    let mut field_names = Vec::new();
72    let mut field_types = Vec::new();
73    let mut field_schemas = Vec::new();
74    let mut field_json_keys = Vec::new();
75
76    for field in fields {
77        let fname = field.ident.as_ref().unwrap();
78        let ftype = &field.ty;
79        field_names.push(fname.clone());
80        field_types.push(ftype.clone());
81
82        // Determine JSON key: #[serde(rename = "...")] > rename_all > field name
83        let json_key = get_serde_rename(&field.attrs).unwrap_or_else(|| {
84            if let Some(ref convention) = rename_all {
85                rename_field(&fname.to_string(), convention)
86            } else {
87                fname.to_string()
88            }
89        });
90        field_json_keys.push(json_key);
91
92        let schema_tokens = field
93            .attrs
94            .iter()
95            .find(|attr| attr.path().is_ident("vld"))
96            .map(|attr| attr.parse_args::<proc_macro2::TokenStream>().unwrap())
97            .unwrap_or_else(|| panic!("Field `{}` is missing #[vld(...)] attribute", fname));
98
99        field_schemas.push(schema_tokens);
100    }
101
102    let expanded = quote! {
103        impl #name {
104            /// Parse and validate input data into this struct.
105            ///
106            /// Named `vld_parse` to avoid conflicts with other derive macros
107            /// (e.g. `clap::Parser::parse()`).
108            pub fn vld_parse<__VldInputT: ::vld::input::VldInput + ?Sized>(
109                input: &__VldInputT,
110            ) -> ::std::result::Result<#name, ::vld::error::VldError> {
111                let __vld_json = <__VldInputT as ::vld::input::VldInput>::to_json_value(input)?;
112                Self::parse_value(&__vld_json)
113            }
114
115            /// Parse and validate directly from a `serde_json::Value`.
116            pub fn parse_value(
117                __vld_json: &::vld::serde_json::Value,
118            ) -> ::std::result::Result<#name, ::vld::error::VldError> {
119                use ::vld::schema::VldSchema as _;
120
121                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
122                    ::vld::error::VldError::single(
123                        ::vld::error::IssueCode::InvalidType {
124                            expected: ::std::string::String::from("object"),
125                            received: ::vld::error::value_type_name(__vld_json),
126                        },
127                        ::std::format!(
128                            "Expected object, received {}",
129                            ::vld::error::value_type_name(__vld_json)
130                        ),
131                    )
132                })?;
133
134                let mut __vld_errors = ::vld::error::VldError::new();
135
136                #(
137                    #[allow(non_snake_case)]
138                    let #field_names: ::std::option::Option<#field_types> = {
139                        let __vld_field_schema = { #field_schemas };
140                        let __vld_field_value = __vld_obj
141                            .get(#field_json_keys)
142                            .unwrap_or(&::vld::serde_json::Value::Null);
143                        match __vld_field_schema.parse_value(__vld_field_value) {
144                            ::std::result::Result::Ok(v) => ::std::option::Option::Some(v),
145                            ::std::result::Result::Err(e) => {
146                                __vld_errors = ::vld::error::VldError::merge(
147                                    __vld_errors,
148                                    ::vld::error::VldError::with_prefix(
149                                        e,
150                                        ::vld::error::PathSegment::Field(
151                                            ::std::string::String::from(#field_json_keys),
152                                        ),
153                                    ),
154                                );
155                                ::std::option::Option::None
156                            }
157                        }
158                    };
159                )*
160
161                if !::vld::error::VldError::is_empty(&__vld_errors) {
162                    return ::std::result::Result::Err(__vld_errors);
163                }
164
165                ::std::result::Result::Ok(#name {
166                    #( #field_names: #field_names.unwrap(), )*
167                })
168            }
169
170            /// Validate each field individually and return per-field results.
171            pub fn validate_fields<__VldInputT: ::vld::input::VldInput + ?Sized>(
172                input: &__VldInputT,
173            ) -> ::std::result::Result<
174                ::std::vec::Vec<::vld::error::FieldResult>,
175                ::vld::error::VldError,
176            > {
177                let __vld_json = <__VldInputT as ::vld::input::VldInput>::to_json_value(input)?;
178                Self::validate_fields_value(&__vld_json)
179            }
180
181            /// Validate each field individually from a `serde_json::Value`.
182            pub fn validate_fields_value(
183                __vld_json: &::vld::serde_json::Value,
184            ) -> ::std::result::Result<
185                ::std::vec::Vec<::vld::error::FieldResult>,
186                ::vld::error::VldError,
187            > {
188                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
189                    ::vld::error::VldError::single(
190                        ::vld::error::IssueCode::InvalidType {
191                            expected: ::std::string::String::from("object"),
192                            received: ::vld::error::value_type_name(__vld_json),
193                        },
194                        ::std::format!(
195                            "Expected object, received {}",
196                            ::vld::error::value_type_name(__vld_json)
197                        ),
198                    )
199                })?;
200
201                let mut __vld_results: ::std::vec::Vec<::vld::error::FieldResult> =
202                    ::std::vec::Vec::new();
203
204                #(
205                    {
206                        let __vld_field_schema = { #field_schemas };
207                        let __vld_field_value = __vld_obj
208                            .get(#field_json_keys)
209                            .unwrap_or(&::vld::serde_json::Value::Null);
210
211                        let __vld_result = ::vld::object::DynSchema::dyn_parse(
212                            &__vld_field_schema,
213                            __vld_field_value,
214                        );
215
216                        __vld_results.push(::vld::error::FieldResult {
217                            name: ::std::string::String::from(#field_json_keys),
218                            input: __vld_field_value.clone(),
219                            result: __vld_result,
220                        });
221                    }
222                )*
223
224                ::std::result::Result::Ok(__vld_results)
225            }
226
227            /// Parse leniently: build the struct even when some fields fail.
228            pub fn parse_lenient<__VldInputT: ::vld::input::VldInput + ?Sized>(
229                input: &__VldInputT,
230            ) -> ::std::result::Result<
231                ::vld::error::ParseResult<#name>,
232                ::vld::error::VldError,
233            > {
234                let __vld_json = <__VldInputT as ::vld::input::VldInput>::to_json_value(input)?;
235                Self::parse_lenient_value(&__vld_json)
236            }
237
238            /// Parse leniently from a `serde_json::Value`.
239            pub fn parse_lenient_value(
240                __vld_json: &::vld::serde_json::Value,
241            ) -> ::std::result::Result<
242                ::vld::error::ParseResult<#name>,
243                ::vld::error::VldError,
244            > {
245                use ::vld::schema::VldSchema as _;
246
247                let __vld_obj = __vld_json.as_object().ok_or_else(|| {
248                    ::vld::error::VldError::single(
249                        ::vld::error::IssueCode::InvalidType {
250                            expected: ::std::string::String::from("object"),
251                            received: ::vld::error::value_type_name(__vld_json),
252                        },
253                        ::std::format!(
254                            "Expected object, received {}",
255                            ::vld::error::value_type_name(__vld_json)
256                        ),
257                    )
258                })?;
259
260                let mut __vld_results: ::std::vec::Vec<::vld::error::FieldResult> =
261                    ::std::vec::Vec::new();
262
263                #(
264                    #[allow(non_snake_case)]
265                    let #field_names: #field_types = {
266                        let __vld_field_schema = { #field_schemas };
267                        let __vld_field_value = __vld_obj
268                            .get(#field_json_keys)
269                            .unwrap_or(&::vld::serde_json::Value::Null);
270
271                        match __vld_field_schema.parse_value(__vld_field_value) {
272                            ::std::result::Result::Ok(v) => {
273                                let __json_repr = ::vld::serde_json::to_value(&v)
274                                    .unwrap_or_else(|_| __vld_field_value.clone());
275                                __vld_results.push(::vld::error::FieldResult {
276                                    name: ::std::string::String::from(#field_json_keys),
277                                    input: __vld_field_value.clone(),
278                                    result: ::std::result::Result::Ok(__json_repr),
279                                });
280                                v
281                            }
282                            ::std::result::Result::Err(e) => {
283                                __vld_results.push(::vld::error::FieldResult {
284                                    name: ::std::string::String::from(#field_json_keys),
285                                    input: __vld_field_value.clone(),
286                                    result: ::std::result::Result::Err(e),
287                                });
288                                <#field_types as ::std::default::Default>::default()
289                            }
290                        }
291                    };
292                )*
293
294                let __vld_struct = #name {
295                    #( #field_names, )*
296                };
297
298                ::std::result::Result::Ok(
299                    ::vld::error::ParseResult::new(__vld_struct, __vld_results)
300                )
301            }
302        }
303
304        impl ::vld::schema::VldParse for #name {
305            fn vld_parse_value(
306                value: &::vld::serde_json::Value,
307            ) -> ::std::result::Result<Self, ::vld::error::VldError> {
308                Self::parse_value(value)
309            }
310        }
311
312        ::vld::__vld_if_openapi! {
313            impl #name {
314                /// Generate a JSON Schema / OpenAPI 3.1 representation of this struct.
315                ///
316                /// Requires the `openapi` feature on `vld`.
317                pub fn json_schema() -> ::vld::serde_json::Value {
318                    use ::vld::json_schema::JsonSchema as _;
319                    let mut __vld_properties = ::vld::serde_json::Map::new();
320                    let mut __vld_required: ::std::vec::Vec<::std::string::String> =
321                        ::std::vec::Vec::new();
322
323                    #(
324                        {
325                            let __vld_field_schema = { #field_schemas };
326                            __vld_properties.insert(
327                                ::std::string::String::from(#field_json_keys),
328                                __vld_field_schema.json_schema(),
329                            );
330                            __vld_required.push(
331                                ::std::string::String::from(#field_json_keys),
332                            );
333                        }
334                    )*
335
336                    ::vld::serde_json::json!({
337                        "type": "object",
338                        "required": __vld_required,
339                        "properties": ::vld::serde_json::Value::Object(__vld_properties),
340                    })
341                }
342
343                /// Wrap `json_schema()` in a minimal OpenAPI 3.1 document.
344                ///
345                /// Requires the `openapi` feature on `vld`.
346                pub fn to_openapi_document() -> ::vld::serde_json::Value {
347                    ::vld::json_schema::to_openapi_document(
348                        stringify!(#name),
349                        &Self::json_schema(),
350                    )
351                }
352            }
353
354            impl ::vld::json_schema::OpenApiParameterIn for #name {
355                fn parameter_in() -> Option<&'static str> {
356                    #parameter_in_expr
357                }
358            }
359        }
360    };
361
362    TokenStream::from(expanded)
363}
364
365// ---------------------------------------------------------------------------
366// Serde attribute parsing helpers
367// ---------------------------------------------------------------------------
368
369/// Extract `#[serde(rename_all = "...")]` from struct-level attributes.
370fn get_serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
371    for attr in attrs {
372        if !attr.path().is_ident("serde") {
373            continue;
374        }
375        if let Ok(nested) = attr
376            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
377        {
378            for meta in &nested {
379                if let Meta::NameValue(nv) = meta {
380                    if nv.path.is_ident("rename_all") {
381                        if let Expr::Lit(lit) = &nv.value {
382                            if let Lit::Str(s) = &lit.lit {
383                                return Some(s.value());
384                            }
385                        }
386                    }
387                }
388            }
389        }
390    }
391    None
392}
393
394/// Extract `#[into_params(parameter_in = ...)]` from struct-level attributes.
395fn get_into_params_parameter_in(attrs: &[syn::Attribute]) -> Option<String> {
396    for attr in attrs {
397        if !attr.path().is_ident("into_params") {
398            continue;
399        }
400        if let Ok(nested) = attr
401            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
402        {
403            for meta in &nested {
404                if let Meta::NameValue(nv) = meta {
405                    if nv.path.is_ident("parameter_in") {
406                        if let Expr::Path(path) = &nv.value {
407                            let ident = path.path.get_ident()?;
408                            return Some(ident.to_string().to_ascii_lowercase());
409                        }
410                    }
411                }
412            }
413        }
414    }
415    None
416}
417
418/// Extract `#[serde(rename = "...")]` from field-level attributes.
419fn get_serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
420    for attr in attrs {
421        if !attr.path().is_ident("serde") {
422            continue;
423        }
424        if let Ok(nested) = attr
425            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
426        {
427            for meta in &nested {
428                if let Meta::NameValue(nv) = meta {
429                    if nv.path.is_ident("rename") {
430                        if let Expr::Lit(lit) = &nv.value {
431                            if let Lit::Str(s) = &lit.lit {
432                                return Some(s.value());
433                            }
434                        }
435                    }
436                }
437            }
438        }
439    }
440    None
441}
442
443/// Convert a snake_case field name to the given naming convention.
444fn rename_field(name: &str, convention: &str) -> String {
445    match convention {
446        "camelCase" => to_camel_case(name),
447        "PascalCase" => to_pascal_case(name),
448        "snake_case" => name.to_string(),
449        "SCREAMING_SNAKE_CASE" => name.to_uppercase(),
450        "kebab-case" => name.replace('_', "-"),
451        "SCREAMING-KEBAB-CASE" => name.replace('_', "-").to_uppercase(),
452        _ => name.to_string(),
453    }
454}
455
456fn to_camel_case(s: &str) -> String {
457    let mut result = String::new();
458    let mut capitalize_next = false;
459    for ch in s.chars() {
460        if ch == '_' {
461            capitalize_next = true;
462        } else if capitalize_next {
463            result.extend(ch.to_uppercase());
464            capitalize_next = false;
465        } else {
466            result.push(ch);
467        }
468    }
469    result
470}
471
472fn to_pascal_case(s: &str) -> String {
473    let mut result = String::new();
474    let mut capitalize_next = true;
475    for ch in s.chars() {
476        if ch == '_' {
477            capitalize_next = true;
478        } else if capitalize_next {
479            result.extend(ch.to_uppercase());
480            capitalize_next = false;
481        } else {
482            result.push(ch);
483        }
484    }
485    result
486}