Skip to main content

slumber_macros/
lib.rs

1// Procedural macros for Slumber
2
3use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{FnArg, Ident, ItemFn, 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, and at most one argument can have this attribute.
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    // Grab metadata from the input fn, then modify it
31    let vis = inner_fn.vis.clone();
32    let original_fn_ident = inner_fn.sig.ident.clone();
33    let inner_fn_ident = format_ident!("{}_inner", original_fn_ident);
34    inner_fn.sig.ident = inner_fn_ident.clone();
35    inner_fn.vis = syn::Visibility::Inherited;
36
37    // Gather argument info and strip custom attributes for the inner function
38    let arg_infos: Vec<ArgumentInfo> = inner_fn
39        .sig
40        .inputs
41        .iter_mut()
42        .filter_map(|input| match input {
43            FnArg::Receiver(_) => None,
44            // This will scan the argument for relevant attributes, and remove
45            // them as they're consumed
46            FnArg::Typed(pat_type) => ArgumentInfo::from_pattern(pat_type),
47        })
48        .collect();
49
50    // Determine context type. If an arg has #[context], use that. Otherwise
51    // add a generic param because we can accept any context type.
52    let context_type_param = if let Some(context_info) = arg_infos
53        .iter()
54        .find(|info| matches!(info.kind, ArgumentKind::Context))
55    {
56        // Extract the type from the context parameter, handling references
57        let context_type = match &context_info.type_name {
58            syn::Type::Reference(type_ref) => &*type_ref.elem,
59            other_type => other_type,
60        };
61        quote! { #context_type }
62    } else {
63        // No context parameter found, use generic T
64        quote! { T }
65    };
66
67    // Add generic parameter if no context param exists
68    let generic_param = if arg_infos
69        .iter()
70        .any(|info| matches!(info.kind, ArgumentKind::Context))
71    {
72        quote! {}
73    } else {
74        quote! { <T> }
75    };
76
77    // Generate one statement per argument to extract each one
78    let argument_extracts = arg_infos.iter().map(ArgumentInfo::extract);
79
80    let call_args = arg_infos.iter().map(|info| {
81        let name = &info.name;
82        quote! { #name }
83    });
84
85    // If the function is async, we'll need to include that on the outer
86    // function and also inject a .await
87    let asyncness = inner_fn.sig.asyncness;
88    let await_inner = if asyncness.is_some() {
89        quote! { .await }
90    } else {
91        quote! {}
92    };
93
94    quote! {
95        #vis #asyncness fn #original_fn_ident #generic_param (
96            #[allow(unused_mut)]
97            mut arguments: ::slumber_template::Arguments<'_, #context_type_param>
98        ) -> ::core::result::Result<
99            ::slumber_template::ValueStream,
100            ::slumber_template::RenderError
101        > {
102            #inner_fn
103
104            #(#argument_extracts)*
105            // Make sure there were no extra arguments passed in
106            arguments.ensure_consumed()?;
107            let output = #inner_fn_ident(#(#call_args),*) #await_inner;
108            ::slumber_template::FunctionOutput::into_result(output)
109        }
110    }
111    .into()
112}
113
114/// Metadata about a parameter to the template function
115struct ArgumentInfo {
116    name: Ident,
117    kind: ArgumentKind,
118    type_name: syn::Type,
119}
120
121impl ArgumentInfo {
122    /// Detect the argument name and kind from its pattern. This will modify the
123    /// pattern to remove any recognized attributes.
124    fn from_pattern(pat_type: &mut PatType) -> Option<Self> {
125        let pat_ident = match &*pat_type.pat {
126            Pat::Ident(pat_ident) => pat_ident.ident.clone(),
127            _ => return None,
128        };
129
130        // Remove known attributes from this arg. Any unrecognized attributes
131        // will be left because they may be from other macros.
132        let mut attributes = ArgumentAttributes::default();
133        pat_type.attrs.retain(|attr| {
134            // Retain any attribute that we don't recognize
135            if let Some(ident) = attr.path().get_ident() {
136                !attributes.add(ident)
137            } else {
138                true
139            }
140        });
141        let kind = ArgumentKind::from_attributes(attributes);
142
143        Some(Self {
144            name: pat_ident,
145            kind,
146            type_name: (*pat_type.ty).clone(),
147        })
148    }
149
150    /// Generate code to extract this argument from an Arguments value
151    fn extract(&self) -> proc_macro2::TokenStream {
152        let name = &self.name;
153        match self.kind {
154            ArgumentKind::Context => quote! {
155                let #name = arguments.context();
156            },
157            ArgumentKind::Positional => quote! {
158                let #name = arguments.pop_position()?;
159            },
160            ArgumentKind::Kwarg => {
161                let key = name.to_string();
162                quote! {
163                    let #name = arguments.pop_keyword(#key)?;
164                }
165            }
166        }
167    }
168}
169
170/// Track what attributes are on a function argument
171#[derive(Default)]
172struct ArgumentAttributes {
173    /// `#[context]` attribute is present
174    context: bool,
175    /// `#[kwarg]` attribute is present
176    kwarg: bool,
177}
178
179impl ArgumentAttributes {
180    /// Enable the given attribute. Return false if it's an unknown attribute
181    fn add(&mut self, ident: &Ident) -> bool {
182        match ident.to_string().as_str() {
183            "context" => {
184                self.context = true;
185                true
186            }
187            "kwarg" => {
188                self.kwarg = true;
189                true
190            }
191            _ => false,
192        }
193    }
194}
195
196/// The kind of an argument defines how it should be extracted
197enum ArgumentKind {
198    /// Extract template context
199    Context,
200    /// Default (no attribute) - Extract next positional argument and convert it
201    /// using its `TryFromValue` implementation
202    Positional,
203    /// Extract keyword argument matching the parameter name and convert it
204    /// using its `TryFromValue` implementation
205    Kwarg,
206}
207
208impl ArgumentKind {
209    /// From the set of attributes on a parameter, determine how it should be
210    /// extracted
211    fn from_attributes(attributes: ArgumentAttributes) -> Self {
212        match attributes {
213            ArgumentAttributes {
214                context: false,
215                kwarg: false,
216            } => Self::Positional,
217            ArgumentAttributes {
218                context: true,
219                kwarg: false,
220            } => Self::Context,
221            ArgumentAttributes {
222                context: false,
223                kwarg: true,
224            } => Self::Kwarg,
225            ArgumentAttributes { context: true, .. } => {
226                panic!("#[context] cannot be used with other attributes")
227            }
228        }
229    }
230}