Skip to main content

server_less_parse/
lib.rs

1//! Shared parsing utilities for server-less proc macros.
2//!
3//! This crate provides common types and functions for extracting
4//! method information from impl blocks.
5
6use syn::{
7    FnArg, GenericArgument, Ident, ImplItem, ImplItemFn, ItemImpl, Lit, Meta, Pat, PathArguments,
8    ReturnType, Type,
9};
10
11/// Parsed information about a method
12#[derive(Debug, Clone)]
13pub struct MethodInfo {
14    /// The original method
15    pub method: ImplItemFn,
16    /// Method name
17    pub name: Ident,
18    /// Documentation string
19    pub docs: Option<String>,
20    /// Parameters (excluding self)
21    pub params: Vec<ParamInfo>,
22    /// Return type info
23    pub return_info: ReturnInfo,
24    /// Whether the method is async
25    pub is_async: bool,
26}
27
28/// Parsed parameter information
29#[derive(Debug, Clone)]
30pub struct ParamInfo {
31    /// Parameter name
32    pub name: Ident,
33    /// Parameter type
34    pub ty: Type,
35    /// Whether this is `Option<T>`
36    pub is_optional: bool,
37    /// Whether this looks like an ID (ends with _id or is named id)
38    pub is_id: bool,
39    /// Custom wire name (from #[param(name = "...")])
40    pub wire_name: Option<String>,
41    /// Parameter location override (from #[param(query/path/body/header)])
42    pub location: Option<ParamLocation>,
43    /// Default value as a string (from #[param(default = ...)])
44    pub default_value: Option<String>,
45}
46
47/// Parameter location for HTTP requests
48#[derive(Debug, Clone, PartialEq)]
49pub enum ParamLocation {
50    Query,
51    Path,
52    Body,
53    Header,
54}
55
56/// Parsed return type information
57#[derive(Debug, Clone)]
58pub struct ReturnInfo {
59    /// The full return type
60    pub ty: Option<Type>,
61    /// Inner type if `Result<T, E>`
62    pub ok_type: Option<Type>,
63    /// Error type if `Result<T, E>`
64    pub err_type: Option<Type>,
65    /// Inner type if `Option<T>`
66    pub some_type: Option<Type>,
67    /// Whether it's a Result
68    pub is_result: bool,
69    /// Whether it's an Option (and not Result)
70    pub is_option: bool,
71    /// Whether it returns ()
72    pub is_unit: bool,
73    /// Whether it's impl Stream<Item=T>
74    pub is_stream: bool,
75    /// The stream item type if is_stream
76    pub stream_item: Option<Type>,
77}
78
79impl MethodInfo {
80    /// Parse a method from an ImplItemFn
81    ///
82    /// Returns None for associated functions without `&self` (constructors, etc.)
83    pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
84        let name = method.sig.ident.clone();
85        let is_async = method.sig.asyncness.is_some();
86
87        // Skip associated functions without self receiver (constructors, etc.)
88        let has_receiver = method
89            .sig
90            .inputs
91            .iter()
92            .any(|arg| matches!(arg, FnArg::Receiver(_)));
93        if !has_receiver {
94            return Ok(None);
95        }
96
97        // Extract doc comments
98        let docs = extract_docs(&method.attrs);
99
100        // Parse parameters
101        let params = parse_params(&method.sig.inputs)?;
102
103        // Parse return type
104        let return_info = parse_return_type(&method.sig.output);
105
106        Ok(Some(Self {
107            method: method.clone(),
108            name,
109            docs,
110            params,
111            return_info,
112            is_async,
113        }))
114    }
115}
116
117/// Extract doc comments from attributes
118pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
119    let docs: Vec<String> = attrs
120        .iter()
121        .filter_map(|attr| {
122            if attr.path().is_ident("doc")
123                && let Meta::NameValue(meta) = &attr.meta
124                && let syn::Expr::Lit(syn::ExprLit {
125                    lit: Lit::Str(s), ..
126                }) = &meta.value
127            {
128                return Some(s.value().trim().to_string());
129            }
130            None
131        })
132        .collect();
133
134    if docs.is_empty() {
135        None
136    } else {
137        Some(docs.join("\n"))
138    }
139}
140
141/// Parse #[param(...)] attributes from a parameter
142pub fn parse_param_attrs(
143    attrs: &[syn::Attribute],
144) -> syn::Result<(Option<String>, Option<ParamLocation>, Option<String>)> {
145    let mut wire_name = None;
146    let mut location = None;
147    let mut default_value = None;
148
149    for attr in attrs {
150        if !attr.path().is_ident("param") {
151            continue;
152        }
153
154        attr.parse_nested_meta(|meta| {
155            // #[param(name = "...")]
156            if meta.path.is_ident("name") {
157                let value: syn::LitStr = meta.value()?.parse()?;
158                wire_name = Some(value.value());
159                Ok(())
160            }
161            // #[param(default = ...)]
162            else if meta.path.is_ident("default") {
163                // Accept various literal types
164                let value = meta.value()?;
165                let lookahead = value.lookahead1();
166                if lookahead.peek(syn::LitStr) {
167                    let lit: syn::LitStr = value.parse()?;
168                    default_value = Some(format!("\"{}\"", lit.value()));
169                } else if lookahead.peek(syn::LitInt) {
170                    let lit: syn::LitInt = value.parse()?;
171                    default_value = Some(lit.to_string());
172                } else if lookahead.peek(syn::LitBool) {
173                    let lit: syn::LitBool = value.parse()?;
174                    default_value = Some(lit.value.to_string());
175                } else {
176                    return Err(lookahead.error());
177                }
178                Ok(())
179            }
180            // #[param(query)] or #[param(path)] etc.
181            else if meta.path.is_ident("query") {
182                location = Some(ParamLocation::Query);
183                Ok(())
184            } else if meta.path.is_ident("path") {
185                location = Some(ParamLocation::Path);
186                Ok(())
187            } else if meta.path.is_ident("body") {
188                location = Some(ParamLocation::Body);
189                Ok(())
190            } else if meta.path.is_ident("header") {
191                location = Some(ParamLocation::Header);
192                Ok(())
193            } else {
194                Err(meta.error(
195                    "unknown attribute\n\
196                     \n\
197                     Valid attributes: name, default, query, path, body, header\n\
198                     \n\
199                     Examples:\n\
200                     - #[param(name = \"q\")]\n\
201                     - #[param(default = 10)]\n\
202                     - #[param(query)]\n\
203                     - #[param(header, name = \"X-API-Key\")]",
204                ))
205            }
206        })?;
207    }
208
209    Ok((wire_name, location, default_value))
210}
211
212/// Parse function parameters (excluding self)
213pub fn parse_params(
214    inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
215) -> syn::Result<Vec<ParamInfo>> {
216    let mut params = Vec::new();
217
218    for arg in inputs {
219        match arg {
220            FnArg::Receiver(_) => continue, // skip self
221            FnArg::Typed(pat_type) => {
222                let name = match pat_type.pat.as_ref() {
223                    Pat::Ident(pat_ident) => pat_ident.ident.clone(),
224                    other => {
225                        return Err(syn::Error::new_spanned(
226                            other,
227                            "unsupported parameter pattern\n\
228                             \n\
229                             Server-less macros require simple parameter names.\n\
230                             Use: name: String\n\
231                             Not: (name, _): (String, i32) or &name: &String",
232                        ));
233                    }
234                };
235
236                let ty = (*pat_type.ty).clone();
237                let is_optional = is_option_type(&ty);
238                let is_id = is_id_param(&name);
239
240                // Parse #[param(...)] attributes
241                let (wire_name, location, default_value) = parse_param_attrs(&pat_type.attrs)?;
242
243                params.push(ParamInfo {
244                    name,
245                    ty,
246                    is_optional,
247                    is_id,
248                    wire_name,
249                    location,
250                    default_value,
251                });
252            }
253        }
254    }
255
256    Ok(params)
257}
258
259/// Parse return type information
260pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
261    match output {
262        ReturnType::Default => ReturnInfo {
263            ty: None,
264            ok_type: None,
265            err_type: None,
266            some_type: None,
267            is_result: false,
268            is_option: false,
269            is_unit: true,
270            is_stream: false,
271            stream_item: None,
272        },
273        ReturnType::Type(_, ty) => {
274            let ty = ty.as_ref().clone();
275
276            // Check for Result<T, E>
277            if let Some((ok, err)) = extract_result_types(&ty) {
278                return ReturnInfo {
279                    ty: Some(ty),
280                    ok_type: Some(ok),
281                    err_type: Some(err),
282                    some_type: None,
283                    is_result: true,
284                    is_option: false,
285                    is_unit: false,
286                    is_stream: false,
287                    stream_item: None,
288                };
289            }
290
291            // Check for Option<T>
292            if let Some(inner) = extract_option_type(&ty) {
293                return ReturnInfo {
294                    ty: Some(ty),
295                    ok_type: None,
296                    err_type: None,
297                    some_type: Some(inner),
298                    is_result: false,
299                    is_option: true,
300                    is_unit: false,
301                    is_stream: false,
302                    stream_item: None,
303                };
304            }
305
306            // Check for impl Stream<Item=T>
307            if let Some(item) = extract_stream_item(&ty) {
308                return ReturnInfo {
309                    ty: Some(ty),
310                    ok_type: None,
311                    err_type: None,
312                    some_type: None,
313                    is_result: false,
314                    is_option: false,
315                    is_unit: false,
316                    is_stream: true,
317                    stream_item: Some(item),
318                };
319            }
320
321            // Check for ()
322            if is_unit_type(&ty) {
323                return ReturnInfo {
324                    ty: Some(ty),
325                    ok_type: None,
326                    err_type: None,
327                    some_type: None,
328                    is_result: false,
329                    is_option: false,
330                    is_unit: true,
331                    is_stream: false,
332                    stream_item: None,
333                };
334            }
335
336            // Regular type
337            ReturnInfo {
338                ty: Some(ty),
339                ok_type: None,
340                err_type: None,
341                some_type: None,
342                is_result: false,
343                is_option: false,
344                is_unit: false,
345                is_stream: false,
346                stream_item: None,
347            }
348        }
349    }
350}
351
352/// Check if a type is `Option<T>` and extract T
353pub fn extract_option_type(ty: &Type) -> Option<Type> {
354    if let Type::Path(type_path) = ty
355        && let Some(segment) = type_path.path.segments.last()
356        && segment.ident == "Option"
357        && let PathArguments::AngleBracketed(args) = &segment.arguments
358        && let Some(GenericArgument::Type(inner)) = args.args.first()
359    {
360        return Some(inner.clone());
361    }
362    None
363}
364
365/// Check if a type is `Option<T>`
366pub fn is_option_type(ty: &Type) -> bool {
367    extract_option_type(ty).is_some()
368}
369
370/// Check if a type is Result<T, E> and extract T and E
371pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
372    if let Type::Path(type_path) = ty
373        && let Some(segment) = type_path.path.segments.last()
374        && segment.ident == "Result"
375        && let PathArguments::AngleBracketed(args) = &segment.arguments
376    {
377        let mut iter = args.args.iter();
378        if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
379            (iter.next(), iter.next())
380        {
381            return Some((ok.clone(), err.clone()));
382        }
383    }
384    None
385}
386
387/// Check if a type is impl Stream<Item=T> and extract T
388pub fn extract_stream_item(ty: &Type) -> Option<Type> {
389    if let Type::ImplTrait(impl_trait) = ty {
390        for bound in &impl_trait.bounds {
391            if let syn::TypeParamBound::Trait(trait_bound) = bound
392                && let Some(segment) = trait_bound.path.segments.last()
393                && segment.ident == "Stream"
394                && let PathArguments::AngleBracketed(args) = &segment.arguments
395            {
396                for arg in &args.args {
397                    if let GenericArgument::AssocType(assoc) = arg
398                        && assoc.ident == "Item"
399                    {
400                        return Some(assoc.ty.clone());
401                    }
402                }
403            }
404        }
405    }
406    None
407}
408
409/// Check if a type is ()
410pub fn is_unit_type(ty: &Type) -> bool {
411    if let Type::Tuple(tuple) = ty {
412        return tuple.elems.is_empty();
413    }
414    false
415}
416
417/// Check if a parameter name looks like an ID
418pub fn is_id_param(name: &Ident) -> bool {
419    let name_str = name.to_string();
420    name_str == "id" || name_str.ends_with("_id")
421}
422
423/// Extract all methods from an impl block
424///
425/// Skips:
426/// - Private methods (starting with `_`)
427/// - Associated functions without `&self` receiver (constructors, etc.)
428pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
429    let mut methods = Vec::new();
430
431    for item in &impl_block.items {
432        if let ImplItem::Fn(method) = item {
433            // Skip private methods (those starting with _)
434            if method.sig.ident.to_string().starts_with('_') {
435                continue;
436            }
437            // Parse method - returns None for associated functions without self
438            if let Some(info) = MethodInfo::parse(method)? {
439                methods.push(info);
440            }
441        }
442    }
443
444    Ok(methods)
445}
446
447/// Get the struct name from an impl block
448pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
449    if let Type::Path(type_path) = impl_block.self_ty.as_ref()
450        && let Some(segment) = type_path.path.segments.last()
451    {
452        return Ok(segment.ident.clone());
453    }
454    Err(syn::Error::new_spanned(
455        &impl_block.self_ty,
456        "Expected a simple type name",
457    ))
458}