concrete_type/
lib.rs

1#![doc(html_root_url = "https://docs.rs/concrete-type")]
2#![warn(missing_docs)]
3
4//! # Concrete Type
5//!
6//! A procedural macro library for mapping enum variants to concrete types.
7//!
8//! This crate provides two main derive macros:
9//!
10//! - [`Concrete`] - For enums where each variant maps to a specific concrete type
11//! - [`ConcreteConfig`] - For enums where each variant has associated configuration data
12//!   and maps to a specific concrete type
13//!
14//! These macros enable type-level programming based on runtime enum values by generating
15//! helper methods and macros that provide access to the concrete types associated with
16//! enum variants.
17//!
18//! ## Path Resolution
19//!
20//! When specifying concrete types, you can use two path formats:
21//!
22//! - `crate::path::to::Type` - Use this for types defined in the same crate as the enum.
23//!   The macro will transform this to `$crate::path::to::Type` for proper hygiene,
24//!   allowing the generated macro to work both within the defining crate and from external crates.
25//!
26//! - `other_crate::path::to::Type` - Use this for types from external crates.
27//!   The path is used as-is.
28//!
29//! ## Examples
30//!
31//! ### Basic Usage with `Concrete`
32//!
33//! ```rust,ignore
34//! use concrete_type::Concrete;
35//!
36//! #[derive(Concrete, Clone, Copy)]
37//! enum Exchange {
38//!     #[concrete = "crate::exchanges::Binance"]
39//!     Binance,
40//!     #[concrete = "crate::exchanges::Coinbase"]
41//!     Coinbase,
42//! }
43//!
44//! mod exchanges {
45//!     pub struct Binance;
46//!     pub struct Coinbase;
47//!
48//!     impl Binance {
49//!         pub fn new() -> Self { Binance }
50//!         pub fn name(&self) -> &'static str { "binance" }
51//!     }
52//!
53//!     impl Coinbase {
54//!         pub fn new() -> Self { Coinbase }
55//!         pub fn name(&self) -> &'static str { "coinbase" }
56//!     }
57//! }
58//!
59//! // Use the auto-generated exchange! macro for type-level dispatch
60//! let exchange = Exchange::Binance;
61//! let name = exchange!(exchange; ExchangeImpl => {
62//!     // ExchangeImpl is aliased to the concrete type
63//!     let instance = ExchangeImpl::new();
64//!     instance.name()
65//! });
66//! assert_eq!(name, "binance");
67//! ```
68//!
69//! ### Using `ConcreteConfig` with Configuration Data
70//!
71//! ```rust,ignore
72//! use concrete_type::ConcreteConfig;
73//!
74//! // Define concrete types and configuration types
75//! mod exchanges {
76//!     pub trait ExchangeApi {
77//!         type Config;
78//!         fn new(config: Self::Config) -> Self;
79//!         fn name(&self) -> &'static str;
80//!     }
81//!
82//!     pub struct Binance;
83//!     pub struct BinanceConfig {
84//!         pub api_key: String,
85//!     }
86//!
87//!     impl ExchangeApi for Binance {
88//!         type Config = BinanceConfig;
89//!         fn new(_: Self::Config) -> Self { Self }
90//!         fn name(&self) -> &'static str { "binance" }
91//!     }
92//! }
93//!
94//! // Define the enum with concrete type mappings and config data
95//! #[derive(ConcreteConfig)]
96//! enum ExchangeConfig {
97//!     #[concrete = "crate::exchanges::Binance"]
98//!     Binance(exchanges::BinanceConfig),
99//! }
100//!
101//! // Using the auto-generated macro with access to both type and config
102//! let config = ExchangeConfig::Binance(
103//!     exchanges::BinanceConfig { api_key: "secret".to_string() }
104//! );
105//!
106//! let name = exchange_config!(config; (Exchange, cfg) => {
107//!     // Inside this block:
108//!     // - Exchange is the concrete type
109//!     // - cfg is the configuration instance (BinanceConfig)
110//!     use exchanges::ExchangeApi;
111//!     Exchange::new(cfg).name()
112//! });
113//! ```
114//!
115//! See the crate documentation and examples for more details.
116
117extern crate proc_macro;
118
119use convert_case::{Case, Casing};
120use proc_macro::TokenStream;
121use quote::quote;
122use syn::{Attribute, DeriveInput, Expr, Fields, Lit, Meta, parse_macro_input};
123
124/// Helper function to extract concrete type path from an attribute
125fn extract_concrete_type_path(attrs: &[Attribute]) -> Option<syn::Path> {
126    for attr in attrs {
127        if attr.path().is_ident("concrete") {
128            if let Meta::NameValue(meta) = &attr.meta {
129                if let Expr::Lit(expr_lit) = &meta.value {
130                    if let Lit::Str(lit_str) = &expr_lit.lit {
131                        return syn::parse_str::<syn::Path>(&lit_str.value()).ok();
132                    }
133                }
134            }
135        }
136    }
137    None
138}
139
140/// Transforms a path for use in generated macro code.
141///
142/// If the path starts with `crate::`, it transforms to `$crate::` for proper
143/// macro hygiene. This allows the generated macro to work correctly both within
144/// the defining crate and from external crates.
145///
146/// This function also recursively transforms any `crate::` paths inside generic
147/// arguments (e.g., `Wrapper<crate::inner::Type>` becomes `Wrapper<$crate::inner::Type>`).
148///
149/// Paths that don't start with `crate::` are returned as-is (after processing their generics).
150fn transform_path_for_macro(path: &syn::Path) -> proc_macro2::TokenStream {
151    let starts_with_crate = path
152        .segments
153        .first()
154        .map(|s| s.ident == "crate")
155        .unwrap_or(false);
156
157    // Process each segment, transforming generic arguments recursively
158    let transformed_segments: Vec<proc_macro2::TokenStream> = path
159        .segments
160        .iter()
161        .enumerate()
162        .filter_map(|(i, segment)| {
163            // Skip the leading `crate` segment if present
164            if starts_with_crate && i == 0 {
165                return None;
166            }
167
168            let ident = &segment.ident;
169            let args = transform_path_arguments(&segment.arguments);
170
171            Some(quote! { #ident #args })
172        })
173        .collect();
174
175    if starts_with_crate && !transformed_segments.is_empty() {
176        quote! { $crate :: #(#transformed_segments)::* }
177    } else if transformed_segments.is_empty() {
178        // Path was just `crate` with no following segments - unusual but handle it
179        quote! { #path }
180    } else {
181        quote! { #(#transformed_segments)::* }
182    }
183}
184
185/// Transform path arguments (generic parameters), recursively handling nested `crate::` paths.
186fn transform_path_arguments(args: &syn::PathArguments) -> proc_macro2::TokenStream {
187    match args {
188        syn::PathArguments::None => quote! {},
189        syn::PathArguments::AngleBracketed(angle) => {
190            let transformed_args: Vec<proc_macro2::TokenStream> = angle
191                .args
192                .iter()
193                .map(|arg| match arg {
194                    syn::GenericArgument::Type(ty) => transform_type(ty),
195                    syn::GenericArgument::Lifetime(lt) => quote! { #lt },
196                    syn::GenericArgument::Const(expr) => quote! { #expr },
197                    other => quote! { #other },
198                })
199                .collect();
200            quote! { < #(#transformed_args),* > }
201        }
202        syn::PathArguments::Parenthesized(paren) => {
203            let inputs: Vec<_> = paren.inputs.iter().map(transform_type).collect();
204            let output = match &paren.output {
205                syn::ReturnType::Default => quote! {},
206                syn::ReturnType::Type(arrow, ty) => {
207                    let transformed = transform_type(ty);
208                    quote! { #arrow #transformed }
209                }
210            };
211            quote! { ( #(#inputs),* ) #output }
212        }
213    }
214}
215
216/// Transform a type, recursively handling `crate::` paths within.
217fn transform_type(ty: &syn::Type) -> proc_macro2::TokenStream {
218    match ty {
219        syn::Type::Path(type_path) => {
220            let transformed = transform_path_for_macro(&type_path.path);
221            if let Some(qself) = &type_path.qself {
222                let qself_ty = transform_type(&qself.ty);
223                quote! { < #qself_ty > :: #transformed }
224            } else {
225                transformed
226            }
227        }
228        syn::Type::Reference(ref_type) => {
229            let lifetime = &ref_type.lifetime;
230            let mutability = &ref_type.mutability;
231            let elem = transform_type(&ref_type.elem);
232            quote! { & #lifetime #mutability #elem }
233        }
234        syn::Type::Tuple(tuple) => {
235            let elems: Vec<_> = tuple.elems.iter().map(transform_type).collect();
236            quote! { ( #(#elems),* ) }
237        }
238        syn::Type::Slice(slice) => {
239            let elem = transform_type(&slice.elem);
240            quote! { [ #elem ] }
241        }
242        syn::Type::Array(array) => {
243            let elem = transform_type(&array.elem);
244            let len = &array.len;
245            quote! { [ #elem ; #len ] }
246        }
247        syn::Type::Ptr(ptr) => {
248            let mutability = if ptr.mutability.is_some() {
249                quote! { mut }
250            } else {
251                quote! { const }
252            };
253            let elem = transform_type(&ptr.elem);
254            quote! { * #mutability #elem }
255        }
256        // For other types, just quote them as-is
257        other => quote! { #other },
258    }
259}
260
261/// A derive macro that implements the mapping between enum variants and concrete types.
262///
263/// This macro is designed for enums where each variant maps to a specific concrete type.
264/// Each variant must be annotated with the `#[concrete = "path::to::Type"]` attribute that
265/// specifies the concrete type that the variant represents.
266///
267/// # Path Resolution
268///
269/// - Use `crate::path::to::Type` for types in the same crate (transforms to `$crate::`)
270/// - Use `other_crate::path::to::Type` for types from external crates (used as-is)
271///
272/// # Generated Code
273///
274/// The macro generates a macro with the snake_case name of the enum
275/// (e.g., `exchange!` for `Exchange`, `strategy_kind!` for `StrategyKind`) that can be used
276/// to execute code with the concrete type.
277///
278/// # Example
279///
280/// ```rust,ignore
281/// use concrete_type::Concrete;
282///
283/// #[derive(Concrete)]
284/// enum StrategyKind {
285///     #[concrete = "crate::strategies::StrategyA"]
286///     StrategyA,
287///     #[concrete = "crate::strategies::StrategyB"]
288///     StrategyB,
289/// }
290///
291/// // The generated macro is named after the enum in snake_case
292/// let strategy = StrategyKind::StrategyA;
293/// let result = strategy_kind!(strategy; T => {
294///     // T is aliased to strategies::StrategyA here
295///     std::any::type_name::<T>()
296/// });
297/// ```
298///
299/// This enables type-level programming with enums, where you can define enum variants and
300/// map them to concrete type implementations.
301#[proc_macro_derive(Concrete, attributes(concrete))]
302pub fn derive_concrete(input: TokenStream) -> TokenStream {
303    // Parse the input tokens into a syntax tree
304    let input = parse_macro_input!(input as DeriveInput);
305
306    // Extract the name of the type
307    let type_name = &input.ident;
308
309    // Create a snake_case version of the type name for the macro_rules! name
310    let type_name_str = type_name.to_string();
311    let macro_name_str = type_name_str.to_case(Case::Snake);
312    let macro_name = syn::Ident::new(&macro_name_str, type_name.span());
313
314    // Handle enum case
315    let data_enum = match &input.data {
316        syn::Data::Enum(data_enum) => data_enum,
317        _ => {
318            return syn::Error::new_spanned(
319                type_name,
320                "Concrete can only be derived for enums or structs with type parameters",
321            )
322            .to_compile_error()
323            .into();
324        }
325    };
326
327    // Extract variant names and their concrete types
328    let mut variant_mappings = Vec::new();
329
330    for variant in &data_enum.variants {
331        let variant_name = &variant.ident;
332
333        // Extract the concrete type path from the variant's attributes
334        if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
335            variant_mappings.push((variant_name, concrete_type));
336        } else {
337            // Variant is missing the #[concrete = "..."] attribute
338            return syn::Error::new_spanned(
339                variant_name,
340                format!(
341                    "Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
342                    variant_name
343                ),
344            )
345            .to_compile_error()
346            .into();
347        }
348    }
349
350    // Generate match arms for the macro_rules! version
351    let macro_match_arms = variant_mappings
352        .iter()
353        .map(|(variant_name, concrete_type)| {
354            let transformed_path = transform_path_for_macro(concrete_type);
355            quote! {
356                #type_name::#variant_name => {
357                    type $type_param = #transformed_path;
358                    $code_block
359                }
360            }
361        });
362
363    // Generate a top-level macro with the snake_case name of the enum
364    let macro_def = quote! {
365        #[macro_export]
366        macro_rules! #macro_name {
367            ($enum_instance:expr; $type_param:ident => $code_block:block) => {
368                match $enum_instance {
369                    #(#macro_match_arms),*
370                }
371            };
372        }
373    };
374
375    // Combine the macro definition and methods implementation
376    let expanded = quote! {
377        // Define the macro outside any module to make it directly accessible
378        #macro_def
379    };
380
381    // Return the generated implementation
382    TokenStream::from(expanded)
383}
384
385/// A derive macro that implements the mapping between enum variants with associated data and
386/// concrete types.
387///
388/// This macro is designed for enums where each variant has associated configuration data and maps
389/// to a specific concrete type. Each variant must be annotated with the
390/// `#[concrete = "path::to::Type"]` attribute and contain a single field (no tuples)
391/// that holds the configuration data for that concrete type. If the variant has no data, then it
392/// defaults to the unit type `()`.
393///
394/// # Path Resolution
395///
396/// - Use `crate::path::to::Type` for types in the same crate (transforms to `$crate::`)
397/// - Use `other_crate::path::to::Type` for types from external crates (used as-is)
398///
399/// # Generated Code
400///
401/// The macro generates:
402/// 1. A `config` method that returns a reference to the configuration data.
403/// 2. A macro with the snake_case name of the enum + "_config" (with "Config" suffix removed if present)
404///    that allows access to both the concrete type and configuration data
405///
406/// # Example
407///
408/// ```rust,ignore
409/// use concrete_type::ConcreteConfig;
410///
411/// // Define concrete types and configuration types
412/// #[derive(Debug)]
413/// struct BinanceConfig {
414///     api_key: String,
415/// }
416///
417/// struct Binance;
418///
419/// struct Okx;
420///
421/// #[derive(ConcreteConfig)]
422/// enum ExchangeConfig {
423///     #[concrete = "Binance"]
424///     Binance(BinanceConfig),
425///     #[concrete = "Okx"]
426///     Okx,
427/// }
428///
429/// // Using the generated macro for a variant with config data
430/// let config = ExchangeConfig::Binance(BinanceConfig { api_key: "key".to_string() });
431/// let result = exchange_config!(config; (Exchange, cfg) => {
432///     // "Exchange" symbol is concrete type Binance
433///     // "cfg" symbol is a reference to the BinanceConfig instance
434///     format!("{} with config: {:?}", std::any::type_name::<Exchange>(), cfg)
435/// });
436///
437/// // Using the generated macro for a variant without config data
438/// let config = ExchangeConfig::Okx;
439/// let result = exchange_config!(config; (Exchange, cfg) => {
440///     // "Exchange" symbol is concrete type Okx
441///     // "cfg" symbol is a reference to the unit type () (since the Okx variant doesn't have config)
442///     format!("{} with config: {:?}", std::any::type_name::<Exchange>(), cfg)
443/// });
444/// ```
445#[proc_macro_derive(ConcreteConfig, attributes(concrete))]
446pub fn derive_concrete_config(input: TokenStream) -> TokenStream {
447    // Parse the input tokens into a syntax tree
448    let input = parse_macro_input!(input as DeriveInput);
449
450    // Extract the name of the type
451    let type_name = &input.ident;
452
453    // Create a snake_case version of the type name for the macro_rules! name
454    let type_name_str = type_name.to_string();
455    // Strip "Config" suffix if present for cleaner macro names
456    let base_name = if type_name_str.ends_with("Config") {
457        &type_name_str[0..type_name_str.len() - 6]
458    } else {
459        &type_name_str
460    };
461    let macro_name_str = format!("{}_config", base_name.to_case(Case::Snake));
462    let macro_name = syn::Ident::new(&macro_name_str, type_name.span());
463
464    // Ensure we're dealing with an enum
465    let data_enum = match &input.data {
466        syn::Data::Enum(data_enum) => data_enum,
467        _ => {
468            return syn::Error::new_spanned(
469                type_name,
470                "ConcreteConfig can only be derived for enums with data",
471            )
472            .to_compile_error()
473            .into();
474        }
475    };
476
477    // Extract variant names, their concrete types, and field types
478    // We now include a boolean flag to indicate if the variant has config data
479    let mut variant_mappings = Vec::new();
480
481    for variant in &data_enum.variants {
482        let variant_name = &variant.ident;
483
484        // Extract the concrete type path from the variant's attributes
485        if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
486            // Check variant field type - now accepting both unit variants and single-field variants
487            match &variant.fields {
488                Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
489                    // Variant with config data
490                    variant_mappings.push((variant_name, concrete_type, true));
491                }
492                Fields::Unit => {
493                    // Unit variant (no config data)
494                    variant_mappings.push((variant_name, concrete_type, false));
495                }
496                _ => {
497                    return syn::Error::new_spanned(
498                        variant_name,
499                        format!(
500                            "Enum variant `{}` must either be a unit variant or have exactly one unnamed field for config",
501                            variant_name
502                        ),
503                    )
504                        .to_compile_error()
505                        .into();
506                }
507            }
508        } else {
509            // Variant is missing the #[concrete = "..."] attribute
510            return syn::Error::new_spanned(
511                variant_name,
512                format!(
513                    "Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
514                    variant_name
515                ),
516            )
517            .to_compile_error()
518            .into();
519        }
520    }
521
522    // Generate match arms for the config method
523    let config_arms = variant_mappings
524        .iter()
525        .map(|(variant_name, _concrete_type, has_config)| {
526            if *has_config {
527                quote! {
528                    #type_name::#variant_name(config) => config
529                }
530            } else {
531                quote! {
532                    #type_name::#variant_name => &() // Return unit type for variants w/o config
533                }
534            }
535        });
536
537    // Generate match arms for the macro_rules! version
538    let macro_match_arms =
539        variant_mappings
540            .iter()
541            .map(|(variant_name, concrete_type, has_config)| {
542                let transformed_path = transform_path_for_macro(concrete_type);
543                if *has_config {
544                    quote! {
545                        #type_name::#variant_name(config) => {
546                            type $type_param = #transformed_path;
547                            let $config_param = config;
548                            $code_block
549                        }
550                    }
551                } else {
552                    quote! {
553                        #type_name::#variant_name => {
554                            type $type_param = #transformed_path;
555                            let $config_param = (); // Use unit type
556                            $code_block
557                        }
558                    }
559                }
560            });
561
562    // Generate a top-level macro with the snake_case name of the enum + "_config"
563    let macro_def = quote! {
564        #[macro_export]
565        macro_rules! #macro_name {
566            ($enum_instance:expr; ($type_param:ident, $config_param:ident) => $code_block:block) => {
567                match $enum_instance {
568                    #(#macro_match_arms),*
569                }
570            };
571        }
572    };
573
574    // Generate the methods implementation
575    let methods_impl = quote! {
576        impl #type_name {
577            /// Returns a reference to the configuration data associated with this enum variant
578            /// Unit variants return a reference to the unit type `()`
579            pub fn config(&self) -> &dyn std::any::Any {
580                match self {
581                    #(#config_arms),*
582                }
583            }
584        }
585    };
586
587    // Combine the macro definition and methods implementation
588    let expanded = quote! {
589        // Define the macro
590        #macro_def
591
592        // Implement methods on the enum
593        #methods_impl
594    };
595
596    TokenStream::from(expanded)
597}