use proc_macro::TokenStream;
use std::env;
use std::fmt::{Display, Formatter};
use darling::{FromDeriveInput, FromMeta};
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Configuration, attributes(config))]
pub fn derive_helper_attr(input: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(input);
let configuration = ConfigurationData::from_derive_input(&input).unwrap();
expand(configuration).into()
}
fn expand(configuration: ConfigurationData) -> TokenStream2 {
let ident = configuration.ident;
let prefix = configuration.prefix;
let files = configuration.file;
let envs = configuration.env;
let environment = configuration.environment;
let https = configuration.http;
let file_sources = file_source(envs, files);
let environment_source = environment_source(environment);
let http_sources = http_source(https);
let get_result = process_prefix(prefix, &ident);
quote! {
impl #ident {
pub fn config() -> &'static Self {
static CONFIG: ::std::sync::LazyLock<#ident> = ::std::sync::LazyLock::new(|| {
::config_plus::Config::builder()
#(#file_sources)*
#environment_source
#(#http_sources)*
.build()
.unwrap()
#get_result
});
&CONFIG
}
}
}
}
fn file_source(envs:Vec<EnvData>, files: Vec<FileData>) -> Vec<TokenStream2> {
let mut ts = vec![];
for file in files {
let path = file.path;
let require = file.require;
let format = file.format;
match format {
Some(format) => {
let format = format.to_string();
ts.push(quote! {
.add_source(
::config_plus::get_file(#path, #format).required(#require),
)
})
},
None => {
ts.push(quote! {
.add_source(
::config_plus::File::with_name(#path).required(#require),
)
})
}
}
}
for env in envs {
let name = env.name;
let require = env.require;
let format = env.format;
match env::var(name) {
Ok(path) => {
let path = path.trim();
match format {
Some(format) => {
let format = format.to_string();
ts.push(quote! {
.add_source(
::config_plus::get_file(#path, #format).required(#require),
)
})
},
None => {
ts.push(quote! {
.add_source(
::config_plus::File::with_name(#path).required(#require),
)
})
}
}
},
Err(_) => {},
}
}
ts
}
fn environment_source(environment: bool) -> TokenStream2 {
let mut ts = quote! {};
if environment {
ts.extend(quote! {
.add_source(
::config_plus::Environment::with_prefix("")
.separator(".")
.prefix_separator("")
.list_separator(",")
)
})
}
ts
}
fn http_source(https: Vec<HttpData>) -> Vec<TokenStream2> {
let mut ts = vec![];
for http in https {
let url = http.url;
let format = http.format.to_string();
let method = http.method.to_string();
ts.push(quote! {
.add_source(
::config_plus::Http::with(#url, #format, #method)
)
})
}
ts
}
fn process_prefix(prefix: String, ident: &syn::Ident) -> TokenStream2 {
let mut ts = quote! {};
if prefix.is_empty() {
return quote! {
.try_deserialize()
.unwrap()
};
}
let split = prefix.split(".");
let count = split.clone().count();
for (index, str) in split.enumerate() {
if count == 1 {
ts.extend(quote! {
.get::<#ident>(#str)
.unwrap()
});
return ts;
}
if index == 0 {
ts.extend(quote! {
.get_table(#str)
.unwrap()
})
} else if count == index + 1 {
ts.extend(quote! {
.get(#str)
.ok_or_else(|| ::config_plus::ConfigError::NotFound(#str.into()))
.unwrap()
.clone()
.try_deserialize()
.unwrap()
})
} else {
ts.extend(quote! {
.get(#str)
.ok_or_else(|| ::config_plus::ConfigError::NotFound(#str.into()))
.unwrap()
.clone()
.into_table()
.unwrap()
})
}
}
ts
}
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(config), supports(struct_named))]
struct ConfigurationData {
ident: syn::Ident,
#[darling(default)]
prefix: String,
#[darling(multiple, default)]
file: Vec<FileData>,
#[darling(multiple, default)]
env: Vec<EnvData>,
#[darling(multiple, default)]
http: Vec<HttpData>,
#[darling(default = env_default)]
environment: bool,
}
#[derive(Debug, FromMeta)]
struct FileData{
path: String,
#[darling(default)]
require: bool,
format: Option<Format>,
}
#[derive(Debug, FromMeta)]
struct EnvData{
name: String,
#[darling(default)]
require: bool,
format: Option<Format>,
}
#[derive(Debug, FromMeta)]
struct HttpData{
url: String,
#[darling(default)]
method: Method,
format: Format,
}
#[derive(Debug, FromMeta)]
enum Method {
Get,
Post,
}
impl Default for Method {
fn default() -> Self {
Method::Get
}
}
impl Display for Method{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Method::Get => write!(f, "get"),
Method::Post => write!(f, "post"),
}
}
}
#[derive(Debug, FromMeta)]
enum Format {
Toml,
Json,
Yaml,
Yml,
Ini,
Ron,
Json5,
}
impl Display for Format{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Format::Toml => write!(f, "toml"),
Format::Json => write!(f, "json"),
Format::Yaml => write!(f, "yaml"),
Format::Yml => write!(f, "yaml"),
Format::Ini => write!(f, "ini"),
Format::Ron => write!(f, "ron"),
Format::Json5 => write!(f, "json5"),
}
}
}
fn env_default() -> bool {
true
}