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