crml_derive/
lib.rs

1use serde::{Serialize, Deserialize};
2use pathbufd::PathBufD;
3
4use std::sync::LazyLock;
5use std::fs::{read_to_string, File};
6
7mod generator;
8
9/// The `./crml.json` file.
10#[derive(Serialize, Deserialize, Clone)]
11struct Config {
12    /// The root directory to load all templates from.
13    ///
14    /// # Example
15    /// ```json
16    /// {
17    ///     "root_dir": "./templates"
18    /// }
19    /// ```
20    pub root_dir: PathBufD,
21}
22
23// read config to constant
24static CONFIG: LazyLock<Config> = LazyLock::new(|| {
25    serde_json::from_str::<Config>(
26        &read_to_string(PathBufD::current().join("crml.json"))
27            .expect("failed to read configuration"),
28    )
29    .expect("failed to deserialize configuration")
30});
31
32// macro
33use syn::{parse_macro_input, LitStr, ItemStruct};
34use quote::{quote, ToTokens};
35use proc_macro::TokenStream;
36use proc_macro2::TokenStream as TokenStream2;
37
38pub(crate) fn get_file(file: &str) -> File {
39    File::open(PathBufD::current().extend(&[CONFIG.root_dir.to_string(), format!("{}.crml", file)]))
40        .expect("failed to read included file")
41}
42
43// yes this is an attribute macro and not a derive macro, it used to be derive
44/// Mark a struct as a template and provide the name of the template file it uses.
45///
46/// # Example
47/// ```rust
48/// use crml::{template, Template}; // import template macro *and* Template trait
49///
50/// #[template("mycrmlfile")]
51/// struct MyStruct {
52///     a: i32
53/// }
54///
55/// fn main() {
56///     // the Template trait provides .render()
57///     println!("rendered: {}", MyStruct { a: 1 }.render());
58/// }
59/// ```
60#[proc_macro_attribute]
61pub fn template(args: TokenStream, input: TokenStream) -> TokenStream {
62    // parse args
63    let args = parse_macro_input!(args as LitStr);
64    let file_name = args.value();
65
66    // parse tokens
67    let input = parse_macro_input!(input as ItemStruct);
68
69    let struct_ident = input.ident.clone();
70    let mut struct_tokens = TokenStream2::new();
71    input.to_tokens(&mut struct_tokens);
72
73    // read file into generator
74    let generated = generator::Generator::from_file(get_file(&file_name)).consume();
75
76    let generated_tokens: TokenStream2 = match generated.parse() {
77        Ok(t) => t,
78        Err(e) => {
79            // debug outputs
80            if std::fs::exists("crml_dbg").expect("failed to check for debug dir") == true {
81                println!(
82                    "Debug directory found. Check \"crml_dbg/{}.rs\" to debug template.",
83                    file_name
84                );
85
86                std::fs::write(
87                    format!("crml_dbg/{file_name}.rs"),
88                    format!("fn debug() {{\n{generated}\n}}"),
89                )
90                .expect("failed to write debug file")
91            }
92
93            // panic :(
94            panic!("{}", e.to_string())
95        }
96    };
97
98    // build output
99    let expanded = quote! {
100        #struct_tokens
101
102        impl crml::Template for #struct_ident {
103            fn render(self) -> String {
104                #generated_tokens
105            }
106        }
107    };
108
109    // debug outputs
110    if std::fs::exists("crml_dbg").expect("failed to check for debug dir") == true {
111        let file_name = file_name.replace("/", "_");
112        std::fs::write(format!("crml_dbg/{file_name}.rs"), expanded.to_string())
113            .expect("failed to write debug file")
114    }
115
116    // return
117    expanded.into()
118}