Skip to main content

rorpc_parse/
types.rs

1//! AST-based type analysis utilities.
2//!
3//! All wrapper extraction operates on `syn::Type` AST nodes by checking the
4//! **final path segment ident** — never on a string representation of the type.
5//! This means `Json<T>`, `axum::Json<T>`, and `axum::extract::Json<T>` are
6//! all matched identically.
7
8use syn::{GenericArgument, PathArguments, Token, Type, TypePath, punctuated::Punctuated};
9
10use crate::errors::{Error, Result};
11
12// ---------------------------------------------------------------------------
13// Well-known wrapper names
14// ---------------------------------------------------------------------------
15
16pub 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
25// ---------------------------------------------------------------------------
26// WrapperMatch — result of a successful wrapper extraction
27// ---------------------------------------------------------------------------
28
29/// The generic argument list of a matched wrapper type.
30///
31/// Returned by [`try_extract_wrapper`] and [`extract_wrapper`].
32// WrapperMatch holds references into the syn AST — Debug is derived for test ergonomics.
33pub 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    /// The first type argument — `T` in `Wrapper<T, ...>`.
47    pub fn first_type(&self) -> Option<&'a Type> {
48        self.generic_args.iter().find_map(as_type_arg)
49    }
50
51    /// The second type argument — `E` in `Result<T, E>`.
52    pub fn second_type(&self) -> Option<&'a Type> {
53        self.generic_args.iter().filter_map(as_type_arg).nth(1)
54    }
55
56    /// The nth type argument (0-indexed).
57    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
70// ---------------------------------------------------------------------------
71// Extraction — try (Option) and strict (Result)
72// ---------------------------------------------------------------------------
73
74/// Extract the generic arguments of `wrapper_name<...>` from `ty`.
75///
76/// Checks the **final path segment ident** so qualified paths like
77/// `std::result::Result` and `axum::extract::Json` are handled identically
78/// to bare `Result` and `Json`.
79///
80/// Returns `None` when `ty` is not the named wrapper — use this when a
81/// wrapper is optional (e.g. a parameter may or may not be `Json<T>`).
82pub 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
95/// Extract the generic arguments of `wrapper_name<...>` from `ty`, or return
96/// a structured error if the wrapper is not present or has no type arguments.
97///
98/// Use this when the wrapper is required (e.g. a handler return type must be
99/// `Json<T>` or `Result<Json<T>, E>`).
100pub 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
126// ---------------------------------------------------------------------------
127// Primitive check
128// ---------------------------------------------------------------------------
129
130/// Returns `true` for Rust primitive types and the unit type `()`.
131///
132/// Uses AST ident comparison — never string matching.
133pub fn is_primitive(ty: &Type) -> bool {
134    match ty {
135        // Unit type ()
136        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
163/// Check if a type name string represents a Rust primitive or standard type.
164///
165/// This is for runtime use when you have a type name from metadata as a string,
166/// not a `syn::Type` AST. For compile-time AST checking, use [`is_primitive`] instead.
167///
168/// # Examples
169///
170/// ```
171/// use rorpc_parse::types::is_primitive_type_name;
172///
173/// assert!(is_primitive_type_name("String"));
174/// assert!(is_primitive_type_name("i32"));
175/// assert!(is_primitive_type_name("()"));
176/// assert!(!is_primitive_type_name("Planet"));
177/// ```
178pub 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
203// ---------------------------------------------------------------------------
204// Innermost custom type
205// ---------------------------------------------------------------------------
206
207/// Recursively unwrap well-known wrappers and return the innermost type.
208///
209/// `Result<Json<Vec<Planet>>, E>` → `Planet` (a `Type::Path` for `Planet`)
210///
211/// Returns `None` when the innermost resolved type is a primitive (no schema
212/// registration needed) or when the type cannot be unwrapped further.
213pub 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    // Base case: no wrapper matched — this is the innermost type
222    if is_primitive(ty) { None } else { Some(ty) }
223}
224
225// ---------------------------------------------------------------------------
226// Internal helpers
227// ---------------------------------------------------------------------------
228
229fn 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
246/// Extract the first generic argument from a type string.
247///
248/// String-based parsing for runtime use. Returns the first type argument
249/// from generic type syntax, handling nested generics correctly.
250///
251/// # Examples
252///
253/// ```
254/// use rorpc_parse::types::extract_first_generic_arg_string;
255///
256/// assert_eq!(extract_first_generic_arg_string("Result<T, E>"), Some("T".to_string()));
257/// assert_eq!(extract_first_generic_arg_string("Vec<Planet>"), Some("Planet".to_string()));
258/// assert_eq!(extract_first_generic_arg_string("Result<Json<Planet>, E>"), Some("Json<Planet>".to_string()));
259/// assert_eq!(extract_first_generic_arg_string("NoGenerics"), None);
260/// ```
261pub 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// ---------------------------------------------------------------------------
290// Tests
291// ---------------------------------------------------------------------------
292
293#[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    // --- try_extract_wrapper ---
303
304    #[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        // `Json` with no generics — syn parses this as a bare path, not angle-bracketed
335        let ty = parse_type("Json");
336        assert!(try_extract_wrapper(&ty, JSON).is_none());
337    }
338
339    // --- extract_wrapper (strict) ---
340
341    #[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    // --- is_primitive ---
351
352    #[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    // --- innermost_custom_type ---
372
373    #[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    // --- WrapperMatch ---
394
395    #[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}