1use syn::{GenericArgument, PathArguments, Token, Type, TypePath, punctuated::Punctuated};
9
10use crate::errors::{Error, Result};
11
12pub const JSON: &str = "Json";
17pub const QUERY: &str = "Query";
18pub const PATH: &str = "Path";
19pub const RESULT: &str = "Result";
20pub const OPTION: &str = "Option";
21pub const VEC: &str = "Vec";
22pub const STATE: &str = "State";
23pub const SSE: &str = "Sse";
24
25pub struct WrapperMatch<'a> {
34 pub generic_args: &'a Punctuated<GenericArgument, Token![,]>,
35}
36
37impl std::fmt::Debug for WrapperMatch<'_> {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("WrapperMatch")
40 .field("generic_args_len", &self.generic_args.len())
41 .finish()
42 }
43}
44
45impl<'a> WrapperMatch<'a> {
46 pub fn first_type(&self) -> Option<&'a Type> {
48 self.generic_args.iter().find_map(as_type_arg)
49 }
50
51 pub fn second_type(&self) -> Option<&'a Type> {
53 self.generic_args.iter().filter_map(as_type_arg).nth(1)
54 }
55
56 pub fn nth_type(&self, n: usize) -> Option<&'a Type> {
58 self.generic_args.iter().filter_map(as_type_arg).nth(n)
59 }
60}
61
62fn as_type_arg(arg: &GenericArgument) -> Option<&Type> {
63 if let GenericArgument::Type(ty) = arg {
64 Some(ty)
65 } else {
66 None
67 }
68}
69
70pub fn try_extract_wrapper<'a>(ty: &'a Type, wrapper_name: &str) -> Option<WrapperMatch<'a>> {
83 let last = last_segment(ty)?;
84 if last.ident != wrapper_name {
85 return None;
86 }
87 match &last.arguments {
88 PathArguments::AngleBracketed(args) if !args.args.is_empty() => Some(WrapperMatch {
89 generic_args: &args.args,
90 }),
91 _ => None,
92 }
93}
94
95pub fn extract_wrapper<'a>(ty: &'a Type, wrapper_name: &'static str) -> Result<WrapperMatch<'a>> {
101 let span = span_of(ty);
102
103 let last = last_segment(ty)
104 .ok_or_else(|| Error::unsupported_type(span, ty, "empty type path", "use a named type"))?;
105
106 if last.ident != wrapper_name {
107 return Err(Error::missing_wrapper(span, wrapper_name, ty));
108 }
109
110 match &last.arguments {
111 PathArguments::AngleBracketed(args) if !args.args.is_empty() => Ok(WrapperMatch {
112 generic_args: &args.args,
113 }),
114 PathArguments::AngleBracketed(_) | PathArguments::None => {
115 Err(Error::empty_generic_args(span, wrapper_name))
116 }
117 PathArguments::Parenthesized(_) => Err(Error::unsupported_type(
118 span,
119 ty,
120 "parenthesised arguments are not supported",
121 "use angle-bracketed generics: `Wrapper<T>`",
122 )),
123 }
124}
125
126pub fn is_primitive(ty: &Type) -> bool {
134 match ty {
135 Type::Tuple(t) if t.elems.is_empty() => true,
137 Type::Path(_) => matches!(
138 last_ident_str(ty).as_deref(),
139 Some(
140 "String"
141 | "str"
142 | "bool"
143 | "i8"
144 | "i16"
145 | "i32"
146 | "i64"
147 | "i128"
148 | "isize"
149 | "u8"
150 | "u16"
151 | "u32"
152 | "u64"
153 | "u128"
154 | "usize"
155 | "f32"
156 | "f64"
157 | "Uuid"
159 | "DateTime"
160 | "Value" )
162 ),
163 _ => false,
164 }
165}
166
167pub fn is_primitive_type_name(type_name: &str) -> bool {
183 matches!(
184 type_name,
185 "()" | "String"
186 | "str"
187 | "i8"
188 | "i16"
189 | "i32"
190 | "i64"
191 | "i128"
192 | "u8"
193 | "u16"
194 | "u32"
195 | "u64"
196 | "u128"
197 | "f32"
198 | "f64"
199 | "bool"
200 | "usize"
201 | "isize"
202 | "serde_json::Value"
203 | "Json<serde_json::Value>"
204 )
205}
206
207pub fn innermost_custom_type(ty: &Type) -> Option<&Type> {
218 for wrapper in &[RESULT, JSON, QUERY, OPTION, VEC, STATE, SSE] {
219 if let Some(m) = try_extract_wrapper(ty, wrapper)
220 && let Some(inner) = m.first_type()
221 {
222 return innermost_custom_type(inner);
223 }
224 }
225 if is_primitive(ty) { None } else { Some(ty) }
227}
228
229fn last_segment(ty: &Type) -> Option<&syn::PathSegment> {
234 if let Type::Path(TypePath { path, .. }) = ty {
235 path.segments.last()
236 } else {
237 None
238 }
239}
240
241fn last_ident_str(ty: &Type) -> Option<String> {
242 last_segment(ty).map(|seg| seg.ident.to_string())
243}
244
245fn span_of(ty: &Type) -> proc_macro2::Span {
246 use syn::spanned::Spanned;
247 ty.span()
248}
249
250pub fn extract_first_generic_arg_string(type_str: &str) -> Option<String> {
266 let start = type_str.find('<')? + 1;
267 let mut depth = 0;
268 let mut end = start;
269
270 for (i, ch) in type_str[start..].char_indices() {
271 match ch {
272 '<' => depth += 1,
273 '>' if depth == 0 => {
274 end = start + i;
275 break;
276 }
277 '>' => depth -= 1,
278 ',' if depth == 0 => {
279 end = start + i;
280 break;
281 }
282 _ => {}
283 }
284 }
285
286 if end > start {
287 Some(type_str[start..end].trim().to_string())
288 } else {
289 None
290 }
291}
292
293#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::errors::type_display;
301
302 fn parse_type(s: &str) -> Type {
303 syn::parse_str(s).unwrap()
304 }
305
306 #[test]
309 fn bare_json() {
310 let ty = parse_type("Json<Planet>");
311 let m = try_extract_wrapper(&ty, JSON).unwrap();
312 assert!(m.first_type().is_some());
313 }
314
315 #[test]
316 fn qualified_json() {
317 let ty = parse_type("axum::extract::Json<Planet>");
318 let m = try_extract_wrapper(&ty, JSON).unwrap();
319 assert!(m.first_type().is_some());
320 }
321
322 #[test]
323 fn qualified_result() {
324 let ty = parse_type("std::result::Result<Json<Planet>, AppError>");
325 let m = try_extract_wrapper(&ty, RESULT).unwrap();
326 assert!(m.first_type().is_some());
327 assert!(m.second_type().is_some());
328 }
329
330 #[test]
331 fn wrong_wrapper_returns_none() {
332 let ty = parse_type("Option<String>");
333 assert!(try_extract_wrapper(&ty, JSON).is_none());
334 }
335
336 #[test]
337 fn wrapper_without_args_returns_none() {
338 let ty = parse_type("Json");
340 assert!(try_extract_wrapper(&ty, JSON).is_none());
341 }
342
343 #[test]
346 fn extract_wrong_wrapper_gives_error() {
347 let ty = parse_type("Vec<Planet>");
348 let err = extract_wrapper(&ty, JSON).unwrap_err();
349 let msg = err.to_string();
350 assert!(msg.contains("expected `Json` wrapper"));
351 assert!(msg.contains("Vec<Planet>"));
352 }
353
354 #[test]
357 fn primitives_identified() {
358 for s in &["String", "i32", "u64", "f64", "bool", "usize"] {
359 assert!(is_primitive(&parse_type(s)), "{} should be primitive", s);
360 }
361 }
362
363 #[test]
364 fn unit_type_is_primitive() {
365 let ty: Type = syn::parse_str("()").unwrap();
366 assert!(is_primitive(&ty));
367 }
368
369 #[test]
370 fn custom_type_not_primitive() {
371 assert!(!is_primitive(&parse_type("Planet")));
372 assert!(!is_primitive(&parse_type("AppError")));
373 }
374
375 #[test]
378 fn unwraps_result_json_vec() {
379 let ty = parse_type("Result<Json<Vec<Planet>>, AppError>");
380 let inner = innermost_custom_type(&ty).unwrap();
381 assert_eq!(type_display(inner), "Planet");
382 }
383
384 #[test]
385 fn primitive_innermost_returns_none() {
386 let ty = parse_type("Json<String>");
387 assert!(innermost_custom_type(&ty).is_none());
388 }
389
390 #[test]
391 fn custom_type_at_root_returned_as_is() {
392 let ty = parse_type("Planet");
393 let inner = innermost_custom_type(&ty).unwrap();
394 assert_eq!(type_display(inner), "Planet");
395 }
396
397 #[test]
400 fn second_type_on_result() {
401 let ty = parse_type("Result<Json<Planet>, AppError>");
402 let m = try_extract_wrapper(&ty, RESULT).unwrap();
403 let second = m.second_type().unwrap();
404 assert_eq!(type_display(second), "AppError");
405 }
406
407 #[test]
408 fn nth_type_indexing() {
409 let ty = parse_type("Result<Json<Planet>, AppError>");
410 let m = try_extract_wrapper(&ty, RESULT).unwrap();
411 assert!(m.nth_type(0).is_some());
412 assert!(m.nth_type(1).is_some());
413 assert!(m.nth_type(2).is_none());
414 }
415}