#![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")
)
}
}
fn static_toml2(input: TokenStream2) -> Result<TokenStream2, Error> {
let static_toml_data: StaticToml = syn::parse2(input).map_err(Error::Syn)?;
let mut tokens = Vec::with_capacity(static_toml_data.0.len());
for static_toml in static_toml_data.0.iter() {
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
))?;
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);
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()];
let visibility = static_toml
.visibility
.as_ref()
.map(|vis| vis.to_token_stream())
.unwrap_or_default();
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))?;
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
};
let name = &static_toml.name;
let root_type = fixed_ident(
root_mod.to_string().as_str(),
&static_toml.attrs.prefix,
&static_toml.attrs.suffix
);
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;
tokens.push(quote! {
#(#doc)*
#auto_doc
#visibility #storage_class #name: #root_mod::#root_type = #static_tokens;
#(#other_attrs)*
#type_tokens
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)
}
}
}