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