use std::collections::HashSet;
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{Attribute, Ident as Ident2};
use toml::value::Array;
use toml::Value;
use crate::parse::{StaticTomlAttributes, StorageClass};
mod static_tokens;
mod type_tokens;
#[cfg(test)]
mod tests;
pub(crate) trait TomlTokens {
fn type_eq(&self, other: &Self) -> bool;
fn type_tokens(
&self,
key: &str,
config: &StaticTomlAttributes,
visibility: TokenStream2,
derive: &[Attribute]
) -> Result<TokenStream2, super::TomlError>;
fn static_tokens(
&self,
key: &str,
config: &StaticTomlAttributes,
namespace: &mut Vec<Ident2>
) -> Result<TokenStream2, super::TomlError>;
}
impl TomlTokens for Value {
fn type_eq(&self, other: &Self) -> bool {
use Value::*;
match (self, other) {
(String(_), String(_)) => true,
(Integer(_), Integer(_)) => true,
(Float(_), Float(_)) => true,
(Boolean(_), Boolean(_)) => true,
(Datetime(_), Datetime(_)) => true,
(Array(a), Array(b)) => {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.map(|(a, b)| a.type_eq(b))
.reduce(|acc, b| acc && b)
.unwrap_or(true)
}
(Table(a), Table(b)) => HashSet::<std::string::String>::from_iter(
a.keys().cloned().chain(b.keys().cloned())
)
.iter()
.map(|k| (a.get(k), b.get(k)))
.map(|(a, b)| match (a, b) {
(Some(a), Some(b)) => a.type_eq(b),
_ => false
})
.reduce(|acc, b| acc && b)
.unwrap_or(true),
_ => false
}
}
fn type_tokens(
&self,
key: &str,
config: &StaticTomlAttributes,
visibility: TokenStream2,
derive: &[Attribute]
) -> Result<TokenStream2, super::TomlError> {
use Value::*;
if !is_valid_identifier(key.to_case(Case::Snake).as_str()) {
return Err(super::TomlError::KeyInvalid(key.to_string()));
}
let mod_ident = format_ident!("{}", key.to_case(Case::Snake));
let type_ident = fixed_ident(key, &config.prefix, &config.suffix);
#[rustfmt::skip]
let inner = match (self, config.cow) {
(String(_), None) => quote!(pub type #type_ident = &'static str;),
(String(_), Some(_)) => quote!(pub type #type_ident = std::borrow::Cow<'static, str>;),
(Integer(_), _) => quote!(pub type #type_ident = i64;),
(Float(_), _) => quote!(pub type #type_ident = f64;),
(Boolean(_), _) => quote!(pub type #type_ident = bool;),
(Datetime(_), None) => quote!(pub type #type_ident = &'static str;),
(Datetime(_), Some(_)) => quote!(pub type #type_ident = std::borrow::Cow<'static, str>;),
(Array(values), _) => type_tokens::array(values, &type_ident, config, derive)?,
(Table(values), _) => type_tokens::table(values, &type_ident, config, derive)?
};
Ok(quote! {
#visibility mod #mod_ident {
#inner
}
})
}
fn static_tokens(
&self,
key: &str,
config: &StaticTomlAttributes,
namespace: &mut Vec<Ident2>
) -> Result<TokenStream2, super::TomlError> {
if !is_valid_identifier(key.to_case(Case::Snake).as_str()) {
return Err(super::TomlError::KeyInvalid(key.to_string()));
}
let namespace_ts = quote!(#(#namespace)::*);
Ok(match (self, config.cow) {
(Value::String(s), None) => quote!(#s),
(Value::String(s), Some(_)) => quote!(std::borrow::Cow::Borrowed(#s)),
(Value::Integer(i), _) => quote!(#i),
(Value::Float(f), _) => quote!(#f),
(Value::Boolean(b), _) => quote!(#b),
(Value::Datetime(d), cow) => {
let d = d.to_string();
match cow {
None => quote!(#d),
Some(_) => quote!(std::borrow::Cow::Borrowed(#d))
}
}
(Value::Array(values), _) => {
static_tokens::array(values, key, config, namespace, namespace_ts)?
}
(Value::Table(values), _) => {
static_tokens::table(values, key, config, namespace, namespace_ts)?
}
})
}
}
pub fn fixed_ident(ident: &str, prefix: &Option<Ident2>, suffix: &Option<Ident2>) -> Ident2 {
let ident = ident.to_case(Case::Pascal);
match (prefix, suffix) {
(None, None) => format_ident!("{ident}"),
(Some(prefix), None) => format_ident!("{prefix}{ident}"),
(None, Some(suffix)) => format_ident!("{ident}{suffix}"),
(Some(prefix), Some(suffix)) => format_ident!("{prefix}{ident}{suffix}")
}
}
fn use_slices(array: &Array, config: &StaticTomlAttributes) -> bool {
if !config
.prefer_slices
.as_ref()
.map(|b| b.value())
.unwrap_or(true)
{
return false;
}
array
.iter()
.zip(array.iter().skip(1))
.map(|(a, b)| a.type_eq(b))
.reduce(|acc, b| acc && b)
.unwrap_or(true)
}
fn is_valid_identifier(input: &str) -> bool {
let mut chars = input.chars();
let Some(first) = chars.next()
else {
return false;
};
if !(first.is_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub fn gen_auto_doc(path: &str, content: &str, storage_class: &StorageClass) -> TokenStream2 {
let storage_class = match storage_class {
StorageClass::Static(_) => "Static",
StorageClass::Const(_) => "Constant"
};
let summary = format!("{storage_class} inclusion of `{path}`.");
quote! {
#[doc = ""]
#[doc = #summary]
#[doc = ""]
#[doc = "```toml"]
#[doc = #content]
#[doc = "```"]
}
}