display_as_proc_macro/
lib.rs

1//! This is the implementation crate for `display-as-template`.
2
3extern crate proc_macro;
4// extern crate syn;
5#[macro_use]
6extern crate quote;
7extern crate glob;
8extern crate proc_macro2;
9
10use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
11use std::fmt::Write;
12use std::fs::File;
13use std::io::Read;
14use std::path::{Path, PathBuf};
15
16fn find_template_file(path: &str) -> PathBuf {
17    let sourcedirs: std::collections::HashSet<_> = glob::glob("**/*.rs").unwrap()
18        .flat_map(|x| x.ok())
19        .filter(|x| !x.starts_with("target/"))
20        .map(|x| PathBuf::from(x.clone().parent().unwrap()))
21        .collect();
22    let paths: Vec<_> = sourcedirs.into_iter()
23        .filter(|d| d.join(path).exists())
24        .collect();
25    if paths.len() == 0 {
26        panic!("No template file named {:?} exists.", path);
27    } else if paths.len() > 1 {
28        panic!(r"Multiple files named {:?} exist.  Eventually display-as will
29    support this, but for now each template file must have a unique name.", path);
30    }
31    paths.into_iter().next().unwrap()
32}
33
34fn proc_to_two(i: TokenStream) -> proc_macro2::TokenStream {
35    i.into()
36}
37fn two_to_proc(i: proc_macro2::TokenStream) -> TokenStream {
38    i.into()
39}
40
41fn is_str(x: &TokenTree) -> bool {
42    match x {
43        TokenTree::Literal(_) => {
44            let s = x.to_string();
45            s.len() > 0 && s.contains("\"") && s.chars().next() != Some('b')
46        }
47        _ => false,
48    }
49}
50
51fn to_tokens(s: &str) -> impl Iterator<Item = TokenTree> {
52    let ts: TokenStream = s.parse().unwrap();
53    ts.into_iter()
54}
55
56fn count_pounds(x: &str) -> &'static str {
57    for pounds in &["#######", "######", "#####", "####", "###", "##", "#", ""] {
58        if x.contains(pounds) {
59            return pounds;
60        }
61    }
62    ""
63}
64
65/// Use the given template to create a string.
66///
67/// You can think of this as being kind of like `format!` on strange drugs.
68#[proc_macro]
69pub fn format_as(input: TokenStream) -> TokenStream {
70    let mut tokens = input.into_iter();
71    let format = if let Some(format) = tokens.next() {
72        proc_to_two(format.into())
73    } else {
74        panic!("format_as! needs a Format as its first argument")
75    };
76    if let Some(comma) = tokens.next() {
77        if &comma.to_string() != "," {
78            panic!(
79                "format_as! needs a Format followed by a comma, not {}",
80                comma.to_string()
81            );
82        }
83    } else {
84        panic!("format_as! needs a Format followed by a comma");
85    }
86
87    let statements = proc_to_two(template_to_statements(
88        "templates".as_ref(),
89        &format,
90        tokens.collect(), "", ""
91    ));
92
93    quote!(
94        {
95            use std::fmt::Write;
96            use display_as::DisplayAs;
97            let doit = || -> Result<String, std::fmt::Error> {
98                let mut __f = String::with_capacity(32);
99                #statements
100                Ok(__f)
101            };
102            display_as::FormattedString::<#format>::from_formatted(doit().expect("trouble writing to String??!"))
103        }
104    )
105    .into()
106}
107
108/// Write the given template to a file.
109///
110/// You can think of this as being kind of like `write!` on strange drugs.
111#[proc_macro]
112pub fn write_as(input: TokenStream) -> TokenStream {
113    let mut tokens = input.into_iter();
114    let format = if let Some(format) = tokens.next() {
115        proc_to_two(format.into())
116    } else {
117        panic!("write_as! needs a Format as its first argument")
118    };
119    if let Some(comma) = tokens.next() {
120        if &comma.to_string() != "," {
121            panic!(
122                "write_as! needs a Format followed by a comma, not {}",
123                comma.to_string()
124            );
125        }
126    } else {
127        panic!("write_as! needs a Format followed by a comma");
128    }
129
130    let mut writer: Vec<TokenTree> = Vec::new();
131    while let Some(tok) = tokens.next() {
132        if &tok.to_string() == "," {
133            break;
134        } else {
135            writer.push(tok);
136        }
137    }
138    if writer.len() == 0 {
139        panic!("write_as! needs a Writer as its second argument followed by comma.")
140    }
141    let writer = proc_to_two(writer.into_iter().collect());
142
143    let statements = proc_to_two(template_to_statements(
144        "templates".as_ref(),
145        &format,
146        tokens.collect(), "", ""
147    ));
148
149    quote!(
150        {
151            use std::fmt::Write;
152            use display_as::DisplayAs;
153            let __f = &mut #writer;
154            let mut doit = || -> Result<(), std::fmt::Error> {
155                #statements
156                Ok(())
157            };
158            doit()
159        }
160    )
161    .into()
162}
163
164fn expr_toks_to_stmt(
165    format: &proc_macro2::TokenStream,
166    expr: &mut Vec<TokenTree>,
167) -> impl Iterator<Item = TokenTree> {
168    let len = expr.len();
169
170    let to_display_as = {
171        // We generate a unique method name to avoid a bug that happens if
172        // there are nested calls to format_as!.  The ToDisplayAs type below
173        // is my hokey approach to use deref coersion (which happens on method
174        // calls) ensure that either references to DisplayAs types or the types
175        // themselves can be used.
176        use rand::distributions::Alphanumeric;
177        use rand::{thread_rng, Rng};
178        use std::iter;
179
180        let mut rng = thread_rng();
181        let rand_chars: String = iter::repeat(())
182            .map(|()| rng.sample(Alphanumeric))
183            .map(char::from)
184            .take(13)
185            .collect();
186        proc_macro2::Ident::new(
187            &format!("ToDisplayAs{}xxx{}", format, rand_chars),
188            proc_macro2::Span::call_site(),
189        )
190    };
191    if len > 2 && expr[len - 2].to_string() == "as" {
192        let format = proc_to_two(expr.pop().unwrap().into());
193        expr.pop();
194        let expr = proc_to_two(expr.drain(..).collect());
195        two_to_proc(quote! {
196            {
197                trait ToDisplayAs {
198                    fn #to_display_as(&self) -> &Self;
199                }
200                impl<T: DisplayAs<#format>> ToDisplayAs for T {
201                    fn #to_display_as(&self) -> &Self { self }
202                }
203                __f.write_fmt(format_args!("{}", <_ as DisplayAs<#format>>::display((#expr).#to_display_as())))?;
204            }
205        })
206        .into_iter()
207    } else if expr.len() > 0 {
208        let expr = proc_to_two(expr.drain(..).collect());
209        let format = format.clone();
210        two_to_proc(quote! {
211            {
212                trait ToDisplayAs {
213                    fn #to_display_as(&self) -> &Self;
214                }
215                impl<T: DisplayAs<#format>> ToDisplayAs for T {
216                    fn #to_display_as(&self) -> &Self { self }
217                }
218                __f.write_fmt(format_args!("{}", <_ as DisplayAs<#format>>::display((#expr).#to_display_as())))?;
219            }
220        })
221        .into_iter()
222    } else {
223        two_to_proc(quote! {}).into_iter()
224    }
225}
226fn expr_toks_to_conditional(expr: &mut Vec<TokenTree>) -> TokenStream {
227    expr.drain(..).collect()
228}
229
230fn read_template_file(dirname: &Path, pathname: &str,
231                      left_delim: &str, right_delim: &str) -> TokenStream {
232    let path = dirname.join(&pathname);
233    if let Ok(mut f) = File::open(&path) {
234        let mut contents = String::new();
235        f.read_to_string(&mut contents)
236            .expect("something went wrong reading the file");
237        let raw_template_len = contents.len();
238        let pounds: String = if left_delim == "" {
239            count_pounds(&contents).to_string()
240        } else {
241            let mut pounds = count_pounds(&contents).to_string();
242            pounds.write_str("#").unwrap();
243            contents = contents.replace(left_delim, &format!(r#""{}"#, pounds));
244            contents = contents.replace(right_delim, &format!(r#"r{}""#, pounds));
245            pounds
246        };
247        contents.write_str("\"").unwrap();
248        contents.write_str(&pounds).unwrap();
249        let mut template = "r".to_string();
250        template.write_str(&pounds).unwrap();
251        template.write_str("\"").unwrap();
252        template.write_str(&contents).unwrap();
253        template
254            .write_str("  ({ assert_eq!(include_str!(\"")
255            .unwrap();
256        template.write_str(&pathname).unwrap();
257        write!(template, "\").len(), {}); \"\"}}); ", raw_template_len).unwrap();
258        template.parse().expect("trouble parsing file")
259    } else {
260        panic!("No such file: {}", path.display())
261    }
262}
263
264fn template_to_statements(
265    dir: &Path,
266    format: &proc_macro2::TokenStream,
267    template: TokenStream,
268    left_delim: &str,
269    right_delim: &str) -> TokenStream
270{
271    let mut toks: Vec<TokenTree> = Vec::new();
272    let mut next_expr: Vec<TokenTree> = Vec::new();
273    for t in template.into_iter() {
274        if let TokenTree::Group(g) = t.clone() {
275            let next_expr_len = next_expr.len();
276            if g.delimiter() == Delimiter::Brace {
277                if next_expr_len > 2
278                    && !next_expr.iter().any(|x| x.to_string() == "=")
279                    && &next_expr[0].to_string() == "if"
280                    && &next_expr[1].to_string() == "let"
281                {
282                    // We presumably are looking at a destructuring
283                    // pattern.
284                    next_expr.push(t);
285                } else if next_expr_len > 1
286                    && &next_expr[next_expr_len - 1].to_string() != "="
287                    && &next_expr[0].to_string() == "let"
288                {
289                    // We presumably are looking at a destructuring
290                    // pattern.
291                    next_expr.push(t);
292                } else if next_expr_len > 2
293                    && &next_expr[next_expr_len - 1].to_string() == "="
294                    && &next_expr[0].to_string() == "let"
295                {
296                    // We are doing an assignment to a template
297                    // thingy, so let's create a DisplayAs thingy
298                    // rather than adding the stuff right now.
299                    toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter());
300                    let actions = proc_to_two(template_to_statements(dir, format, g.stream(),
301                                                                     left_delim, right_delim));
302                    toks.extend(
303                        two_to_proc(quote! {
304                            display_as::display_closure_as(#format, |__f: &mut ::std::fmt::Formatter|
305                                 -> Result<(), ::std::fmt::Error> {
306                                { #actions };
307                                Ok(())
308                            })
309                            // |_format: #format, __f: &mut ::std::fmt::Formatter|
310                            //      -> Result<(), ::std::fmt::Error> {
311                            //     { #actions };
312                            //     Ok(())
313                            // }
314                        })
315                        .into_iter(),
316                    );
317                } else if next_expr_len > 0 && &next_expr[0].to_string() == "match" {
318                    toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter());
319                    let mut interior_toks: Vec<TokenTree> = Vec::new();
320                    for x in g.stream() {
321                        if let TokenTree::Group(g) = x.clone() {
322                            if g.delimiter() == Delimiter::Brace {
323                                interior_toks.push(TokenTree::Group(Group::new(
324                                    Delimiter::Brace,
325                                    template_to_statements(dir, format, g.stream(),
326                                                           left_delim, right_delim),
327                                    )));
328                            } else {
329                                interior_toks.push(x);
330                            }
331                        } else {
332                            interior_toks.push(x);
333                        }
334                    }
335                    toks.push(TokenTree::Group(Group::new(Delimiter::Brace,
336                                                          interior_toks.into_iter().collect())));
337                } else {
338                    toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter());
339                    toks.push(TokenTree::Group(Group::new(
340                        Delimiter::Brace,
341                        template_to_statements(dir, format, g.stream(),
342                                               left_delim, right_delim),
343                    )));
344                }
345            } else if g.delimiter() == Delimiter::Parenthesis
346                && next_expr.len() >= 2
347                && &next_expr[next_expr_len - 1].to_string() == "!"
348                && &next_expr[next_expr_len - 2].to_string() == "include"
349            {
350                next_expr.pop();
351                next_expr.pop(); // remove the include!
352                let filenames: Vec<_> = g.stream().into_iter().collect();
353                if filenames.len() != 1 {
354                    panic!(
355                        "include! macro within a template must have one argument, a string literal"
356                    );
357                }
358                let filename = filenames[0].to_string().replace("\"", "");
359                let templ = read_template_file(dir, &filename, left_delim, right_delim);
360                let statements = template_to_statements(dir, format, templ,
361                                                        left_delim, right_delim);
362                next_expr.extend(statements.into_iter());
363                next_expr.extend(to_tokens(";").into_iter());
364                toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter());
365                toks.push(t);
366            } else {
367                next_expr.push(t);
368            }
369        } else if t.to_string() == ";" {
370            toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter());
371            toks.push(t);
372        } else if is_str(&t) {
373            // First print the previous expression...
374            toks.extend(expr_toks_to_stmt(&format, &mut next_expr));
375            // Now we print this str...
376            toks.extend(to_tokens("__f.write_str"));
377            toks.push(TokenTree::Group(Group::new(
378                Delimiter::Parenthesis,
379                TokenStream::from(t),
380            )));
381            toks.extend(to_tokens("?;"));
382        } else {
383            next_expr.push(t);
384        }
385    }
386    // Now print the final expression...
387    toks.extend(expr_toks_to_stmt(&format, &mut next_expr));
388    TokenTree::Group(Group::new(Delimiter::Brace, toks.into_iter().collect())).into()
389}
390
391/// Implement `DisplayAs` for a given type.
392///
393/// Why not use `derive`? Because we need to be able to specify which
394/// format we want to implement, and we might want to also use
395/// additional generic bounds.
396///
397/// You may use `with_template` in two different ways: inline or with
398/// a separate template file.  To use an inline template, you provide
399/// your template as an argument, as in `#[with_template("Vec(" self.x
400/// "," self.y "," self.z ",")]`.  The template consists of
401/// alternating strings and expressions, although you can also use if
402/// statements, for loops, or match expressions, although match
403/// expressions must use curly braces on each branch.
404///
405/// A template file is specified by giving the path relative to the
406/// current source file as a string argument:
407/// `#[with_template("filename.html")]`.  There are a few hokey
408/// restrictions on your filenames.
409///
410/// 1. Your filename cannot have an embedded `"` character.
411/// 2. Your string specifying the filename cannot be a "raw" string.
412/// 3. You cannot use any characters (including a backslash) that need escaping in rust strings.
413///
414/// These constraints are very hokey, and may be lifted in the future.
415/// File a bug report if you have a good use for lifting these
416/// constraints.
417///
418/// The file itself will have a template like those above, but without
419/// the beginning or ending quotation marks.  Furthermore, it is
420/// assumed that you are using raw strings, and that you use an equal
421/// number of `#` signs throughout.
422///
423/// You may also give **three** strings to `with_template`, in which
424/// case the first two strings are the left and right delimiters for
425/// rust content.  This can make your template files a little easier
426/// to read.
427#[proc_macro_attribute]
428pub fn with_template(input: TokenStream, my_impl: TokenStream) -> TokenStream {
429    let mut sourcedir = PathBuf::from(".");
430
431    let mut impl_toks: Vec<_> = my_impl.into_iter().collect();
432    if &impl_toks[0].to_string() != "impl" || impl_toks.len() < 3 {
433        panic!("with_template can only be applied to an impl of DisplayAs");
434    }
435    let mut my_format: proc_macro2::TokenStream = quote!();
436    for i in 0..impl_toks.len() - 2 {
437        if impl_toks[i].to_string() == "DisplayAs" && impl_toks[i + 1].to_string() == "<" {
438            my_format = proc_to_two(impl_toks[i + 2].clone().into());
439            break;
440        }
441    }
442    let last = impl_toks.pop().unwrap();
443    match last.to_string().as_ref() {
444        "{  }" | "{ }" | "{}" => (), // this is what we expect.
445        s => panic!(
446            "with_template must be applied to an impl that ends in '{{}}', not {}",
447            s
448        ),
449    };
450    let my_format = my_format; // no longer mut
451
452    let input_vec: Vec<_> = input.clone().into_iter().collect();
453    let mut left_delim = "".to_string();
454    let mut right_delim = "".to_string();
455    let input = if input_vec.len() == 1 {
456        let pathname = input_vec[0].to_string().replace("\"", "");
457        sourcedir = find_template_file(&pathname);
458        read_template_file(&sourcedir, &pathname, "", "")
459    } else if input_vec.len() == 3
460        && input_vec[0].to_string().contains("\"")
461        && input_vec[1].to_string().contains("\"")
462        && input_vec[2].to_string().contains("\"")
463    {
464        // If we have three string literals, the first two are the
465        // delimiters we want to use.
466        let pathname = input_vec[2].to_string().replace("\"", "");
467        sourcedir = find_template_file(&pathname);
468        left_delim = input_vec[0].to_string().replace("\"", "");
469        right_delim = input_vec[1].to_string().replace("\"", "");
470        read_template_file(&sourcedir, &pathname, &left_delim, &right_delim)
471    } else {
472        input
473    };
474    let statements = proc_to_two(template_to_statements(&sourcedir, &my_format, input,
475                                                        &left_delim, &right_delim));
476
477    let out = quote! {
478        {
479            #statements
480            Ok(())
481        }
482    };
483    let mut new_impl: Vec<TokenTree> = Vec::new();
484    new_impl.extend(impl_toks.into_iter());
485    new_impl.extend(
486        two_to_proc(quote! {
487            {
488                fn fmt(&self, __f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
489                    #out
490                }
491            }
492        })
493        .into_iter(),
494    );
495    let new_impl = new_impl.into_iter().collect();
496
497    // println!("new_impl is {}", &new_impl);
498    new_impl
499}
500
501/// Like [macro@with_template], but also generate any web responder
502/// implementations that are handled via feature flags.
503#[proc_macro_attribute]
504pub fn with_response_template(input: TokenStream, my_impl: TokenStream) -> TokenStream {
505    let displayas_impl = with_template(input, my_impl.clone());
506    displayas_impl
507}