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