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(¯o_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(¯o_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}