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}