use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, ItemFn, ReturnType};
#[derive(Debug, FromMeta)]
struct FeatureFlagArgs {
feature: String,
#[darling(default)]
client: Option<String>,
#[darling(default)]
context: Option<String>,
#[darling(default)]
fallback: Option<String>,
#[darling(default)]
negate: bool,
}
#[proc_macro_attribute]
pub fn feature_flag(args: TokenStream, input: TokenStream) -> TokenStream {
let attr_args = match darling::ast::NestedMeta::parse_meta_list(args.into()) {
Ok(v) => v,
Err(e) => return TokenStream::from(darling::Error::from(e).write_errors()),
};
let input_fn = parse_macro_input!(input as ItemFn);
let args = match FeatureFlagArgs::from_list(&attr_args) {
Ok(args) => args,
Err(e) => return TokenStream::from(e.write_errors()),
};
expand_feature_flag(args, input_fn)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn expand_feature_flag(args: FeatureFlagArgs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
let feature_key = &args.feature;
let negate = args.negate;
let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_block = &input_fn.block;
let fn_attrs = &input_fn.attrs;
let client_expr: TokenStream2 = args
.client
.as_deref()
.unwrap_or("toggly_client")
.parse()
.unwrap_or_else(|_| quote!(toggly_client));
let context_expr: TokenStream2 = args
.context
.as_deref()
.unwrap_or("toggly::EvalContext::default()")
.parse()
.unwrap_or_else(|_| quote!(toggly::EvalContext::default()));
let fallback_expr = if let Some(fallback) = &args.fallback {
let fallback_tokens: TokenStream2 = fallback
.parse()
.unwrap_or_else(|_| quote!(Default::default()));
quote!(return #fallback_tokens;)
} else {
match &fn_sig.output {
ReturnType::Default => quote!(return;),
ReturnType::Type(_, _) => quote!(return Default::default();),
}
};
let check_condition = if negate {
quote!(!enabled)
} else {
quote!(enabled)
};
let expanded = quote! {
#(#fn_attrs)*
#fn_vis #fn_sig {
let __toggly_client = &#client_expr;
let __toggly_context = #context_expr;
let enabled = __toggly_client
.is_enabled(#feature_key, __toggly_context)
.await
.unwrap_or(false);
if !#check_condition {
#fallback_expr
}
#fn_block
}
};
Ok(expanded)
}
#[proc_macro_derive(FeatureFlags, attributes(feature))]
pub fn derive_feature_flags(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
expand_feature_flags(input)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn expand_feature_flags(input: syn::DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let variants = match &input.data {
syn::Data::Enum(data) => &data.variants,
_ => {
return Err(syn::Error::new_spanned(
input,
"FeatureFlags can only be derived for enums",
))
}
};
let mut key_arms = Vec::new();
let mut default_arms = Vec::new();
for variant in variants {
let variant_name = &variant.ident;
let mut feature_key = variant_name.to_string();
let mut default_value = false;
for attr in &variant.attrs {
if attr.path().is_ident("feature") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
let value: syn::LitStr = meta.value()?.parse()?;
feature_key = value.value();
} else if meta.path.is_ident("default") {
let value: syn::LitBool = meta.value()?.parse()?;
default_value = value.value();
}
Ok(())
})?;
}
}
key_arms.push(quote! {
#name::#variant_name => #feature_key,
});
default_arms.push(quote! {
#name::#variant_name => #default_value,
});
}
let expanded = quote! {
impl #name {
pub fn key(&self) -> &'static str {
match self {
#(#key_arms)*
}
}
pub fn default_value(&self) -> bool {
match self {
#(#default_arms)*
}
}
pub async fn is_enabled(
&self,
client: &toggly::TogglyClient,
context: toggly::EvalContext,
) -> toggly::Result<bool> {
client.is_enabled(self.key(), context).await
}
pub async fn is_disabled(
&self,
client: &toggly::TogglyClient,
context: toggly::EvalContext,
) -> toggly::Result<bool> {
client.is_disabled(self.key(), context).await
}
}
};
Ok(expanded)
}
#[proc_macro]
pub fn feature_gate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as FeatureGateInput);
let client = &input.client;
let feature = &input.feature;
let context = &input.context;
let enabled_block = &input.enabled_block;
let disabled_block = input
.disabled_block
.as_ref()
.map(|b| {
quote! { else #b }
})
.unwrap_or_default();
let expanded = quote! {
{
let __enabled = #client.is_enabled(#feature, #context).await.unwrap_or(false);
if __enabled #enabled_block #disabled_block
}
};
expanded.into()
}
struct FeatureGateInput {
client: syn::Expr,
feature: syn::LitStr,
context: syn::Expr,
enabled_block: syn::Block,
disabled_block: Option<syn::Block>,
}
impl syn::parse::Parse for FeatureGateInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let client: syn::Expr = input.parse()?;
input.parse::<syn::Token![,]>()?;
let feature: syn::LitStr = input.parse()?;
input.parse::<syn::Token![,]>()?;
let context: syn::Expr = input.parse()?;
input.parse::<syn::Token![,]>()?;
let enabled_block: syn::Block = input.parse()?;
let disabled_block = if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
Some(input.parse()?)
} else {
None
};
Ok(FeatureGateInput {
client,
feature,
context,
enabled_block,
disabled_block,
})
}
}