slumber_macros/
lib.rs

1// Procedural macros for Slumber
2
3use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{FnArg, Ident, ItemFn, Meta, Pat, PatType, parse_macro_input};
6
7/// Procedural macro to convert a plain function into a template function.
8///
9/// The given function can take any number of arguments, as long as each one
10/// can be converted from `Value`. It can return any output as long as it can be
11/// converted to `Result<Value, RenderError>`. The function can be sync or
12/// `async`.
13///
14/// By default, arguments to the function are extracted and supplied as
15/// positional arguments from the template function call, using the type's
16/// `TryFromValue` implementation to convert from `Value`. This can be
17/// customized using a set of attributes on each argument:
18/// - `#[context]` - Pass the template context value. Cannot be combined with
19///   other attributes.
20/// - `#[kwarg]` - Extract a keyword argument with the same name as the argument
21/// - `#[serde]` - Use the type's `Deserialize` implementation to convert from
22///   `Value`, instead of `TryFromValue`. Can be used alone for positional
23///   arguments, or combined with `#[kwarg]` for keyword arguments.
24#[proc_macro_attribute]
25pub fn template(attr: TokenStream, item: TokenStream) -> TokenStream {
26    // The input fn will be replaced by a wrapper, and it will be moved into a
27    // definition within the wrapper
28    let mut inner_fn = parse_macro_input!(item as ItemFn);
29
30    // Parse attribute for context type
31    let meta = parse_macro_input!(attr as Meta);
32    let context_type: Ident = match meta {
33        Meta::Path(path) => path.get_ident().cloned(),
34        _ => None,
35    }
36    .expect("#[template] expects context type as a parameter");
37
38    // Grab metadata from the input fn, then modify it
39    let vis = inner_fn.vis.clone();
40    let original_fn_ident = inner_fn.sig.ident.clone();
41    let inner_fn_ident = format_ident!("{}_inner", original_fn_ident);
42    inner_fn.sig.ident = inner_fn_ident.clone();
43    inner_fn.vis = syn::Visibility::Inherited;
44
45    // Gather argument info and strip custom attributes for the inner function
46    let arg_infos: Vec<ArgumentInfo> = inner_fn
47        .sig
48        .inputs
49        .iter_mut()
50        .filter_map(|input| match input {
51            FnArg::Receiver(_) => None,
52            // This will scan the argument for relevant attributes, and remove
53            // them as they're consumed
54            FnArg::Typed(pat_type) => ArgumentInfo::from_pattern(pat_type),
55        })
56        .collect();
57
58    // Generate one statement per argument to extract each one
59    let argument_extracts = arg_infos.iter().map(ArgumentInfo::extract);
60
61    let call_args = arg_infos.iter().map(|info| {
62        let name = &info.name;
63        quote! { #name }
64    });
65
66    // If the function is async, we'll need to include that on the outer
67    // function and also inject a .await
68    let asyncness = inner_fn.sig.asyncness;
69    let await_inner = if asyncness.is_some() {
70        quote! { .await }
71    } else {
72        quote! {}
73    };
74
75    quote! {
76        #vis #asyncness fn #original_fn_ident(
77            #[allow(unused_mut)]
78            mut arguments: ::slumber_template::Arguments<'_, #context_type>
79        ) -> ::core::result::Result<
80            ::slumber_template::Value,
81            ::slumber_template::RenderError
82        > {
83            #inner_fn
84
85            #(#argument_extracts)*
86            // Make sure there were no extra arguments passed in
87            arguments.ensure_consumed()?;
88            let output = #inner_fn_ident(#(#call_args),*) #await_inner;
89            ::slumber_template::FunctionOutput::into_result(output)
90        }
91    }
92    .into()
93}
94
95/// Metadata about a parameter to the template function
96struct ArgumentInfo {
97    name: Ident,
98    kind: ArgumentKind,
99}
100
101impl ArgumentInfo {
102    /// Detect the argument name and kind from its pattern. This will modify the
103    /// pattern to remove any recognized attributes.
104    fn from_pattern(pat_type: &mut PatType) -> Option<Self> {
105        let pat_ident = match &*pat_type.pat {
106            Pat::Ident(pat_ident) => pat_ident.ident.clone(),
107            _ => return None,
108        };
109
110        // Remove known attributes from this arg. Any unrecognized attributes
111        // will be left because they may be from other macros.
112        let mut attributes = ArgumentAttributes::default();
113        pat_type.attrs.retain(|attr| {
114            // Retain any attribute that we don't recognize
115            if let Some(ident) = attr.path().get_ident() {
116                !attributes.add(ident)
117            } else {
118                true
119            }
120        });
121        let kind = ArgumentKind::from_attributes(attributes);
122
123        Some(Self {
124            name: pat_ident,
125            kind,
126        })
127    }
128
129    /// Generate code to extract this argument from an Arguments value
130    fn extract(&self) -> proc_macro2::TokenStream {
131        let name = &self.name;
132        match self.kind {
133            ArgumentKind::Context => quote! {
134                let #name = arguments.context();
135            },
136            ArgumentKind::Positional => quote! {
137                let #name = arguments.pop_position()?;
138            },
139            ArgumentKind::Kwarg => {
140                let key = name.to_string();
141                quote! {
142                    let #name = arguments.pop_keyword(#key)?;
143                }
144            }
145        }
146    }
147}
148
149/// Track what attributes are on a function argument
150#[derive(Default)]
151struct ArgumentAttributes {
152    /// #[context] attribute is present
153    context: bool,
154    /// #[kwarg] attribute is present
155    kwarg: bool,
156}
157
158impl ArgumentAttributes {
159    /// Enable the given attribute. Return false if it's an unknown attribute
160    fn add(&mut self, ident: &Ident) -> bool {
161        match ident.to_string().as_str() {
162            "context" => {
163                self.context = true;
164                true
165            }
166            "kwarg" => {
167                self.kwarg = true;
168                true
169            }
170            _ => false,
171        }
172    }
173}
174
175/// The kind of an argument defines how it should be extracted
176enum ArgumentKind {
177    /// Extract template context
178    Context,
179    /// Default (no attribute) - Extract next positional argument and convert it
180    /// using its `TryFromValue` implementation
181    Positional,
182    /// Extract keyword argument matching the parameter name and convert it
183    /// using its `TryFromValue` implementation
184    Kwarg,
185}
186
187impl ArgumentKind {
188    /// From the set of attributes on a parameter, determine how it should be
189    /// extracted
190    fn from_attributes(attributes: ArgumentAttributes) -> Self {
191        match attributes {
192            ArgumentAttributes {
193                context: false,
194                kwarg: false,
195            } => Self::Positional,
196            ArgumentAttributes {
197                context: true,
198                kwarg: false,
199            } => Self::Context,
200            ArgumentAttributes {
201                context: false,
202                kwarg: true,
203            } => Self::Kwarg,
204            ArgumentAttributes { context: true, .. } => {
205                panic!("#[context] cannot be used with other attributes")
206            }
207        }
208    }
209}