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}