1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#![doc = include_str!("../doc/crate.md")]

extern crate proc_macro;

use std::fmt::{Debug, Formatter};
use std::path::PathBuf;
use std::{env, fs, io};

use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
use quote::{format_ident, quote, ToTokens};
use syn::LitStr;
use toml::value::{Table, Value};

use crate::parse::{StaticToml, StaticTomlItem, StorageClass};
use crate::toml_tokens::{fixed_ident, TomlTokens};

mod parse;
mod toml_tokens;

#[doc = include_str!("../doc/macro.md")]
#[proc_macro_error]
#[proc_macro]
pub fn static_toml(input: TokenStream) -> TokenStream {
    let token_stream2 = TokenStream2::from(input);
    match static_toml2(token_stream2) {
        Ok(ts) => ts.into(),
        Err(Error::Syn(e)) => abort!(e.span(), e.to_string()),
        Err(Error::MissingCargoManifestDirEnv) => {
            abort_call_site!("`CARGO_MANIFEST_DIR` env not set"; help = "use `cargo` to build")
        }
        Err(Error::Toml(p, TomlError::FilePathInvalid)) => {
            abort!(p, "cannot construct valid file path"; note = "path to file must be valid utf-8")
        }
        Err(Error::Toml(p, TomlError::ReadToml(e))) => abort!(p, e.to_string()),
        Err(Error::Toml(p, TomlError::ParseToml(e))) => abort!(p, e.to_string()),
        Err(Error::Toml(p, TomlError::KeyInvalid(k))) => abort!(
            p,
            format!("`{k}` cannot be converted to a valid identifier")
        )
    }
}

/// Process the input token stream and generate the corresponding Rust code
/// using `proc_macro2`.
///
/// This function serves as the `proc_macro2` variant of the `static_toml`
/// procedural macro.
/// It is necessary for making the library testable.
/// By using `proc_macro2` data structures, this function can be tested in
/// environments where procedural macros are not natively supported.
fn static_toml2(input: TokenStream2) -> Result<TokenStream2, Error> {
    // Parse the input into StaticToml data structure.
    let static_toml_data: StaticToml = syn::parse2(input).map_err(Error::Syn)?;

    // Iterate through each static_toml item, process it, and generate the
    // corresponding Rust code.
    let mut tokens = Vec::with_capacity(static_toml_data.0.len());
    for static_toml in static_toml_data.0.iter() {
        // Construct the full path to the TOML file that needs to be embedded.
        let mut file_path = PathBuf::new();
        file_path.push(env::var("CARGO_MANIFEST_DIR").or(Err(Error::MissingCargoManifestDirEnv))?);
        file_path.push(static_toml.path.value());
        let include_file_path = file_path.to_str().ok_or(Error::Toml(
            static_toml.path.clone(),
            TomlError::FilePathInvalid
        ))?;

        // Read the TOML file and parse it into a TOML table.
        let content = fs::read_to_string(&file_path)
            .map_err(|e| Error::Toml(static_toml.path.clone(), TomlError::ReadToml(e)))?;
        let table: Table = toml::from_str(&content)
            .map_err(|e| Error::Toml(static_toml.path.clone(), TomlError::ParseToml(e)))?;
        let value_table = Value::Table(table);

        // Determine the root module name, either specified by the user or the default
        // based on the static value's name.
        let root_mod = static_toml.attrs.root_mod.clone().unwrap_or(format_ident!(
            "{}",
            static_toml.name.to_string().to_case(Case::Snake)
        ));
        let mut namespace = vec![root_mod.clone()];

        // Determine the visibility of the generated code, either specified by the user
        // or default.
        let visibility = static_toml
            .visibility
            .as_ref()
            .map(|vis| vis.to_token_stream())
            .unwrap_or_default();

        // Generate the tokens for the static value based on the parsed TOML data.
        let static_tokens = value_table
            .static_tokens(
                root_mod.to_string().as_str(),
                &static_toml.attrs,
                &mut namespace
            )
            .map_err(|e| Error::Toml(static_toml.path.clone(), e))?;

        // Generate the tokens for the types based on the parsed TOML data.
        let type_tokens = value_table
            .type_tokens(
                root_mod.to_string().as_str(),
                &static_toml.attrs,
                visibility,
                &static_toml.derive
            )
            .map_err(|e| Error::Toml(static_toml.path.clone(), e))?;

        let storage_class: &dyn ToTokens = match static_toml.storage_class {
            StorageClass::Static(ref token) => token,
            StorageClass::Const(ref token) => token
        };

        // Extract relevant fields from the StaticTomlItem.
        let name = &static_toml.name;
        let root_type = fixed_ident(
            root_mod.to_string().as_str(),
            &static_toml.attrs.prefix,
            &static_toml.attrs.suffix
        );

        // Generate auto doc comments.
        let raw_file_path = static_toml.path.value();
        let auto_doc = match (
            static_toml
                .attrs
                .auto_doc
                .as_ref()
                .map(|lit_bool| lit_bool.value),
            static_toml.doc.len()
        ) {
            (None, 0) | (Some(true), _) => {
                toml_tokens::gen_auto_doc(&raw_file_path, &content, &static_toml.storage_class)
            }

            (None, _) | (Some(false), _) => Default::default()
        };

        let StaticTomlItem {
            doc,
            other_attrs,
            visibility,
            ..
        } = static_toml;

        // Generate the final Rust code for the static value and types.
        tokens.push(quote! {
            #(#doc)*
            #auto_doc
            #visibility #storage_class #name: #root_mod::#root_type = #static_tokens;

            #(#other_attrs)*
            #type_tokens

            // This is a trick to make the compiler re-evaluate the macro call when the included file changes.
            const _: &str = include_str!(#include_file_path);
        });
    }

    Ok(TokenStream2::from_iter(tokens))
}

pub(crate) enum Error {
    Syn(syn::Error),
    MissingCargoManifestDirEnv,
    Toml(LitStr, TomlError)
}

#[derive(Debug)]
pub(crate) enum TomlError {
    FilePathInvalid,
    ReadToml(io::Error),
    ParseToml(toml::de::Error),
    KeyInvalid(String)
}

impl Debug for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Syn(e) => write!(f, "Syn({:?})", e),
            Error::MissingCargoManifestDirEnv => write!(f, "MissingCargoManifestDirEnv"),
            Error::Toml(p, e) => write!(f, "Toml({}, {:?})", p.value(), e)
        }
    }
}