1use syn::{Attribute, Meta, spanned::Spanned};
8
9use crate::errors::{Error, Result};
10
11#[derive(Debug, Default, PartialEq)]
17pub struct SerdeAttrs {
18 pub rename: Option<String>,
20 pub rename_all: Option<String>,
22 pub skip: bool,
24 pub default: bool,
26 pub tag: Option<String>,
28 pub content: Option<String>,
30 pub untagged: bool,
32 pub skip_serializing_if: Option<String>,
34}
35
36pub fn parse_serde_attrs(attrs: &[Attribute]) -> Result<SerdeAttrs> {
40 let mut out = SerdeAttrs::default();
41
42 for attr in attrs {
43 if !attr.path().is_ident("serde") {
44 continue;
45 }
46 attr.parse_nested_meta(|meta| {
47 if meta.path.is_ident("rename") {
48 out.rename = Some(parse_string_or_nested(&meta, "deserialize")?);
49 } else if meta.path.is_ident("rename_all") {
50 out.rename_all = Some(parse_lit_str_value(&meta)?);
51 } else if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
52 out.skip = true;
53 } else if meta.path.is_ident("default") {
55 out.default = true;
56 if meta.input.peek(syn::Token![=]) {
58 let _: syn::Expr = meta.value()?.parse()?;
59 }
60 } else if meta.path.is_ident("tag") {
61 out.tag = Some(parse_lit_str_value(&meta)?);
62 } else if meta.path.is_ident("content") {
63 out.content = Some(parse_lit_str_value(&meta)?);
64 } else if meta.path.is_ident("untagged") {
65 out.untagged = true;
66 } else if meta.path.is_ident("skip_serializing_if") {
67 out.skip_serializing_if = Some(parse_lit_str_value(&meta)?);
68 } else {
69 skip_meta_value(&meta)?;
71 }
72 Ok(())
73 })
74 .map_err(Error::from)?;
75 }
76
77 Ok(out)
78}
79
80#[derive(Debug, Default, PartialEq)]
86pub struct ZodAttrs {
87 pub min: Option<f64>,
89 pub max: Option<f64>,
90 pub int: bool,
91 pub positive: bool,
92 pub negative: bool,
93 pub nonnegative: bool,
94 pub nonpositive: bool,
95 pub finite: bool,
96 pub length: Option<usize>,
98 pub min_length: Option<usize>,
99 pub max_length: Option<usize>,
100 pub starts_with: Option<String>,
101 pub ends_with: Option<String>,
102 pub includes: Option<String>,
103 pub email: bool,
104 pub url: bool,
105 pub regex: Option<String>,
106}
107
108pub fn parse_zod_attrs(attrs: &[Attribute]) -> Result<ZodAttrs> {
113 let mut out = ZodAttrs::default();
114
115 for attr in attrs {
116 if !attr.path().is_ident("zod") {
117 continue;
118 }
119
120 let Meta::List(ref list) = attr.meta else {
121 continue;
122 };
123
124 attr.parse_nested_meta(|meta| {
125 if meta.path.is_ident("min") {
126 out.min = Some(parse_f64_value(&meta)?);
127 } else if meta.path.is_ident("max") {
128 out.max = Some(parse_f64_value(&meta)?);
129 } else if meta.path.is_ident("length") {
130 out.length = Some(parse_usize_value(&meta)?);
131 } else if meta.path.is_ident("min_length") {
132 out.min_length = Some(parse_usize_value(&meta)?);
133 } else if meta.path.is_ident("max_length") {
134 out.max_length = Some(parse_usize_value(&meta)?);
135 } else if meta.path.is_ident("starts_with") {
136 out.starts_with = Some(parse_lit_str_value(&meta)?);
137 } else if meta.path.is_ident("ends_with") {
138 out.ends_with = Some(parse_lit_str_value(&meta)?);
139 } else if meta.path.is_ident("includes") {
140 out.includes = Some(parse_lit_str_value(&meta)?);
141 } else if meta.path.is_ident("regex") {
142 out.regex = Some(parse_lit_str_value(&meta)?);
143 } else if meta.path.is_ident("email") {
144 out.email = true;
145 } else if meta.path.is_ident("url") {
146 out.url = true;
147 } else if meta.path.is_ident("int") {
148 out.int = true;
149 } else if meta.path.is_ident("positive") {
150 out.positive = true;
151 } else if meta.path.is_ident("negative") {
152 out.negative = true;
153 } else if meta.path.is_ident("nonnegative") {
154 out.nonnegative = true;
155 } else if meta.path.is_ident("nonpositive") {
156 out.nonpositive = true;
157 } else if meta.path.is_ident("finite") {
158 out.finite = true;
159 } else {
160 let key = meta
161 .path
162 .get_ident()
163 .map(|i| i.to_string())
164 .unwrap_or_default();
165 return Err(syn::Error::new(
166 meta.path.span(),
167 Error::unknown_key(
168 meta.path.span(),
169 &key,
170 &[
171 "min",
172 "max",
173 "length",
174 "min_length",
175 "max_length",
176 "starts_with",
177 "ends_with",
178 "includes",
179 "regex",
180 "email",
181 "url",
182 "int",
183 "positive",
184 "negative",
185 "nonnegative",
186 "nonpositive",
187 "finite",
188 ],
189 )
190 .to_string(),
191 ));
192 }
193 let _ = list; Ok(())
195 })
196 .map_err(Error::from)?;
197 }
198
199 Ok(out)
200}
201
202pub fn apply_rename_rule(rule: &str, name: &str) -> String {
208 match rule {
209 "lowercase" => name.to_ascii_lowercase(),
210 "UPPERCASE" => name.to_ascii_uppercase(),
211 "camelCase" => {
212 let mut chars = name.chars();
213 match chars.next() {
214 Some(first) => first.to_ascii_lowercase().to_string() + chars.as_str(),
215 None => String::new(),
216 }
217 }
218 "snake_case" => to_snake_case(name),
219 "SCREAMING_SNAKE_CASE" => to_snake_case(name).to_ascii_uppercase(),
220 "kebab-case" => to_snake_case(name).replace('_', "-"),
221 "SCREAMING-KEBAB-CASE" => to_snake_case(name).to_ascii_uppercase().replace('_', "-"),
222 _ => name.to_string(),
224 }
225}
226
227fn to_snake_case(name: &str) -> String {
228 let mut out = String::new();
229 for (i, ch) in name.char_indices() {
230 if i > 0 && ch.is_uppercase() {
231 out.push('_');
232 }
233 out.push(ch.to_ascii_lowercase());
234 }
235 out
236}
237
238fn parse_lit_str_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<String> {
244 if meta.input.peek(syn::Token![=]) {
245 let lit: syn::LitStr = meta.value()?.parse()?;
246 return Ok(lit.value());
247 }
248 if meta.input.peek(syn::token::Paren) {
250 let mut result = None;
251 meta.parse_nested_meta(|inner| {
252 let lit: syn::LitStr = inner.input.parse()?;
255 result = Some(lit.value());
256 Ok(())
257 })?;
258 if let Some(v) = result {
259 return Ok(v);
260 }
261 }
262 Err(syn::Error::new(
263 meta.input.span(),
264 "expected `= \"value\"` or `(\"value\")`",
265 ))
266}
267
268fn parse_f64_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<f64> {
270 let expr: syn::Expr = if meta.input.peek(syn::Token![=]) {
271 meta.value()?.parse()?
272 } else if meta.input.peek(syn::token::Paren) {
273 let content;
275 syn::parenthesized!(content in meta.input);
276 content.parse()?
277 } else {
278 return Err(syn::Error::new(
279 meta.input.span(),
280 "expected `= <number>` or `(<number>)`",
281 ));
282 };
283
284 match &expr {
285 syn::Expr::Lit(syn::ExprLit {
286 lit: syn::Lit::Float(f),
287 ..
288 }) => f
289 .base10_parse::<f64>()
290 .map_err(|e| syn::Error::new(f.span(), e)),
291 syn::Expr::Lit(syn::ExprLit {
292 lit: syn::Lit::Int(i),
293 ..
294 }) => i
295 .base10_parse::<f64>()
296 .map_err(|e| syn::Error::new(i.span(), e)),
297 _ => Err(syn::Error::new(expr.span(), "expected a numeric literal")),
298 }
299}
300
301fn parse_usize_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<usize> {
303 let lit: syn::LitInt = if meta.input.peek(syn::Token![=]) {
304 meta.value()?.parse()?
305 } else if meta.input.peek(syn::token::Paren) {
306 let content;
308 syn::parenthesized!(content in meta.input);
309 content.parse()?
310 } else {
311 return Err(syn::Error::new(
312 meta.input.span(),
313 "expected `= <integer>` or `(<integer>)`",
314 ));
315 };
316 lit.base10_parse::<usize>()
317 .map_err(|e| syn::Error::new(lit.span(), e))
318}
319
320fn parse_string_or_nested(
323 meta: &syn::meta::ParseNestedMeta,
324 prefer_key: &str,
325) -> syn::Result<String> {
326 if meta.input.peek(syn::Token![=]) {
327 return parse_lit_str_value(meta);
328 }
329 let mut serialize = None;
331 let mut deserialize = None;
332 meta.parse_nested_meta(|inner| {
333 let lit: syn::LitStr = inner.value()?.parse()?;
334 if inner.path.is_ident("serialize") {
335 serialize = Some(lit.value());
336 } else if inner.path.is_ident("deserialize") {
337 deserialize = Some(lit.value());
338 }
339 Ok(())
340 })?;
341 if prefer_key == "deserialize" {
343 Ok(deserialize.or(serialize).unwrap_or_default())
344 } else {
345 Ok(serialize.or(deserialize).unwrap_or_default())
346 }
347}
348
349fn skip_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
351 if meta.input.peek(syn::Token![=]) {
352 let _: syn::Expr = meta.value()?.parse()?;
353 } else if meta.input.peek(syn::token::Paren) {
354 meta.parse_nested_meta(|inner| skip_meta_value(&inner))?;
355 }
356 Ok(())
357}
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366 use syn::{DeriveInput, parse_quote};
367
368 fn attrs_of(input: DeriveInput) -> Vec<Attribute> {
369 input.attrs
370 }
371
372 fn field_attrs(input: &syn::ItemStruct, field_name: &str) -> Vec<Attribute> {
373 if let syn::Fields::Named(fields) = &input.fields {
374 for f in &fields.named {
375 if f.ident.as_ref().map(|i| i == field_name).unwrap_or(false) {
376 return f.attrs.clone();
377 }
378 }
379 }
380 vec![]
381 }
382
383 #[test]
386 fn serde_rename_simple() {
387 let input: DeriveInput = parse_quote! {
388 #[serde(rename = "planet_name")]
389 struct S;
390 };
391 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
392 assert_eq!(attrs.rename, Some("planet_name".to_string()));
393 }
394
395 #[test]
396 fn serde_rename_all() {
397 let input: DeriveInput = parse_quote! {
398 #[serde(rename_all = "camelCase")]
399 struct S;
400 };
401 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
402 assert_eq!(attrs.rename_all, Some("camelCase".to_string()));
403 }
404
405 #[test]
406 fn serde_skip() {
407 let input: DeriveInput = parse_quote! {
408 #[serde(skip)]
409 struct S;
410 };
411 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
412 assert!(attrs.skip);
413 }
414
415 #[test]
416 fn serde_default() {
417 let input: DeriveInput = parse_quote! {
418 #[serde(default)]
419 struct S;
420 };
421 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
422 assert!(attrs.default);
423 }
424
425 #[test]
426 fn serde_unknown_key_ignored() {
427 let input: DeriveInput = parse_quote! {
429 #[serde(some_future_key = "value")]
430 struct S;
431 };
432 assert!(parse_serde_attrs(&attrs_of(input)).is_ok());
433 }
434
435 #[test]
436 fn serde_tag_only() {
437 let input: DeriveInput = parse_quote! {
438 #[serde(tag = "type")]
439 enum E {}
440 };
441 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
442 assert_eq!(attrs.tag, Some("type".to_string()));
443 assert_eq!(attrs.content, None);
444 assert!(!attrs.untagged);
445 }
446
447 #[test]
448 fn serde_tag_and_content() {
449 let input: DeriveInput = parse_quote! {
450 #[serde(tag = "type", content = "data")]
451 enum E {}
452 };
453 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
454 assert_eq!(attrs.tag, Some("type".to_string()));
455 assert_eq!(attrs.content, Some("data".to_string()));
456 assert!(!attrs.untagged);
457 }
458
459 #[test]
460 fn serde_untagged() {
461 let input: DeriveInput = parse_quote! {
462 #[serde(untagged)]
463 enum E {}
464 };
465 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
466 assert!(attrs.untagged);
467 assert_eq!(attrs.tag, None);
468 assert_eq!(attrs.content, None);
469 }
470
471 #[test]
472 fn serde_skip_serializing_if_option_is_none() {
473 let s: syn::ItemStruct = parse_quote! {
474 struct S {
475 #[serde(skip_serializing_if = "Option::is_none")]
476 field: Option<String>,
477 }
478 };
479 let attrs = parse_serde_attrs(&field_attrs(&s, "field")).unwrap();
480 assert_eq!(attrs.skip_serializing_if, Some("Option::is_none".to_string()));
481 }
482
483 #[test]
484 fn serde_skip_serializing_if_custom_predicate() {
485 let s: syn::ItemStruct = parse_quote! {
486 struct S {
487 #[serde(skip_serializing_if = "String::is_empty")]
488 field: String,
489 }
490 };
491 let attrs = parse_serde_attrs(&field_attrs(&s, "field")).unwrap();
492 assert_eq!(attrs.skip_serializing_if, Some("String::is_empty".to_string()));
493 }
494
495 #[test]
496 fn non_serde_attr_ignored() {
497 let input: DeriveInput = parse_quote! {
498 #[derive(Debug)]
499 struct S;
500 };
501 let attrs = parse_serde_attrs(&attrs_of(input)).unwrap();
502 assert_eq!(attrs, SerdeAttrs::default());
503 }
504
505 #[test]
508 fn zod_string_constraints() {
509 let s: syn::ItemStruct = parse_quote! {
510 struct S {
511 #[zod(min_length(3), max_length(100), email)]
512 name: String,
513 }
514 };
515 let attrs = parse_zod_attrs(&field_attrs(&s, "name")).unwrap();
516 assert_eq!(attrs.min_length, Some(3));
517 assert_eq!(attrs.max_length, Some(100));
518 assert!(attrs.email);
519 }
520
521 #[test]
522 fn zod_number_constraints() {
523 let s: syn::ItemStruct = parse_quote! {
524 struct S {
525 #[zod(min(0), max(100), int, positive)]
526 score: f64,
527 }
528 };
529 let attrs = parse_zod_attrs(&field_attrs(&s, "score")).unwrap();
530 assert_eq!(attrs.min, Some(0.0));
531 assert_eq!(attrs.max, Some(100.0));
532 assert!(attrs.int);
533 assert!(attrs.positive);
534 }
535
536 #[test]
537 fn zod_unknown_key_returns_error() {
538 let s: syn::ItemStruct = parse_quote! {
539 struct S {
540 #[zod(unknown_key)]
541 name: String,
542 }
543 };
544 let err = parse_zod_attrs(&field_attrs(&s, "name")).unwrap_err();
545 assert!(err.to_string().contains("unknown key"));
546 }
547
548 #[test]
551 fn rename_rules() {
552 assert_eq!(apply_rename_rule("camelCase", "PlanetName"), "planetName");
553 assert_eq!(apply_rename_rule("snake_case", "PlanetName"), "planet_name");
554 assert_eq!(
555 apply_rename_rule("SCREAMING_SNAKE_CASE", "PlanetName"),
556 "PLANET_NAME"
557 );
558 assert_eq!(apply_rename_rule("kebab-case", "PlanetName"), "planet-name");
559 assert_eq!(apply_rename_rule("lowercase", "PlanetName"), "planetname");
560 assert_eq!(apply_rename_rule("UPPERCASE", "PlanetName"), "PLANETNAME");
561 }
562
563 #[test]
564 fn unknown_rule_passthrough() {
565 assert_eq!(
566 apply_rename_rule("PascalCase", "planet_name"),
567 "planet_name"
568 );
569 }
570}