1#![forbid(unsafe_code)]
2
3use 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 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 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 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}