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}