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