1use darling::FromMeta;
27use proc_macro::TokenStream;
28use proc_macro2::TokenStream as TokenStream2;
29use quote::quote;
30use syn::{parse_macro_input, ItemFn, ReturnType};
31
32#[derive(Debug, FromMeta)]
34struct FeatureFlagArgs {
35 feature: String,
37
38 #[darling(default)]
40 client: Option<String>,
41
42 #[darling(default)]
44 context: Option<String>,
45
46 #[darling(default)]
48 fallback: Option<String>,
49
50 #[darling(default)]
52 negate: bool,
53}
54
55#[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 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 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 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 match &fn_sig.output {
135 ReturnType::Default => quote!(return;),
136 ReturnType::Type(_, _) => quote!(return Default::default();),
137 }
138 };
139
140 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#[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 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 pub fn key(&self) -> &'static str {
250 match self {
251 #(#key_arms)*
252 }
253 }
254
255 pub fn default_value(&self) -> bool {
257 match self {
258 #(#default_arms)*
259 }
260 }
261
262 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 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#[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}