Skip to main content

rustia_macros/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Proc-macro derive implementation for `rustia`.
4
5use std::collections::{BTreeSet, HashMap};
6
7use heck::{
8    ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase,
9    ToUpperCamelCase,
10};
11use proc_macro::TokenStream;
12use proc_macro_crate::{FoundCrate, crate_name};
13use proc_macro2::{Span, TokenStream as TokenStream2};
14use quote::{ToTokens, quote};
15use syn::{
16    Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericParam, Ident, LitBool, LitInt,
17    LitStr, Token, Type, TypePath, meta::ParseNestedMeta, parse_macro_input,
18    punctuated::Punctuated, spanned::Spanned,
19};
20
21#[proc_macro_derive(LLMData, attributes(rustia, serde))]
22pub fn derive_llm_data(input: TokenStream) -> TokenStream {
23    let input = parse_macro_input!(input as DeriveInput);
24    match expand_llm_data(&input) {
25        Ok(tokens) => tokens.into(),
26        Err(error) => error.into_compile_error().into(),
27    }
28}
29
30fn expand_llm_data(input: &DeriveInput) -> syn::Result<TokenStream2> {
31    match input.data {
32        Data::Struct(_) | Data::Enum(_) => {}
33        Data::Union(_) => {
34            return Err(syn::Error::new_spanned(
35                input,
36                "`LLMData` can only be derived for structs and enums",
37            ));
38        }
39    }
40
41    let rustia_path = rustia_path();
42    let ident = &input.ident;
43    let validate_generics = add_validate_bounds(&input.generics, &rustia_path);
44    let (impl_generics, ty_generics, where_clause) = validate_generics.split_for_impl();
45
46    let validate_impl = match &input.data {
47        Data::Struct(data) => expand_struct_validate(input, data, &rustia_path)?,
48        Data::Enum(data) => expand_enum_validate(input, data, &rustia_path)?,
49        Data::Union(_) => unreachable!(),
50    };
51
52    Ok(quote! {
53        impl #impl_generics #rustia_path::LLMData for #ident #ty_generics #where_clause {}
54        #validate_impl
55    })
56}
57
58fn expand_struct_validate(
59    input: &DeriveInput,
60    data: &DataStruct,
61    rustia_path: &TokenStream2,
62) -> syn::Result<TokenStream2> {
63    let ident = &input.ident;
64    let validate_generics = add_validate_bounds(&input.generics, rustia_path);
65    let (impl_generics, ty_generics, where_clause) = validate_generics.split_for_impl();
66
67    let body = match &data.fields {
68        Fields::Named(fields) => {
69            let struct_options = parse_struct_serde_options(input)?;
70            expand_named_struct_validate(fields, &struct_options, rustia_path)?
71        }
72        Fields::Unnamed(_) | Fields::Unit => {
73            quote! {
74                if __strict {
75                    #rustia_path::__private::validate_with_serde::<Self>(__input)
76                } else {
77                    #rustia_path::__private::validate_with_serde::<Self>(__input)
78                }
79            }
80        }
81    };
82
83    Ok(quote! {
84        impl #impl_generics #rustia_path::Validate for #ident #ty_generics #where_clause {
85            fn validate(value: #rustia_path::serde_json::Value) -> #rustia_path::IValidation<Self> {
86                let __input = value;
87                let __strict = false;
88                #body
89            }
90
91            fn validate_equals(value: #rustia_path::serde_json::Value) -> #rustia_path::IValidation<Self> {
92                let __input = value;
93                let __strict = true;
94                #body
95            }
96        }
97    })
98}
99
100fn expand_named_struct_validate(
101    fields: &syn::FieldsNamed,
102    struct_options: &StructSerdeOptions,
103    rustia_path: &TokenStream2,
104) -> syn::Result<TokenStream2> {
105    let mut field_blocks = Vec::new();
106    let mut known_fields = Vec::new();
107    let mut has_flatten = false;
108
109    for field in &fields.named {
110        let field_ty = &field.ty;
111        let field_options = field_serde_options(field, struct_options)?;
112        if field_options.flatten {
113            has_flatten = true;
114        }
115        let field_name = field_options.wire_name;
116        let field_name_lit = LitStr::new(&field_name, Span::call_site());
117        if !field_options.flatten {
118            known_fields.push(field_name_lit.clone());
119        }
120
121        let tags = parse_rustia_tags(&field.attrs)?;
122        validate_tags_for_type(&tags, &field.ty)?;
123
124        if field_options.skip_deserializing {
125            // Serde skips these fields during deserialization and injects
126            // defaults instead, so runtime validation must not enforce
127            // requiredness or field-level value constraints for input keys.
128            continue;
129        }
130
131        let optional = is_option_type(field_ty) || field_options.has_default;
132        let apply_tags = if tags.is_empty() {
133            quote! {}
134        } else {
135            let tag_tokens = quote_runtime_tags(&tags, rustia_path);
136            if optional {
137                quote! {
138                    if !__field_value.is_null() {
139                        let __rustia_tags = #tag_tokens;
140                        #rustia_path::__private::apply_tags(__field_value, &__field_path, &__rustia_tags, &mut __errors);
141                    }
142                }
143            } else {
144                quote! {
145                    let __rustia_tags = #tag_tokens;
146                    #rustia_path::__private::apply_tags(__field_value, &__field_path, &__rustia_tags, &mut __errors);
147                }
148            }
149        };
150
151        let missing_behavior = if optional {
152            quote! {}
153        } else {
154            quote! {
155                __errors.push(#rustia_path::IValidationError {
156                    path: __field_path,
157                    expected: "required property".to_owned(),
158                    value: #rustia_path::serde_json::Value::Null,
159                    description: Some("missing required field".to_owned()),
160                });
161            }
162        };
163
164        if field_options.flatten {
165            field_blocks.push(quote! {
166                // Flattened fields consume keys from the parent object.
167                // Keep this validation non-strict even in validate_equals to
168                // avoid false unknown-field errors from sibling parent keys.
169                // Remove this workaround once strict flattened-key ownership
170                // analysis is implemented in the derive validator.
171                let __validated_field = <#field_ty as #rustia_path::Validate>::validate(__root.clone());
172                match __validated_field {
173                    #rustia_path::IValidation::Success { .. } => {}
174                    #rustia_path::IValidation::Failure { errors: __nested_errors, .. } => {
175                        #rustia_path::__private::merge_prefixed_errors(
176                            &mut __errors,
177                            "$input",
178                            __nested_errors,
179                        );
180                    }
181                }
182            });
183        } else {
184            field_blocks.push(quote! {
185                let __field_path = #rustia_path::__private::join_object_path("$input", #field_name_lit);
186                match __object.get(#field_name_lit) {
187                    Some(__field_value) => {
188                        let __validated_field = if __strict {
189                            <#field_ty as #rustia_path::Validate>::validate_equals(__field_value.clone())
190                        } else {
191                            <#field_ty as #rustia_path::Validate>::validate(__field_value.clone())
192                        };
193                        match __validated_field {
194                            #rustia_path::IValidation::Success { .. } => {}
195                            #rustia_path::IValidation::Failure { errors: __nested_errors, .. } => {
196                                #rustia_path::__private::merge_prefixed_errors(
197                                    &mut __errors,
198                                    &__field_path,
199                                    __nested_errors,
200                                );
201                            }
202                        }
203                        #apply_tags
204                    }
205                    None => {
206                        #missing_behavior
207                    }
208                }
209            });
210        }
211    }
212
213    let strict_unknown_check = if has_flatten {
214        quote! {}
215    } else {
216        quote! {
217            if __strict {
218                let __known_fields = [#(#known_fields),*];
219                for __unknown_key in __object.keys() {
220                    if !__known_fields.contains(&__unknown_key.as_str()) {
221                        __errors.push(#rustia_path::IValidationError {
222                            path: #rustia_path::__private::join_object_path("$input", __unknown_key),
223                            expected: "undefined".to_owned(),
224                            value: __object
225                                .get(__unknown_key)
226                                .cloned()
227                                .unwrap_or(#rustia_path::serde_json::Value::Null),
228                            description: Some("unexpected property".to_owned()),
229                        });
230                    }
231                }
232            }
233        }
234    };
235
236    Ok(quote! {
237        let __root = __input.clone();
238        let __object = match __root.as_object() {
239            Some(__object) => __object,
240            None => {
241                return #rustia_path::IValidation::Failure {
242                    data: __root.clone(),
243                    errors: vec![#rustia_path::IValidationError {
244                        path: "$input".to_owned(),
245                        expected: "object".to_owned(),
246                        value: __root,
247                        description: Some("expected an object value".to_owned()),
248                    }],
249                };
250            }
251        };
252
253        let mut __errors = Vec::<#rustia_path::IValidationError>::new();
254        #(#field_blocks)*
255        #strict_unknown_check
256
257        if __errors.is_empty() {
258            match #rustia_path::__private::validate_with_serde::<Self>(__root.clone()) {
259                #rustia_path::IValidation::Success { data } => #rustia_path::IValidation::Success { data },
260                #rustia_path::IValidation::Failure { errors, .. } => {
261                    #rustia_path::IValidation::Failure {
262                        data: __root,
263                        errors,
264                    }
265                }
266            }
267        } else {
268            #rustia_path::IValidation::Failure {
269                data: __root,
270                errors: __errors,
271            }
272        }
273    })
274}
275
276#[allow(clippy::enum_variant_names)]
277#[derive(Clone, Copy)]
278enum RenameRule {
279    LowerCase,
280    UpperCase,
281    PascalCase,
282    CamelCase,
283    SnakeCase,
284    ScreamingSnakeCase,
285    KebabCase,
286    ScreamingKebabCase,
287}
288
289impl RenameRule {
290    fn parse(literal: &LitStr) -> syn::Result<Self> {
291        match literal.value().as_str() {
292            "lowercase" => Ok(Self::LowerCase),
293            "UPPERCASE" => Ok(Self::UpperCase),
294            "PascalCase" => Ok(Self::PascalCase),
295            "camelCase" => Ok(Self::CamelCase),
296            "snake_case" => Ok(Self::SnakeCase),
297            "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
298            "kebab-case" => Ok(Self::KebabCase),
299            "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebabCase),
300            _ => Err(syn::Error::new(
301                literal.span(),
302                "unsupported serde rename rule",
303            )),
304        }
305    }
306
307    fn apply(self, value: &str) -> String {
308        match self {
309            Self::LowerCase => value.to_lowercase(),
310            Self::UpperCase => value.to_uppercase(),
311            Self::PascalCase => value.to_upper_camel_case(),
312            Self::CamelCase => value.to_lower_camel_case(),
313            Self::SnakeCase => value.to_snake_case(),
314            Self::ScreamingSnakeCase => value.to_shouty_snake_case(),
315            Self::KebabCase => value.to_kebab_case(),
316            Self::ScreamingKebabCase => value.to_shouty_kebab_case(),
317        }
318    }
319}
320
321struct StructSerdeOptions {
322    rename_all_deserialize: Option<RenameRule>,
323    default: bool,
324}
325
326struct FieldSerdeOptions {
327    wire_name: String,
328    has_default: bool,
329    flatten: bool,
330    skip_deserializing: bool,
331}
332
333fn expand_enum_validate(
334    input: &DeriveInput,
335    data: &DataEnum,
336    rustia_path: &TokenStream2,
337) -> syn::Result<TokenStream2> {
338    // Parse and validate tags on variant fields now, while enum runtime path
339    // still delegates to serde validation. This keeps compile-time diagnostics
340    // for tag syntax and target compatibility stable.
341    for variant in &data.variants {
342        match &variant.fields {
343            Fields::Named(fields) => {
344                for field in &fields.named {
345                    let tags = parse_rustia_tags(&field.attrs)?;
346                    validate_tags_for_type(&tags, &field.ty)?;
347                }
348            }
349            Fields::Unnamed(fields) => {
350                for field in &fields.unnamed {
351                    let tags = parse_rustia_tags(&field.attrs)?;
352                    validate_tags_for_type(&tags, &field.ty)?;
353                }
354            }
355            Fields::Unit => {}
356        }
357    }
358
359    let ident = &input.ident;
360    let validate_generics = add_validate_bounds(&input.generics, rustia_path);
361    let (impl_generics, ty_generics, where_clause) = validate_generics.split_for_impl();
362
363    Ok(quote! {
364        impl #impl_generics #rustia_path::Validate for #ident #ty_generics #where_clause {
365            fn validate(value: #rustia_path::serde_json::Value) -> #rustia_path::IValidation<Self> {
366                #rustia_path::__private::validate_with_serde::<Self>(value)
367            }
368
369            fn validate_equals(value: #rustia_path::serde_json::Value) -> #rustia_path::IValidation<Self> {
370                #rustia_path::__private::validate_with_serde::<Self>(value)
371            }
372        }
373    })
374}
375
376#[derive(Clone)]
377enum ParsedTag {
378    MinLength {
379        value: usize,
380        span: Span,
381    },
382    MaxLength {
383        value: usize,
384        span: Span,
385    },
386    MinItems {
387        value: usize,
388        span: Span,
389    },
390    MaxItems {
391        value: usize,
392        span: Span,
393    },
394    UniqueItems {
395        value: bool,
396        span: Span,
397    },
398    Minimum {
399        value: f64,
400        span: Span,
401    },
402    Maximum {
403        value: f64,
404        span: Span,
405    },
406    ExclusiveMinimum {
407        value: f64,
408        span: Span,
409    },
410    ExclusiveMaximum {
411        value: f64,
412        span: Span,
413    },
414    MultipleOf {
415        value: f64,
416        span: Span,
417    },
418    Pattern {
419        value: String,
420        span: Span,
421    },
422    Format {
423        value: String,
424        span: Span,
425    },
426    Type {
427        value: String,
428        span: Span,
429    },
430    Items {
431        tags: Vec<ParsedTag>,
432        span: Span,
433    },
434    Keys {
435        tags: Vec<ParsedTag>,
436        span: Span,
437    },
438    Values {
439        tags: Vec<ParsedTag>,
440        span: Span,
441    },
442    Metadata {
443        kind: String,
444        args: Vec<String>,
445        span: Span,
446    },
447}
448
449impl ParsedTag {
450    fn kind_name(&self) -> &str {
451        match self {
452            Self::MinLength { .. } => "minLength",
453            Self::MaxLength { .. } => "maxLength",
454            Self::MinItems { .. } => "minItems",
455            Self::MaxItems { .. } => "maxItems",
456            Self::UniqueItems { .. } => "uniqueItems",
457            Self::Minimum { .. } => "minimum",
458            Self::Maximum { .. } => "maximum",
459            Self::ExclusiveMinimum { .. } => "exclusiveMinimum",
460            Self::ExclusiveMaximum { .. } => "exclusiveMaximum",
461            Self::MultipleOf { .. } => "multipleOf",
462            Self::Pattern { .. } => "pattern",
463            Self::Format { .. } => "format",
464            Self::Type { .. } => "type",
465            Self::Items { .. } => "items",
466            Self::Keys { .. } => "keys",
467            Self::Values { .. } => "values",
468            Self::Metadata { kind, .. } => kind,
469        }
470    }
471
472    fn span(&self) -> Span {
473        match self {
474            Self::MinLength { span, .. }
475            | Self::MaxLength { span, .. }
476            | Self::MinItems { span, .. }
477            | Self::MaxItems { span, .. }
478            | Self::UniqueItems { span, .. }
479            | Self::Minimum { span, .. }
480            | Self::Maximum { span, .. }
481            | Self::ExclusiveMinimum { span, .. }
482            | Self::ExclusiveMaximum { span, .. }
483            | Self::MultipleOf { span, .. }
484            | Self::Pattern { span, .. }
485            | Self::Format { span, .. }
486            | Self::Type { span, .. }
487            | Self::Items { span, .. }
488            | Self::Keys { span, .. }
489            | Self::Values { span, .. }
490            | Self::Metadata { span, .. } => *span,
491        }
492    }
493
494    fn is_duplicate_exclusive(&self) -> bool {
495        if matches!(
496            self,
497            Self::MinLength { .. }
498                | Self::MaxLength { .. }
499                | Self::MinItems { .. }
500                | Self::MaxItems { .. }
501                | Self::UniqueItems { .. }
502                | Self::Minimum { .. }
503                | Self::Maximum { .. }
504                | Self::ExclusiveMinimum { .. }
505                | Self::ExclusiveMaximum { .. }
506                | Self::MultipleOf { .. }
507                | Self::Pattern { .. }
508                | Self::Format { .. }
509                | Self::Type { .. }
510        ) {
511            return true;
512        }
513
514        match self {
515            Self::Metadata { kind, .. } => {
516                kind == "default" || kind == "example" || kind == "examples" || kind == "sequence"
517            }
518            _ => false,
519        }
520    }
521}
522
523fn parse_rustia_tags(attrs: &[syn::Attribute]) -> syn::Result<Vec<ParsedTag>> {
524    let mut tags = Vec::new();
525    for attr in attrs {
526        if !attr.path().is_ident("rustia") {
527            continue;
528        }
529        attr.parse_nested_meta(|meta| {
530            if meta.path.is_ident("tags") {
531                parse_tag_list(meta, &mut tags)
532            } else {
533                Err(syn::Error::new_spanned(
534                    meta.path,
535                    "unsupported `#[rustia(...)]` item; expected `tags(...)`",
536                ))
537            }
538        })?;
539    }
540    Ok(tags)
541}
542
543fn parse_tag_list(meta: ParseNestedMeta<'_>, output: &mut Vec<ParsedTag>) -> syn::Result<()> {
544    meta.parse_nested_meta(|tag_meta| {
545        output.push(parse_one_tag(tag_meta)?);
546        Ok(())
547    })
548}
549
550fn parse_one_tag(meta: ParseNestedMeta<'_>) -> syn::Result<ParsedTag> {
551    let Some(ident) = meta.path.get_ident() else {
552        return Err(syn::Error::new_spanned(
553            meta.path,
554            "tag name must be a simple lowerCamelCase identifier",
555        ));
556    };
557    let name = ident.to_string();
558    let span = ident.span();
559    match name.as_str() {
560        "minLength" => Ok(ParsedTag::MinLength {
561            value: parse_usize_arg(&meta)?,
562            span,
563        }),
564        "maxLength" => Ok(ParsedTag::MaxLength {
565            value: parse_usize_arg(&meta)?,
566            span,
567        }),
568        "minItems" => Ok(ParsedTag::MinItems {
569            value: parse_usize_arg(&meta)?,
570            span,
571        }),
572        "maxItems" => Ok(ParsedTag::MaxItems {
573            value: parse_usize_arg(&meta)?,
574            span,
575        }),
576        "uniqueItems" => Ok(ParsedTag::UniqueItems {
577            value: parse_optional_bool_arg(&meta)?,
578            span,
579        }),
580        "minimum" => Ok(ParsedTag::Minimum {
581            value: parse_f64_arg(&meta)?,
582            span,
583        }),
584        "maximum" => Ok(ParsedTag::Maximum {
585            value: parse_f64_arg(&meta)?,
586            span,
587        }),
588        "exclusiveMinimum" => Ok(ParsedTag::ExclusiveMinimum {
589            value: parse_f64_arg(&meta)?,
590            span,
591        }),
592        "exclusiveMaximum" => Ok(ParsedTag::ExclusiveMaximum {
593            value: parse_f64_arg(&meta)?,
594            span,
595        }),
596        "multipleOf" => Ok(ParsedTag::MultipleOf {
597            value: parse_f64_arg(&meta)?,
598            span,
599        }),
600        "pattern" => Ok(ParsedTag::Pattern {
601            value: parse_string_arg(&meta)?,
602            span,
603        }),
604        "format" => {
605            let value = parse_string_arg(&meta)?;
606            let allowed = BTreeSet::from([
607                "byte",
608                "password",
609                "regex",
610                "uuid",
611                "email",
612                "hostname",
613                "idn-email",
614                "idn-hostname",
615                "iri",
616                "iri-reference",
617                "ipv4",
618                "ipv6",
619                "uri",
620                "uri-reference",
621                "uri-template",
622                "url",
623                "date-time",
624                "date",
625                "time",
626                "duration",
627                "json-pointer",
628                "relative-json-pointer",
629            ]);
630            if !allowed.contains(value.as_str()) {
631                return Err(syn::Error::new(
632                    span,
633                    format!("unsupported format `{value}`"),
634                ));
635            }
636            Ok(ParsedTag::Format { value, span })
637        }
638        "type" => {
639            let value = parse_string_arg(&meta)?;
640            let allowed = BTreeSet::from(["int32", "uint32", "int64", "uint64", "float", "double"]);
641            if !allowed.contains(value.as_str()) {
642                return Err(syn::Error::new(
643                    span,
644                    format!("unsupported type tag `{value}`"),
645                ));
646            }
647            Ok(ParsedTag::Type { value, span })
648        }
649        "items" => Ok(ParsedTag::Items {
650            tags: parse_nested_tags_group(&meta, "items")?,
651            span,
652        }),
653        "keys" => Ok(ParsedTag::Keys {
654            tags: parse_nested_tags_group(&meta, "keys")?,
655            span,
656        }),
657        "values" => Ok(ParsedTag::Values {
658            tags: parse_nested_tags_group(&meta, "values")?,
659            span,
660        }),
661        "default" | "example" | "examples" | "sequence" | "contentMediaType"
662        | "jsonSchemaPlugin" | "constant" => Ok(ParsedTag::Metadata {
663            kind: name,
664            args: parse_metadata_args(&meta)?,
665            span,
666        }),
667        _ => Err(syn::Error::new(
668            span,
669            format!("unsupported rustia tag `{name}`"),
670        )),
671    }
672}
673
674fn parse_nested_tags_group(
675    meta: &ParseNestedMeta<'_>,
676    context: &str,
677) -> syn::Result<Vec<ParsedTag>> {
678    let mut tags = Vec::new();
679    meta.parse_nested_meta(|nested| {
680        if nested.path.is_ident("tags") {
681            parse_tag_list(nested, &mut tags)
682        } else {
683            Err(syn::Error::new_spanned(
684                nested.path,
685                format!("`{context}(...)` expects `tags(...)`"),
686            ))
687        }
688    })?;
689    if tags.is_empty() {
690        return Err(syn::Error::new(
691            meta.path.span(),
692            format!("`{context}(...)` requires at least one nested tag"),
693        ));
694    }
695    Ok(tags)
696}
697
698fn parse_metadata_args(meta: &ParseNestedMeta<'_>) -> syn::Result<Vec<String>> {
699    if meta.input.is_empty() {
700        return Ok(Vec::new());
701    }
702
703    let content;
704    syn::parenthesized!(content in meta.input);
705    let exprs: Punctuated<syn::Expr, Token![,]> =
706        content.parse_terminated(|input| input.parse(), Token![,])?;
707    Ok(exprs
708        .iter()
709        .map(ToTokens::to_token_stream)
710        .map(|tokens| tokens.to_string())
711        .collect())
712}
713
714fn parse_usize_arg(meta: &ParseNestedMeta<'_>) -> syn::Result<usize> {
715    let content;
716    syn::parenthesized!(content in meta.input);
717    let lit: LitInt = content.parse()?;
718    if !content.is_empty() {
719        return Err(syn::Error::new(
720            content.span(),
721            "unexpected trailing tokens",
722        ));
723    }
724    lit.base10_parse()
725}
726
727fn parse_optional_bool_arg(meta: &ParseNestedMeta<'_>) -> syn::Result<bool> {
728    if meta.input.is_empty() {
729        return Ok(true);
730    }
731    let content;
732    syn::parenthesized!(content in meta.input);
733    if content.is_empty() {
734        return Ok(true);
735    }
736    let lit: LitBool = content.parse()?;
737    if !content.is_empty() {
738        return Err(syn::Error::new(
739            content.span(),
740            "unexpected trailing tokens",
741        ));
742    }
743    Ok(lit.value)
744}
745
746fn parse_f64_arg(meta: &ParseNestedMeta<'_>) -> syn::Result<f64> {
747    let content;
748    syn::parenthesized!(content in meta.input);
749    let expression: syn::Expr = content.parse()?;
750    if !content.is_empty() {
751        return Err(syn::Error::new(
752            content.span(),
753            "unexpected trailing tokens",
754        ));
755    }
756    parse_f64_expression(&expression)
757}
758
759fn parse_f64_expression(expression: &syn::Expr) -> syn::Result<f64> {
760    match expression {
761        syn::Expr::Lit(literal) => match &literal.lit {
762            syn::Lit::Float(value) => value.base10_parse(),
763            syn::Lit::Int(value) => value.base10_parse(),
764            _ => Err(syn::Error::new_spanned(
765                literal,
766                "expected a numeric literal",
767            )),
768        },
769        syn::Expr::Paren(paren) => parse_f64_expression(&paren.expr),
770        syn::Expr::Unary(unary) => match unary.op {
771            syn::UnOp::Neg(_) => Ok(-parse_f64_expression(&unary.expr)?),
772            _ => Err(syn::Error::new_spanned(
773                unary,
774                "expected a signed numeric literal",
775            )),
776        },
777        _ => Err(syn::Error::new_spanned(
778            expression,
779            "expected a numeric literal",
780        )),
781    }
782}
783
784fn parse_string_arg(meta: &ParseNestedMeta<'_>) -> syn::Result<String> {
785    let content;
786    syn::parenthesized!(content in meta.input);
787    let lit: LitStr = content.parse()?;
788    if !content.is_empty() {
789        return Err(syn::Error::new(
790            content.span(),
791            "unexpected trailing tokens",
792        ));
793    }
794    Ok(lit.value())
795}
796
797fn validate_tags_for_type(tags: &[ParsedTag], ty: &Type) -> syn::Result<()> {
798    if tags.is_empty() {
799        return Ok(());
800    }
801
802    check_exclusive_rules(tags)?;
803    validate_tag_targets(tags, ty)
804}
805
806fn check_exclusive_rules(tags: &[ParsedTag]) -> syn::Result<()> {
807    let mut seen = HashMap::<&str, Span>::new();
808    for tag in tags {
809        if tag.is_duplicate_exclusive()
810            && let Some(previous) = seen.insert(tag.kind_name(), tag.span())
811        {
812            return Err(syn::Error::new(
813                tag.span(),
814                format!(
815                    "tag `{}` cannot be declared multiple times (previous declaration at {:?})",
816                    tag.kind_name(),
817                    previous,
818                ),
819            ));
820        }
821    }
822
823    let has_format = tags
824        .iter()
825        .any(|tag| matches!(tag, ParsedTag::Format { .. }));
826    let has_pattern = tags
827        .iter()
828        .any(|tag| matches!(tag, ParsedTag::Pattern { .. }));
829    if has_format && has_pattern {
830        return Err(syn::Error::new(
831            Span::call_site(),
832            "`format(...)` and `pattern(...)` are mutually exclusive",
833        ));
834    }
835
836    let has_minimum = tags
837        .iter()
838        .any(|tag| matches!(tag, ParsedTag::Minimum { .. }));
839    let has_exclusive_minimum = tags
840        .iter()
841        .any(|tag| matches!(tag, ParsedTag::ExclusiveMinimum { .. }));
842    if has_minimum && has_exclusive_minimum {
843        return Err(syn::Error::new(
844            Span::call_site(),
845            "`minimum(...)` and `exclusiveMinimum(...)` are mutually exclusive",
846        ));
847    }
848
849    let has_maximum = tags
850        .iter()
851        .any(|tag| matches!(tag, ParsedTag::Maximum { .. }));
852    let has_exclusive_maximum = tags
853        .iter()
854        .any(|tag| matches!(tag, ParsedTag::ExclusiveMaximum { .. }));
855    if has_maximum && has_exclusive_maximum {
856        return Err(syn::Error::new(
857            Span::call_site(),
858            "`maximum(...)` and `exclusiveMaximum(...)` are mutually exclusive",
859        ));
860    }
861
862    Ok(())
863}
864
865fn validate_tag_targets(tags: &[ParsedTag], ty: &Type) -> syn::Result<()> {
866    let unwrapped = unwrap_option_type(ty);
867    if let Some((key, _)) = extract_map_types(unwrapped)
868        && !is_string_type(key)
869    {
870        return Err(syn::Error::new_spanned(
871            key,
872            "map key type must be `String` for rustia derive validation",
873        ));
874    }
875
876    for tag in tags {
877        match tag {
878            ParsedTag::MinLength { .. }
879            | ParsedTag::MaxLength { .. }
880            | ParsedTag::Pattern { .. }
881            | ParsedTag::Format { .. } => {
882                if !is_string_type(unwrapped) {
883                    return Err(syn::Error::new_spanned(
884                        unwrapped,
885                        format!(
886                            "tag `{}` can only be applied to string targets",
887                            tag.kind_name()
888                        ),
889                    ));
890                }
891            }
892            ParsedTag::MinItems { .. }
893            | ParsedTag::MaxItems { .. }
894            | ParsedTag::UniqueItems { .. } => {
895                if extract_array_item_type(unwrapped).is_none() {
896                    return Err(syn::Error::new_spanned(
897                        unwrapped,
898                        format!(
899                            "tag `{}` can only be applied to array targets",
900                            tag.kind_name()
901                        ),
902                    ));
903                }
904            }
905            ParsedTag::Minimum { .. }
906            | ParsedTag::Maximum { .. }
907            | ParsedTag::ExclusiveMinimum { .. }
908            | ParsedTag::ExclusiveMaximum { .. }
909            | ParsedTag::MultipleOf { .. }
910            | ParsedTag::Type { .. } => {
911                if !is_number_type(unwrapped) {
912                    return Err(syn::Error::new_spanned(
913                        unwrapped,
914                        format!(
915                            "tag `{}` can only be applied to numeric targets",
916                            tag.kind_name()
917                        ),
918                    ));
919                }
920            }
921            ParsedTag::Items { tags: nested, .. } => {
922                let Some(item_ty) = extract_array_item_type(unwrapped) else {
923                    return Err(syn::Error::new_spanned(
924                        unwrapped,
925                        "`items(tags(...))` can only be applied to array targets",
926                    ));
927                };
928                validate_tags_for_type(nested, item_ty)?;
929            }
930            ParsedTag::Keys { tags: nested, .. } => {
931                let Some((key_ty, _)) = extract_map_types(unwrapped) else {
932                    return Err(syn::Error::new_spanned(
933                        unwrapped,
934                        "`keys(tags(...))` can only be applied to map targets",
935                    ));
936                };
937                if !is_string_type(key_ty) {
938                    return Err(syn::Error::new_spanned(
939                        key_ty,
940                        "map key type must be `String` for `keys(tags(...))`",
941                    ));
942                }
943                validate_tags_for_type(nested, key_ty)?;
944            }
945            ParsedTag::Values { tags: nested, .. } => {
946                let Some((_, value_ty)) = extract_map_types(unwrapped) else {
947                    return Err(syn::Error::new_spanned(
948                        unwrapped,
949                        "`values(tags(...))` can only be applied to map targets",
950                    ));
951                };
952                validate_tags_for_type(nested, value_ty)?;
953            }
954            ParsedTag::Metadata { .. } => {}
955        }
956    }
957    Ok(())
958}
959
960fn quote_runtime_tags(tags: &[ParsedTag], rustia_path: &TokenStream2) -> TokenStream2 {
961    let tags = tags.iter().map(|tag| quote_runtime_tag(tag, rustia_path));
962    quote!(::std::vec![#(#tags),*])
963}
964
965fn quote_runtime_tag(tag: &ParsedTag, rustia_path: &TokenStream2) -> TokenStream2 {
966    match tag {
967        ParsedTag::MinLength { value, .. } => quote!(#rustia_path::TagRuntime::MinLength(#value)),
968        ParsedTag::MaxLength { value, .. } => quote!(#rustia_path::TagRuntime::MaxLength(#value)),
969        ParsedTag::MinItems { value, .. } => quote!(#rustia_path::TagRuntime::MinItems(#value)),
970        ParsedTag::MaxItems { value, .. } => quote!(#rustia_path::TagRuntime::MaxItems(#value)),
971        ParsedTag::UniqueItems { value, .. } => {
972            quote!(#rustia_path::TagRuntime::UniqueItems(#value))
973        }
974        ParsedTag::Minimum { value, .. } => quote!(#rustia_path::TagRuntime::Minimum(#value)),
975        ParsedTag::Maximum { value, .. } => quote!(#rustia_path::TagRuntime::Maximum(#value)),
976        ParsedTag::ExclusiveMinimum { value, .. } => {
977            quote!(#rustia_path::TagRuntime::ExclusiveMinimum(#value))
978        }
979        ParsedTag::ExclusiveMaximum { value, .. } => {
980            quote!(#rustia_path::TagRuntime::ExclusiveMaximum(#value))
981        }
982        ParsedTag::MultipleOf { value, .. } => quote!(#rustia_path::TagRuntime::MultipleOf(#value)),
983        ParsedTag::Pattern { value, .. } => {
984            quote!(#rustia_path::TagRuntime::Pattern(::std::string::String::from(#value)))
985        }
986        ParsedTag::Format { value, .. } => {
987            quote!(#rustia_path::TagRuntime::Format(::std::string::String::from(#value)))
988        }
989        ParsedTag::Type { value, .. } => {
990            quote!(#rustia_path::TagRuntime::Type(::std::string::String::from(#value)))
991        }
992        ParsedTag::Items { tags, .. } => {
993            let inner = quote_runtime_tags(tags, rustia_path);
994            quote!(#rustia_path::TagRuntime::Items(#inner))
995        }
996        ParsedTag::Keys { tags, .. } => {
997            let inner = quote_runtime_tags(tags, rustia_path);
998            quote!(#rustia_path::TagRuntime::Keys(#inner))
999        }
1000        ParsedTag::Values { tags, .. } => {
1001            let inner = quote_runtime_tags(tags, rustia_path);
1002            quote!(#rustia_path::TagRuntime::Values(#inner))
1003        }
1004        ParsedTag::Metadata { kind, args, .. } => {
1005            quote!(#rustia_path::TagRuntime::Metadata {
1006                kind: ::std::string::String::from(#kind),
1007                args: ::std::vec![#(::std::string::String::from(#args)),*],
1008            })
1009        }
1010    }
1011}
1012
1013fn parse_struct_serde_options(input: &DeriveInput) -> syn::Result<StructSerdeOptions> {
1014    let mut options = StructSerdeOptions {
1015        rename_all_deserialize: None,
1016        default: false,
1017    };
1018
1019    for attr in &input.attrs {
1020        if !attr.path().is_ident("serde") {
1021            continue;
1022        }
1023        attr.parse_nested_meta(|meta| {
1024            if meta.path.is_ident("default") {
1025                options.default = true;
1026                if meta.input.peek(Token![=]) {
1027                    let value = meta.value()?;
1028                    let _: LitStr = value.parse()?;
1029                }
1030                return Ok(());
1031            }
1032
1033            if meta.path.is_ident("rename_all") {
1034                if meta.input.peek(Token![=]) {
1035                    let value = meta.value()?;
1036                    let lit: LitStr = value.parse()?;
1037                    options.rename_all_deserialize = Some(RenameRule::parse(&lit)?);
1038                    return Ok(());
1039                }
1040
1041                meta.parse_nested_meta(|nested| {
1042                    if nested.path.is_ident("deserialize") {
1043                        let value = nested.value()?;
1044                        let lit: LitStr = value.parse()?;
1045                        options.rename_all_deserialize = Some(RenameRule::parse(&lit)?);
1046                    } else if nested.path.is_ident("serialize") {
1047                        let value = nested.value()?;
1048                        let _: LitStr = value.parse()?;
1049                    } else {
1050                        return Err(syn::Error::new_spanned(
1051                            nested.path,
1052                            "unsupported `serde(rename_all(...))` entry",
1053                        ));
1054                    }
1055                    Ok(())
1056                })?;
1057                return Ok(());
1058            }
1059
1060            consume_unknown_serde_meta(&meta)
1061        })?;
1062    }
1063
1064    Ok(options)
1065}
1066
1067fn field_serde_options(
1068    field: &Field,
1069    struct_options: &StructSerdeOptions,
1070) -> syn::Result<FieldSerdeOptions> {
1071    let default_name = field
1072        .ident
1073        .as_ref()
1074        .map(ToString::to_string)
1075        .ok_or_else(|| {
1076            syn::Error::new_spanned(field, "unnamed field is not supported in this context")
1077        })?;
1078
1079    let mut direct_rename: Option<String> = None;
1080    let mut deserialize_rename: Option<String> = None;
1081    let mut has_default = struct_options.default;
1082    let mut flatten = false;
1083    let mut skip_deserializing = false;
1084    for attr in &field.attrs {
1085        if !attr.path().is_ident("serde") {
1086            continue;
1087        }
1088        attr.parse_nested_meta(|meta| {
1089            if meta.path.is_ident("flatten") {
1090                flatten = true;
1091                return Ok(());
1092            }
1093
1094            if meta.path.is_ident("skip") || meta.path.is_ident("skip_deserializing") {
1095                has_default = true;
1096                skip_deserializing = true;
1097                return Ok(());
1098            }
1099
1100            if meta.path.is_ident("default") {
1101                has_default = true;
1102                if meta.input.peek(Token![=]) {
1103                    let value = meta.value()?;
1104                    let _: LitStr = value.parse()?;
1105                }
1106                return Ok(());
1107            }
1108
1109            if meta.path.is_ident("rename") {
1110                if meta.input.peek(Token![=]) {
1111                    let value = meta.value()?;
1112                    let lit: LitStr = value.parse()?;
1113                    direct_rename = Some(lit.value());
1114                    return Ok(());
1115                }
1116                meta.parse_nested_meta(|nested| {
1117                    if nested.path.is_ident("deserialize") {
1118                        let value = nested.value()?;
1119                        let lit: LitStr = value.parse()?;
1120                        deserialize_rename = Some(lit.value());
1121                        return Ok(());
1122                    }
1123
1124                    consume_unknown_serde_meta(&nested)
1125                })?;
1126                return Ok(());
1127            }
1128
1129            consume_unknown_serde_meta(&meta)
1130        })?;
1131    }
1132
1133    let renamed = if let Some(rule) = struct_options.rename_all_deserialize {
1134        rule.apply(&default_name)
1135    } else {
1136        default_name
1137    };
1138
1139    Ok(FieldSerdeOptions {
1140        wire_name: deserialize_rename.or(direct_rename).unwrap_or(renamed),
1141        has_default,
1142        flatten,
1143        skip_deserializing,
1144    })
1145}
1146
1147fn consume_unknown_serde_meta(meta: &ParseNestedMeta<'_>) -> syn::Result<()> {
1148    if meta.input.peek(Token![=]) {
1149        let value = meta.value()?;
1150        let _: TokenStream2 = value.parse()?;
1151        return Ok(());
1152    }
1153
1154    if meta.input.peek(syn::token::Paren) {
1155        let content;
1156        syn::parenthesized!(content in meta.input);
1157        let _: TokenStream2 = content.parse()?;
1158    }
1159
1160    Ok(())
1161}
1162
1163fn is_option_type(ty: &Type) -> bool {
1164    match ty {
1165        Type::Path(type_path) => type_path
1166            .path
1167            .segments
1168            .last()
1169            .is_some_and(|segment| segment.ident == "Option"),
1170        _ => false,
1171    }
1172}
1173
1174fn unwrap_option_type(ty: &Type) -> &Type {
1175    if let Some(inner) = extract_single_generic(ty, "Option") {
1176        inner
1177    } else {
1178        ty
1179    }
1180}
1181
1182fn extract_array_item_type(ty: &Type) -> Option<&Type> {
1183    match ty {
1184        Type::Array(array) => Some(&array.elem),
1185        _ => extract_single_generic(ty, "Vec"),
1186    }
1187}
1188
1189fn extract_map_types(ty: &Type) -> Option<(&Type, &Type)> {
1190    let Type::Path(TypePath { path, .. }) = ty else {
1191        return None;
1192    };
1193    let segment = path.segments.last()?;
1194    if segment.ident != "HashMap" && segment.ident != "BTreeMap" {
1195        return None;
1196    }
1197    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
1198        return None;
1199    };
1200    let mut iter = args.args.iter();
1201    let key = match iter.next() {
1202        Some(syn::GenericArgument::Type(ty)) => ty,
1203        _ => return None,
1204    };
1205    let value = match iter.next() {
1206        Some(syn::GenericArgument::Type(ty)) => ty,
1207        _ => return None,
1208    };
1209    Some((key, value))
1210}
1211
1212fn extract_single_generic<'a>(ty: &'a Type, ident: &str) -> Option<&'a Type> {
1213    let Type::Path(TypePath { path, .. }) = ty else {
1214        return None;
1215    };
1216    let segment = path.segments.last()?;
1217    if segment.ident != ident {
1218        return None;
1219    }
1220    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
1221        return None;
1222    };
1223    let mut iter = args.args.iter();
1224    match iter.next() {
1225        Some(syn::GenericArgument::Type(ty)) => Some(ty),
1226        _ => None,
1227    }
1228}
1229
1230fn is_string_type(ty: &Type) -> bool {
1231    match ty {
1232        Type::Path(TypePath { path, .. }) => path
1233            .segments
1234            .last()
1235            .is_some_and(|segment| segment.ident == "String"),
1236        Type::Reference(reference) => {
1237            matches!(
1238                &*reference.elem,
1239                Type::Path(TypePath { path, .. })
1240                    if path.segments.last().is_some_and(|segment| segment.ident == "str")
1241            )
1242        }
1243        _ => false,
1244    }
1245}
1246
1247fn is_number_type(ty: &Type) -> bool {
1248    let Type::Path(TypePath { path, .. }) = ty else {
1249        return false;
1250    };
1251    let Some(segment) = path.segments.last() else {
1252        return false;
1253    };
1254    let ident = segment.ident.to_string();
1255    matches!(
1256        ident.as_str(),
1257        "i8" | "i16"
1258            | "i32"
1259            | "i64"
1260            | "i128"
1261            | "isize"
1262            | "u8"
1263            | "u16"
1264            | "u32"
1265            | "u64"
1266            | "u128"
1267            | "usize"
1268            | "f32"
1269            | "f64"
1270    )
1271}
1272
1273fn add_validate_bounds(generics: &syn::Generics, rustia_path: &TokenStream2) -> syn::Generics {
1274    let mut generics = generics.clone();
1275    for parameter in &mut generics.params {
1276        if let GenericParam::Type(type_param) = parameter {
1277            type_param
1278                .bounds
1279                .push(syn::parse_quote!(#rustia_path::Validate));
1280        }
1281    }
1282    generics
1283}
1284
1285fn rustia_path() -> TokenStream2 {
1286    match crate_name("rustia") {
1287        Ok(FoundCrate::Itself) => quote!(crate),
1288        Ok(FoundCrate::Name(name)) => {
1289            let ident = Ident::new(&name.replace('-', "_"), Span::call_site());
1290            quote!(::#ident)
1291        }
1292        Err(_) => quote!(::rustia),
1293    }
1294}