Skip to main content

toggly_macros/
lib.rs

1//! # Toggly Macros
2//!
3//! Procedural macros for Toggly feature flags.
4//!
5//! ## Feature Guard Macro
6//!
7//! The `#[feature_flag]` attribute macro guards a function with a feature flag check.
8//!
9//! ```rust,ignore
10//! use toggly_macros::feature_flag;
11//!
12//! #[feature_flag("my-feature")]
13//! async fn my_feature_function() -> String {
14//!     "Feature is enabled!".to_string()
15//! }
16//! ```
17//!
18//! ## Options
19//!
20//! - `feature` - The feature key (required)
21//! - `client` - Expression to get the client (default: from context)
22//! - `context` - Expression to get the evaluation context (default: `Default::default()`)
23//! - `fallback` - Return value when feature is disabled
24//! - `negate` - Invert the feature check
25
26use darling::FromMeta;
27use proc_macro::TokenStream;
28use proc_macro2::TokenStream as TokenStream2;
29use quote::quote;
30use syn::{parse_macro_input, ItemFn, ReturnType};
31
32/// Arguments for the feature_flag macro.
33#[derive(Debug, FromMeta)]
34struct FeatureFlagArgs {
35    /// Feature key to check.
36    feature: String,
37
38    /// Expression to get the Toggly client.
39    #[darling(default)]
40    client: Option<String>,
41
42    /// Expression to get the evaluation context.
43    #[darling(default)]
44    context: Option<String>,
45
46    /// Fallback value when feature is disabled.
47    #[darling(default)]
48    fallback: Option<String>,
49
50    /// Whether to negate the feature check.
51    #[darling(default)]
52    negate: bool,
53}
54
55/// Guard a function with a feature flag check.
56///
57/// # Example
58///
59/// ```rust,ignore
60/// use toggly_macros::feature_flag;
61///
62/// #[feature_flag(feature = "my-feature")]
63/// async fn my_function() -> Result<String, Error> {
64///     Ok("Feature enabled!".to_string())
65/// }
66///
67/// // With custom client and context
68/// #[feature_flag(
69///     feature = "premium-feature",
70///     client = "get_toggly_client()",
71///     context = "EvalContext::with_identity(user_id)"
72/// )]
73/// async fn premium_function(user_id: &str) -> String {
74///     "Premium content".to_string()
75/// }
76///
77/// // With fallback value
78/// #[feature_flag(feature = "new-ui", fallback = "legacy_ui()")]
79/// async fn render_ui() -> Html {
80///     render_new_ui()
81/// }
82/// ```
83#[proc_macro_attribute]
84pub fn feature_flag(args: TokenStream, input: TokenStream) -> TokenStream {
85    let attr_args = match darling::ast::NestedMeta::parse_meta_list(args.into()) {
86        Ok(v) => v,
87        Err(e) => return TokenStream::from(darling::Error::from(e).write_errors()),
88    };
89    let input_fn = parse_macro_input!(input as ItemFn);
90
91    let args = match FeatureFlagArgs::from_list(&attr_args) {
92        Ok(args) => args,
93        Err(e) => return TokenStream::from(e.write_errors()),
94    };
95
96    expand_feature_flag(args, input_fn)
97        .unwrap_or_else(|e| e.to_compile_error())
98        .into()
99}
100
101fn expand_feature_flag(args: FeatureFlagArgs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
102    let feature_key = &args.feature;
103    let negate = args.negate;
104
105    let fn_vis = &input_fn.vis;
106    let fn_sig = &input_fn.sig;
107    let fn_block = &input_fn.block;
108    let fn_attrs = &input_fn.attrs;
109
110    // Parse client expression or use default
111    let client_expr: TokenStream2 = args
112        .client
113        .as_deref()
114        .unwrap_or("toggly_client")
115        .parse()
116        .unwrap_or_else(|_| quote!(toggly_client));
117
118    // Parse context expression or use default
119    let context_expr: TokenStream2 = args
120        .context
121        .as_deref()
122        .unwrap_or("toggly::EvalContext::default()")
123        .parse()
124        .unwrap_or_else(|_| quote!(toggly::EvalContext::default()));
125
126    // Generate fallback handling
127    let fallback_expr = if let Some(fallback) = &args.fallback {
128        let fallback_tokens: TokenStream2 = fallback
129            .parse()
130            .unwrap_or_else(|_| quote!(Default::default()));
131        quote!(return #fallback_tokens;)
132    } else {
133        // If no fallback, return default for the return type
134        match &fn_sig.output {
135            ReturnType::Default => quote!(return;),
136            ReturnType::Type(_, _) => quote!(return Default::default();),
137        }
138    };
139
140    // Generate the check condition
141    let check_condition = if negate {
142        quote!(!enabled)
143    } else {
144        quote!(enabled)
145    };
146
147    let expanded = quote! {
148        #(#fn_attrs)*
149        #fn_vis #fn_sig {
150            let __toggly_client = &#client_expr;
151            let __toggly_context = #context_expr;
152
153            let enabled = __toggly_client
154                .is_enabled(#feature_key, __toggly_context)
155                .await
156                .unwrap_or(false);
157
158            if !#check_condition {
159                #fallback_expr
160            }
161
162            #fn_block
163        }
164    };
165
166    Ok(expanded)
167}
168
169/// Derive macro for creating feature flag enums.
170///
171/// # Example
172///
173/// ```rust,ignore
174/// use toggly_macros::FeatureFlags;
175///
176/// #[derive(FeatureFlags)]
177/// pub enum Features {
178///     #[feature(key = "dark-mode")]
179///     DarkMode,
180///
181///     #[feature(key = "new-dashboard", default = true)]
182///     NewDashboard,
183///
184///     #[feature(key = "beta-features")]
185///     BetaFeatures,
186/// }
187///
188/// // Usage:
189/// let enabled = Features::DarkMode.is_enabled(&client, context).await?;
190/// ```
191#[proc_macro_derive(FeatureFlags, attributes(feature))]
192pub fn derive_feature_flags(input: TokenStream) -> TokenStream {
193    let input = parse_macro_input!(input as syn::DeriveInput);
194
195    expand_feature_flags(input)
196        .unwrap_or_else(|e| e.to_compile_error())
197        .into()
198}
199
200fn expand_feature_flags(input: syn::DeriveInput) -> syn::Result<TokenStream2> {
201    let name = &input.ident;
202
203    let variants = match &input.data {
204        syn::Data::Enum(data) => &data.variants,
205        _ => {
206            return Err(syn::Error::new_spanned(
207                input,
208                "FeatureFlags can only be derived for enums",
209            ))
210        }
211    };
212
213    let mut key_arms = Vec::new();
214    let mut default_arms = Vec::new();
215
216    for variant in variants {
217        let variant_name = &variant.ident;
218        let mut feature_key = variant_name.to_string();
219        let mut default_value = false;
220
221        // Parse #[feature(...)] attributes
222        for attr in &variant.attrs {
223            if attr.path().is_ident("feature") {
224                attr.parse_nested_meta(|meta| {
225                    if meta.path.is_ident("key") {
226                        let value: syn::LitStr = meta.value()?.parse()?;
227                        feature_key = value.value();
228                    } else if meta.path.is_ident("default") {
229                        let value: syn::LitBool = meta.value()?.parse()?;
230                        default_value = value.value();
231                    }
232                    Ok(())
233                })?;
234            }
235        }
236
237        key_arms.push(quote! {
238            #name::#variant_name => #feature_key,
239        });
240
241        default_arms.push(quote! {
242            #name::#variant_name => #default_value,
243        });
244    }
245
246    let expanded = quote! {
247        impl #name {
248            /// Get the feature key for this variant.
249            pub fn key(&self) -> &'static str {
250                match self {
251                    #(#key_arms)*
252                }
253            }
254
255            /// Get the default value for this feature.
256            pub fn default_value(&self) -> bool {
257                match self {
258                    #(#default_arms)*
259                }
260            }
261
262            /// Check if this feature is enabled.
263            pub async fn is_enabled(
264                &self,
265                client: &toggly::TogglyClient,
266                context: toggly::EvalContext,
267            ) -> toggly::Result<bool> {
268                client.is_enabled(self.key(), context).await
269            }
270
271            /// Check if this feature is disabled.
272            pub async fn is_disabled(
273                &self,
274                client: &toggly::TogglyClient,
275                context: toggly::EvalContext,
276            ) -> toggly::Result<bool> {
277                client.is_disabled(self.key(), context).await
278            }
279        }
280    };
281
282    Ok(expanded)
283}
284
285/// Macro for conditional compilation based on feature flags at runtime.
286///
287/// This macro provides a more ergonomic way to conditionally execute code.
288///
289/// # Example
290///
291/// ```rust,ignore
292/// use toggly_macros::feature_gate;
293///
294/// feature_gate!(client, "my-feature", context, {
295///     // Code to run when feature is enabled
296///     do_something();
297/// }, {
298///     // Code to run when feature is disabled (optional)
299///     do_fallback();
300/// });
301/// ```
302#[proc_macro]
303pub fn feature_gate(input: TokenStream) -> TokenStream {
304    let input = parse_macro_input!(input as FeatureGateInput);
305
306    let client = &input.client;
307    let feature = &input.feature;
308    let context = &input.context;
309    let enabled_block = &input.enabled_block;
310
311    let disabled_block = input
312        .disabled_block
313        .as_ref()
314        .map(|b| {
315            quote! { else #b }
316        })
317        .unwrap_or_default();
318
319    let expanded = quote! {
320        {
321            let __enabled = #client.is_enabled(#feature, #context).await.unwrap_or(false);
322            if __enabled #enabled_block #disabled_block
323        }
324    };
325
326    expanded.into()
327}
328
329struct FeatureGateInput {
330    client: syn::Expr,
331    feature: syn::LitStr,
332    context: syn::Expr,
333    enabled_block: syn::Block,
334    disabled_block: Option<syn::Block>,
335}
336
337impl syn::parse::Parse for FeatureGateInput {
338    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
339        let client: syn::Expr = input.parse()?;
340        input.parse::<syn::Token![,]>()?;
341        let feature: syn::LitStr = input.parse()?;
342        input.parse::<syn::Token![,]>()?;
343        let context: syn::Expr = input.parse()?;
344        input.parse::<syn::Token![,]>()?;
345        let enabled_block: syn::Block = input.parse()?;
346
347        let disabled_block = if input.peek(syn::Token![,]) {
348            input.parse::<syn::Token![,]>()?;
349            Some(input.parse()?)
350        } else {
351            None
352        };
353
354        Ok(FeatureGateInput {
355            client,
356            feature,
357            context,
358            enabled_block,
359            disabled_block,
360        })
361    }
362}