1
Fork 0
mirror of https://github.com/Steffo99/micronfig.git synced 2025-01-07 02:59:43 +00:00
micronfig/micronfig_macros/src/lib.rs

205 lines
No EOL
5.1 KiB
Rust

extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{Ident, parse_macro_input, Token, Type, TypePath};
use syn::punctuated::Punctuated;
type Config = Punctuated<ConfigItem, Token![,]>;
#[derive(Clone)]
struct ConfigItem {
identifier: Ident,
optional: bool,
types: Vec<ConfigPair>,
}
#[derive(Clone)]
struct ConfigPair {
conversion: Conversion,
r#type: Type,
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
enum Conversion {
From,
TryFrom,
FromStr,
}
impl Parse for ConfigItem {
fn parse(input: ParseStream) -> syn::Result<Self> {
let identifier = input.parse::<Ident>()?;
let optional = input.lookahead1().peek(Token![?]);
if optional {
input.parse::<Token![?]>()
.expect("this token to be parsed correctly, as it has been previously peeked");
}
let types = match input.lookahead1().peek(Token![:]) {
true => {
input.parse::<Token![:]>()
.expect("this token to be parsed correctly, as it has been previously peeked");
let string_type = input.parse::<TypePath>()?;
if &*string_type.to_token_stream().to_string() != "String" {
return Err(
syn::Error::new_spanned(
string_type,
"first type of a conversion chain should always be literally `String`, other aliases are not allowed"
)
);
}
let mut types = Vec::new();
while let Ok(typ) = input.parse::<ConfigPair>() {
types.push(typ)
}
types
},
false => Vec::new(),
};
Ok(Self { identifier, optional, types })
}
}
impl Parse for ConfigPair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let conversion = input.parse::<Conversion>()?;
let r#type = input.parse::<Type>()?;
Ok(Self { conversion, r#type })
}
}
impl Parse for Conversion {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.parse::<Token![->]>().is_ok() {
Ok(Conversion::From)
}
else if input.parse::<Token![=>]>().is_ok() {
Ok(Conversion::TryFrom)
}
else if input.parse::<Token![>]>().is_ok() {
Ok(Conversion::FromStr)
}
else {
Err(input.error("cannot determine conversion method to use; valid conversion tokens are `->` (From), `=>` (TryFrom) and `>` (FromStr)."))
}
}
}
#[proc_macro]
pub fn config(input: TokenStream) -> TokenStream {
let input: Config = parse_macro_input!(input with syn::punctuated::Punctuated::parse_terminated);
let cache_code = quote! {
#[allow(non_snake_case)]
mod _cache {
pub static _lock: std::sync::OnceLock<micronfig::cache::Cache> = std::sync::OnceLock::new();
}
#[allow(non_snake_case)]
fn _cache() -> &'static micronfig::cache::Cache {
_cache::_lock.get_or_init(micronfig::cache::Cache::new)
}
};
let items_code = input.iter().map(|item: &ConfigItem| {
let identifier = &item.identifier;
let identifier_string = identifier.to_string();
let type_final = match item.types.last() {
Some(pair) => {
let typ = pair.r#type.clone();
quote! { #typ }
},
None => {
quote! { std::string::String }
},
};
let type_final_option = match item.optional {
true => quote! { std::option::Option<#type_final> },
false => quote! { #type_final },
};
let conversion_code = item.types.iter().map(
|ConfigPair { r#type, conversion }| {
let typ = r#type;
match (conversion, item.optional) {
(Conversion::From, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.into());
},
(Conversion::TryFrom, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.try_into())
.map(|v| v.expect(&format!("to be able to convert {}", #identifier_string)));
},
(Conversion::FromStr, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.parse())
.map(|v| v.expect(&format!("to be able to parse {}", #identifier_string)));
},
(Conversion::From, false) => quote! {
let value: #typ = value
.into();
},
(Conversion::TryFrom, false) => quote! {
let value: #typ = value
.try_into()
.expect(&format!("to be able to convert {}", #identifier_string));
},
(Conversion::FromStr, false) => quote! {
let value: #typ = value
.parse()
.expect(&format!("to be able to parse {}", #identifier_string));
},
}
}
).reduce(|acc, new| {
quote! { #acc #new }
});
let require_code = match item.optional {
true => quote! {},
false => quote! {
let value: String = value
.expect(&format!("that configuration variable {} was set", #identifier_string));
},
};
quote! {
#[allow(non_snake_case)]
mod #identifier {
pub(super) static _lock: std::sync::OnceLock<#type_final_option> = std::sync::OnceLock::new();
}
#[allow(non_snake_case)]
pub(crate) fn #identifier() -> &'static #type_final_option {
#identifier::_lock.get_or_init(|| {
let key = #identifier_string.as_ref();
let value: Option<std::string::String> = _cache().get(key);
#require_code
#conversion_code
value
})
}
}
}).reduce(|acc, new| {
quote! { #acc #new }
});
let quote = quote! {
#cache_code
#items_code
};
quote.into()
}