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 HASHMAP: &str = "HashMap";
23pub const STATE: &str = "State";
24pub const SSE: &str = "Sse";
25pub const STATUSCODE: &str = "StatusCode";
26
27pub struct WrapperMatch<'a> {
36 pub generic_args: &'a Punctuated<GenericArgument, Token![,]>,
37}
38
39impl std::fmt::Debug for WrapperMatch<'_> {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("WrapperMatch")
42 .field("generic_args_len", &self.generic_args.len())
43 .finish()
44 }
45}
46
47impl<'a> WrapperMatch<'a> {
48 pub fn first_type(&self) -> Option<&'a Type> {
50 self.generic_args.iter().find_map(as_type_arg)
51 }
52
53 pub fn second_type(&self) -> Option<&'a Type> {
55 self.generic_args.iter().filter_map(as_type_arg).nth(1)
56 }
57
58 pub fn nth_type(&self, n: usize) -> Option<&'a Type> {
60 self.generic_args.iter().filter_map(as_type_arg).nth(n)
61 }
62
63 pub fn all_types(&self) -> Vec<&'a Type> {
65 self.generic_args.iter().filter_map(as_type_arg).collect()
66 }
67}
68
69fn as_type_arg(arg: &GenericArgument) -> Option<&Type> {
70 if let GenericArgument::Type(ty) = arg {
71 Some(ty)
72 } else {
73 None
74 }
75}
76
77pub fn try_extract_wrapper<'a>(ty: &'a Type, wrapper_name: &str) -> Option<WrapperMatch<'a>> {
90 let last = last_segment(ty)?;
91 if last.ident != wrapper_name {
92 return None;
93 }
94 match &last.arguments {
95 PathArguments::AngleBracketed(args) if !args.args.is_empty() => Some(WrapperMatch {
96 generic_args: &args.args,
97 }),
98 _ => None,
99 }
100}
101
102pub fn extract_wrapper<'a>(ty: &'a Type, wrapper_name: &'static str) -> Result<WrapperMatch<'a>> {
108 let span = span_of(ty);
109
110 let last = last_segment(ty)
111 .ok_or_else(|| Error::unsupported_type(span, ty, "empty type path", "use a named type"))?;
112
113 if last.ident != wrapper_name {
114 return Err(Error::missing_wrapper(span, wrapper_name, ty));
115 }
116
117 match &last.arguments {
118 PathArguments::AngleBracketed(args) if !args.args.is_empty() => Ok(WrapperMatch {
119 generic_args: &args.args,
120 }),
121 PathArguments::AngleBracketed(_) | PathArguments::None => {
122 Err(Error::empty_generic_args(span, wrapper_name))
123 }
124 PathArguments::Parenthesized(_) => Err(Error::unsupported_type(
125 span,
126 ty,
127 "parenthesised arguments are not supported",
128 "use angle-bracketed generics: `Wrapper<T>`",
129 )),
130 }
131}
132
133pub fn is_primitive(ty: &Type) -> bool {
141 match ty {
142 Type::Tuple(t) if t.elems.is_empty() => true,
144 Type::Path(_) => matches!(
145 last_ident_str(ty).as_deref(),
146 Some(
147 "String"
148 | "str"
149 | "bool"
150 | "i8"
151 | "i16"
152 | "i32"
153 | "i64"
154 | "i128"
155 | "isize"
156 | "u8"
157 | "u16"
158 | "u32"
159 | "u64"
160 | "u128"
161 | "usize"
162 | "f32"
163 | "f64"
164 | "Uuid"
166 | "DateTime"
167 | "Value" )
169 ),
170 _ => false,
171 }
172}
173
174pub fn is_primitive_type_name(type_name: &str) -> bool {
190 matches!(
191 type_name,
192 "()" | "String"
193 | "str"
194 | "i8"
195 | "i16"
196 | "i32"
197 | "i64"
198 | "i128"
199 | "u8"
200 | "u16"
201 | "u32"
202 | "u64"
203 | "u128"
204 | "f32"
205 | "f64"
206 | "bool"
207 | "usize"
208 | "isize"
209 | "serde_json::Value"
210 | "Json<serde_json::Value>"
211 )
212}
213
214pub fn innermost_custom_type(ty: &Type) -> Option<&Type> {
225 for wrapper in &[RESULT, JSON, QUERY, OPTION, VEC, HASHMAP, STATE, SSE] {
226 if let Some(m) = try_extract_wrapper(ty, wrapper)
227 && let Some(inner) = m.first_type()
228 {
229 return innermost_custom_type(inner);
230 }
231 }
232 if is_primitive(ty) { None } else { Some(ty) }
234}
235
236fn last_segment(ty: &Type) -> Option<&syn::PathSegment> {
241 if let Type::Path(TypePath { path, .. }) = ty {
242 path.segments.last()
243 } else {
244 None
245 }
246}
247
248fn last_ident_str(ty: &Type) -> Option<String> {
249 last_segment(ty).map(|seg| seg.ident.to_string())
250}
251
252pub fn last_ident_matches(ty: &Type, name: &str) -> bool {
256 last_ident_str(ty).as_deref() == Some(name)
257}
258
259fn span_of(ty: &Type) -> proc_macro2::Span {
260 use syn::spanned::Spanned;
261 ty.span()
262}
263
264pub fn extract_first_generic_arg_string(type_str: &str) -> Option<String> {
280 let start = type_str.find('<')? + 1;
281 let mut depth = 0;
282 let mut end = start;
283
284 for (i, ch) in type_str[start..].char_indices() {
285 match ch {
286 '<' => depth += 1,
287 '>' if depth == 0 => {
288 end = start + i;
289 break;
290 }
291 '>' => depth -= 1,
292 ',' if depth == 0 => {
293 end = start + i;
294 break;
295 }
296 _ => {}
297 }
298 }
299
300 if end > start {
301 Some(type_str[start..end].trim().to_string())
302 } else {
303 None
304 }
305}
306
307#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::errors::type_display;
315
316 fn parse_type(s: &str) -> Type {
317 syn::parse_str(s).unwrap()
318 }
319
320 #[test]
323 fn bare_json() {
324 let ty = parse_type("Json<Planet>");
325 let m = try_extract_wrapper(&ty, JSON).unwrap();
326 assert!(m.first_type().is_some());
327 }
328
329 #[test]
330 fn qualified_json() {
331 let ty = parse_type("axum::extract::Json<Planet>");
332 let m = try_extract_wrapper(&ty, JSON).unwrap();
333 assert!(m.first_type().is_some());
334 }
335
336 #[test]
337 fn qualified_result() {
338 let ty = parse_type("std::result::Result<Json<Planet>, AppError>");
339 let m = try_extract_wrapper(&ty, RESULT).unwrap();
340 assert!(m.first_type().is_some());
341 assert!(m.second_type().is_some());
342 }
343
344 #[test]
345 fn wrong_wrapper_returns_none() {
346 let ty = parse_type("Option<String>");
347 assert!(try_extract_wrapper(&ty, JSON).is_none());
348 }
349
350 #[test]
351 fn wrapper_without_args_returns_none() {
352 let ty = parse_type("Json");
354 assert!(try_extract_wrapper(&ty, JSON).is_none());
355 }
356
357 #[test]
360 fn extract_wrong_wrapper_gives_error() {
361 let ty = parse_type("Vec<Planet>");
362 let err = extract_wrapper(&ty, JSON).unwrap_err();
363 let msg = err.to_string();
364 assert!(msg.contains("expected `Json` wrapper"));
365 assert!(msg.contains("Vec<Planet>"));
366 }
367
368 #[test]
371 fn primitives_identified() {
372 for s in &["String", "i32", "u64", "f64", "bool", "usize"] {
373 assert!(is_primitive(&parse_type(s)), "{} should be primitive", s);
374 }
375 }
376
377 #[test]
378 fn unit_type_is_primitive() {
379 let ty: Type = syn::parse_str("()").unwrap();
380 assert!(is_primitive(&ty));
381 }
382
383 #[test]
384 fn custom_type_not_primitive() {
385 assert!(!is_primitive(&parse_type("Planet")));
386 assert!(!is_primitive(&parse_type("AppError")));
387 }
388
389 #[test]
392 fn unwraps_result_json_vec() {
393 let ty = parse_type("Result<Json<Vec<Planet>>, AppError>");
394 let inner = innermost_custom_type(&ty).unwrap();
395 assert_eq!(type_display(inner), "Planet");
396 }
397
398 #[test]
399 fn primitive_innermost_returns_none() {
400 let ty = parse_type("Json<String>");
401 assert!(innermost_custom_type(&ty).is_none());
402 }
403
404 #[test]
405 fn custom_type_at_root_returned_as_is() {
406 let ty = parse_type("Planet");
407 let inner = innermost_custom_type(&ty).unwrap();
408 assert_eq!(type_display(inner), "Planet");
409 }
410
411 #[test]
414 fn second_type_on_result() {
415 let ty = parse_type("Result<Json<Planet>, AppError>");
416 let m = try_extract_wrapper(&ty, RESULT).unwrap();
417 let second = m.second_type().unwrap();
418 assert_eq!(type_display(second), "AppError");
419 }
420
421 #[test]
422 fn nth_type_indexing() {
423 let ty = parse_type("Result<Json<Planet>, AppError>");
424 let m = try_extract_wrapper(&ty, RESULT).unwrap();
425 assert!(m.nth_type(0).is_some());
426 assert!(m.nth_type(1).is_some());
427 assert!(m.nth_type(2).is_none());
428 }
429}