function_frame/lib.rs
1extern crate proc_macro;
2
3use proc_macro as pm;
4use proc_macro2 as pm2;
5
6use quote::ToTokens;
7use syn::parse::{Parse, Parser};
8use syn::punctuated::Punctuated;
9
10struct FrameConfig {
11 str_opts: Vec<(String, String)>,
12 num_opts: Vec<(String, usize)>,
13 bin_opts: Vec<(String, bool)>,
14}
15
16// The different types of options that we can expect from the user.
17enum Opts {
18 Num(usize),
19 Str(String),
20 Bin(bool),
21}
22
23impl FrameConfig {
24 fn new() -> Self {
25 // Pre-allocate memory for the number of options we are expecting.
26 // Though the user may give more than what we expect.
27 FrameConfig {
28 num_opts: Vec::with_capacity(1),
29 str_opts: Vec::with_capacity(2),
30 bin_opts: Vec::with_capacity(1),
31 }
32 }
33}
34
35// TODO: refactor this so that it returns an error.
36fn parse_macro_arguments(args: pm2::TokenStream) -> FrameConfig {
37 // Use a `Punctuated` sequence of `syn::ExprAssign` which is basically
38 // things of the form:
39 // ```
40 // a = b
41 // ```
42 // which are separated by the `,` character! This parser will parse all
43 // the arguments until no further `syn::ExprAssign` are found.
44 //
45 // In short, a `syn::ExprAssign` consists of the following elements:
46 // a) attrs: Vec<syn::Attributes>
47 //
48 // - which is basically any macro arguments that are
49 // directly above the expression assignment.
50 //
51 // b) left: Box<Expr>
52 //
53 // - which most of the time is an identifier, but it
54 // sometimes be another `syn::ExprAssign` like for
55 // instance:
56 // ` a = b = c `
57 //
58 // c) eq_token: syn::Token![=]
59 // d) right: Box<Expr>
60 //
61 // - which most of the time is a literal, but it could
62 // also be something else as shown above.
63 let expr_parser = Punctuated::<syn::ExprAssign, syn::Token![,]>::parse_terminated;
64
65 // Consume the argument tokenstream.
66 let expressions = match Parser::parse2(expr_parser, args) {
67 Ok(expressions) => {
68 // We cannot construct the headers if we do not have at least three
69 // arguments:
70 //
71 // 1. `title` 2. `sep` 3. `width`
72 //
73 // So if the user only provides 2 or less we cannot construct the
74 // headers so we can safely panic.
75 assert!(
76 expressions.len() > 2,
77 format!(
78 "expected at least 3 arguments received {}.",
79 expressions.len()
80 )
81 );
82 // Collect the expressions into a vector of `syn::ExprAssigns`
83 expressions.into_iter().collect::<Vec<_>>()
84 }
85 Err(_) => {
86 // Happens whenever the arguments of the `attribute_macro` are
87 // not well constructed, e.g.
88 // ```
89 // #[add_headers(title: "", ...)]
90 // ```
91 // will not work because it expects and '=' sign, not a colon.
92 // Hence it's not a valid assignment.
93 panic!("invalid list of expression arguments");
94 }
95 };
96
97 // The config object has three maps:
98 //
99 // 1. (String, usize) 2. (String, String) 3. (String, bool)
100 //
101 // Each of them is separated because it makes it so much easier to work
102 // with the data when it's separated it. If you wanted to keep a single
103 // vector for all three value types (i.e. `usize`, `String` and `bool`).
104 //
105 // Then you would constantly need to match against the multiple types
106 // that the element can be, even when you know for sure that an element
107 // with a key `K` is of type `T`.
108 let mut config = FrameConfig::new();
109
110 // Start looping through all the assignment expressions.
111 // NOTE that there is no limit as to how many of them the user is allowed
112 // pass, but we don't care about limiting this number because either
113 // way we are only going to use the one's we care about.
114 for expr in expressions {
115 // Store the identifier always as a `String`.
116 let lhs_expr = match *(expr.left) {
117 syn::Expr::Path(p) => match p.path.get_ident() {
118 Some(res) => res.to_string(),
119 None => panic!("expected identifier, found `path`."),
120 },
121 _ => panic!("expected identifer, found something else."),
122 };
123
124 // Match the `rhs` with literals only.
125 let rhs_expr: Opts = match *(expr.right) {
126 // The top level match of the `syn::Expr` inside the box
127 // will produce a `syn::ExprLit` which has an element
128 // inside called `lit` which is of type `syn::Lit` which
129 // is an enum that allows us to match against several types
130 // of specific literals.
131 //
132 // For now, we are interested in handling three kinds of
133 // literals:
134 //
135 // a) string literals b) binary literals c) integer literals
136 //
137 // If the user provides something that is not of these three
138 // types then we can safely panic.
139 syn::Expr::Lit(expr) => match expr.lit {
140 syn::Lit::Str(str_lit) => Opts::Str(str_lit.value()),
141 syn::Lit::Bool(bin_lit) => Opts::Bin(bin_lit.value),
142 syn::Lit::Int(num_lit) => {
143 Opts::Num(num_lit.base10_digits().parse::<usize>().unwrap())
144 }
145 _ => {
146 panic!("expected literal of type `bool`, `str` or `num`, found something else.")
147 }
148 },
149 _ => panic!("expected literal, found something else."),
150 };
151
152 // Place each literal and it's associated key in it's respective bucket
153 // inside the config object.
154 match rhs_expr {
155 Opts::Num(num_opt) => config.num_opts.push((lhs_expr, num_opt)),
156 Opts::Bin(bin_opt) => config.bin_opts.push((lhs_expr, bin_opt)),
157 Opts::Str(str_opt) => config.str_opts.push((lhs_expr, str_opt)),
158 }
159 }
160
161 config
162}
163
164use std::{error, fmt};
165
166#[derive(Clone, Debug)]
167struct ArgNotFound<'a> {
168 // We create this custom error, to store the name of the missing argument.
169 name: &'a str,
170}
171
172impl<'a> fmt::Display for ArgNotFound<'a> {
173 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174 write!(
175 f,
176 "expected argument with name '{}', found none.",
177 self.name
178 )
179 }
180}
181
182impl<'a> error::Error for ArgNotFound<'a> {}
183
184// Helper function to help us locate a given argument name within a specified map
185// in the `FrameConfig` object. NOTE that we have to take the map `Vec` as ref
186// and avoid moving values from it cause we might need them later.
187fn find_argument<K, V>(map: &Vec<(K, V)>, arg_name: &'static str) -> Result<V, impl error::Error>
188where
189 K: PartialEq<str>,
190 V: Clone,
191{
192 // Function is simple, we are given the key we are interested in locating, and
193 // the vector it should be located on. Notice that the key is given in the form
194 // of `arg_name` which is of type `&str` so `K` needs to be comparable to a string.
195 match map.iter().find(|(k, _)| k == arg_name) {
196 // If we find the key in the given vector, then we return its ssociated value.
197 // Since, we don't want to move the value from the Vector we are given we need
198 // the value type `V` to implement the `Clone` trait.
199 Some((_, val)) => Ok(val.clone()),
200 // Else we return an error.
201 None => Err(ArgNotFound { name: arg_name }),
202 }
203}
204
205fn construct_guards(
206 segment_title: String,
207 sep: String,
208 width: usize,
209 sep_line: bool,
210) -> (pm2::TokenStream, pm2::TokenStream) {
211 // The `sep_line` argument specifies whether the title should be printed in its
212 // own line or in a same line as the separators.
213 if sep_line {
214 // If we want the segment title in it's own line we need to modify the width
215 // given by the user to account for that.
216 let width = width + segment_title.len();
217 let hsep = sep.repeat(width);
218 let header = quote::quote! {
219 // The blank space in the `format!` macro tells rust to pad the segment
220 // title with whitespace, `width` number of times.
221 println!("{}\n{}\n{}", #hsep, format!("{: ^1$}", #segment_title, #width), #hsep);
222 };
223 // Constructing the `footer` is pretty much the same as with the header.
224 let fsep = sep.repeat(width);
225 let footer = quote::quote! {
226 println!("{}", #fsep);
227 };
228
229 (header, footer)
230 } else {
231 // Print the header and separators in the same line.
232 let hsep = sep.repeat(width);
233 let header = quote::quote! {
234 println!("{} {} {}", #hsep, #segment_title, #hsep);
235 };
236
237 let fsep = sep.repeat(2 * (width + 1) + segment_title.len());
238 let footer = quote::quote! {
239 println!("{}", #fsep);
240 };
241
242 (header, footer)
243 }
244}
245
246#[proc_macro_attribute]
247pub fn frame(args: pm::TokenStream, item: pm::TokenStream) -> pm::TokenStream {
248 // Change the input to `proc_macro2::TokenStream` as `syn` and `quote` both
249 // work with this type of `TokenStream`, and it allows for compiler version
250 // independent code, and allows the code to exist outside the macro compila-
251 // tion level -- which means you can unit test it.
252 let args = pm2::TokenStream::from(args.clone());
253 // Get the config object from the arguments passed by the user.
254 let conf = parse_macro_arguments(args);
255
256 let mut segment_title = match find_argument(&conf.str_opts, "title") {
257 Ok(title) => title,
258 Err(err) => panic!(format!("{}\nmake sure teh value is of type `str`.", err)),
259 };
260
261 // For some reason the `"` character seems to be part of the `syn::Lit` type
262 // so even after we convert it to a string, we get something that is wrapped
263 // in quotes, which in this case is undersirable.
264 segment_title.retain(|c| c != '\"');
265
266 // The separating character or string.
267 let mut sep = match find_argument(&conf.str_opts, "sep") {
268 Ok(sep) => sep,
269 Err(err) => panic!(format!("{}\nmake sure the value is of type `str`.", err)),
270 };
271
272 sep.retain(|c| c != '\"');
273
274 // The number of times you want the separator character to be repeated.
275 let width = match find_argument(&conf.num_opts, "width") {
276 Ok(width) => width,
277 Err(err) => panic!(format!("{}\nmake sure the value is of type `usize`.", err)),
278 };
279
280 // NOTE this argument is really not that important in order to construct a header,
281 // so we can make it optional. notice there's no panic if the `find_argument`
282 // function returns an error.
283 let sep_line = match find_argument(&conf.bin_opts, "sep_line") {
284 Ok(sep_line) => sep_line,
285 Err(_) => true,
286 };
287
288 // Construct two `pm2::TokenStreams` using the `quote` crate.
289 let (header, footer) = construct_guards(segment_title, sep, width, sep_line);
290
291 // Finally we need to parse the input in order to determine someone is not calling this
292 // macro in a context where it doesn't make sense. Right now, this macro expects to be
293 // used only in functions.
294 let input = pm2::TokenStream::from(item.clone());
295 match Parser::parse2(syn::ItemFn::parse, input) {
296 Ok(mut func) => {
297 // Temporarily steal the current function block.
298 let block = func.block;
299 // Reconstruct and inject the header and footer into it, yielding new token stream.
300 let new_block = quote::quote! {{
301 #header
302 let closure = move || #block;
303 let retval = closure();
304 #footer
305 retval
306 }};
307
308 // Convert the token stream into a block again, and box it.
309 func.block = Box::new(Parser::parse2(syn::Block::parse, new_block).unwrap());
310
311 // Return the modified function.
312 pm::TokenStream::from(func.to_token_stream())
313 }
314 Err(_) => panic!("macro can only be applied to `function` items."),
315 }
316}