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 )
158 ),
159 _ => false,
160 }
161}
162
163pub fn is_primitive_type_name(type_name: &str) -> bool {
179 matches!(
180 type_name,
181 "()" | "String"
182 | "str"
183 | "i8"
184 | "i16"
185 | "i32"
186 | "i64"
187 | "i128"
188 | "u8"
189 | "u16"
190 | "u32"
191 | "u64"
192 | "u128"
193 | "f32"
194 | "f64"
195 | "bool"
196 | "usize"
197 | "isize"
198 | "serde_json::Value"
199 | "Json<serde_json::Value>"
200 )
201}
202
203pub fn innermost_custom_type(ty: &Type) -> Option<&Type> {
214 for wrapper in &[RESULT, JSON, QUERY, OPTION, VEC, STATE, SSE] {
215 if let Some(m) = try_extract_wrapper(ty, wrapper)
216 && let Some(inner) = m.first_type()
217 {
218 return innermost_custom_type(inner);
219 }
220 }
221 if is_primitive(ty) { None } else { Some(ty) }
223}
224
225fn last_segment(ty: &Type) -> Option<&syn::PathSegment> {
230 if let Type::Path(TypePath { path, .. }) = ty {
231 path.segments.last()
232 } else {
233 None
234 }
235}
236
237fn last_ident_str(ty: &Type) -> Option<String> {
238 last_segment(ty).map(|seg| seg.ident.to_string())
239}
240
241fn span_of(ty: &Type) -> proc_macro2::Span {
242 use syn::spanned::Spanned;
243 ty.span()
244}
245
246pub fn extract_first_generic_arg_string(type_str: &str) -> Option<String> {
262 let start = type_str.find('<')? + 1;
263 let mut depth = 0;
264 let mut end = start;
265
266 for (i, ch) in type_str[start..].char_indices() {
267 match ch {
268 '<' => depth += 1,
269 '>' if depth == 0 => {
270 end = start + i;
271 break;
272 }
273 '>' => depth -= 1,
274 ',' if depth == 0 => {
275 end = start + i;
276 break;
277 }
278 _ => {}
279 }
280 }
281
282 if end > start {
283 Some(type_str[start..end].trim().to_string())
284 } else {
285 None
286 }
287}
288
289#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::errors::type_display;
297
298 fn parse_type(s: &str) -> Type {
299 syn::parse_str(s).unwrap()
300 }
301
302 #[test]
305 fn bare_json() {
306 let ty = parse_type("Json<Planet>");
307 let m = try_extract_wrapper(&ty, JSON).unwrap();
308 assert!(m.first_type().is_some());
309 }
310
311 #[test]
312 fn qualified_json() {
313 let ty = parse_type("axum::extract::Json<Planet>");
314 let m = try_extract_wrapper(&ty, JSON).unwrap();
315 assert!(m.first_type().is_some());
316 }
317
318 #[test]
319 fn qualified_result() {
320 let ty = parse_type("std::result::Result<Json<Planet>, AppError>");
321 let m = try_extract_wrapper(&ty, RESULT).unwrap();
322 assert!(m.first_type().is_some());
323 assert!(m.second_type().is_some());
324 }
325
326 #[test]
327 fn wrong_wrapper_returns_none() {
328 let ty = parse_type("Option<String>");
329 assert!(try_extract_wrapper(&ty, JSON).is_none());
330 }
331
332 #[test]
333 fn wrapper_without_args_returns_none() {
334 let ty = parse_type("Json");
336 assert!(try_extract_wrapper(&ty, JSON).is_none());
337 }
338
339 #[test]
342 fn extract_wrong_wrapper_gives_error() {
343 let ty = parse_type("Vec<Planet>");
344 let err = extract_wrapper(&ty, JSON).unwrap_err();
345 let msg = err.to_string();
346 assert!(msg.contains("expected `Json` wrapper"));
347 assert!(msg.contains("Vec<Planet>"));
348 }
349
350 #[test]
353 fn primitives_identified() {
354 for s in &["String", "i32", "u64", "f64", "bool", "usize"] {
355 assert!(is_primitive(&parse_type(s)), "{} should be primitive", s);
356 }
357 }
358
359 #[test]
360 fn unit_type_is_primitive() {
361 let ty: Type = syn::parse_str("()").unwrap();
362 assert!(is_primitive(&ty));
363 }
364
365 #[test]
366 fn custom_type_not_primitive() {
367 assert!(!is_primitive(&parse_type("Planet")));
368 assert!(!is_primitive(&parse_type("AppError")));
369 }
370
371 #[test]
374 fn unwraps_result_json_vec() {
375 let ty = parse_type("Result<Json<Vec<Planet>>, AppError>");
376 let inner = innermost_custom_type(&ty).unwrap();
377 assert_eq!(type_display(inner), "Planet");
378 }
379
380 #[test]
381 fn primitive_innermost_returns_none() {
382 let ty = parse_type("Json<String>");
383 assert!(innermost_custom_type(&ty).is_none());
384 }
385
386 #[test]
387 fn custom_type_at_root_returned_as_is() {
388 let ty = parse_type("Planet");
389 let inner = innermost_custom_type(&ty).unwrap();
390 assert_eq!(type_display(inner), "Planet");
391 }
392
393 #[test]
396 fn second_type_on_result() {
397 let ty = parse_type("Result<Json<Planet>, AppError>");
398 let m = try_extract_wrapper(&ty, RESULT).unwrap();
399 let second = m.second_type().unwrap();
400 assert_eq!(type_display(second), "AppError");
401 }
402
403 #[test]
404 fn nth_type_indexing() {
405 let ty = parse_type("Result<Json<Planet>, AppError>");
406 let m = try_extract_wrapper(&ty, RESULT).unwrap();
407 assert!(m.nth_type(0).is_some());
408 assert!(m.nth_type(1).is_some());
409 assert!(m.nth_type(2).is_none());
410 }
411}