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//! ## Examples
19//!
20//! ### Basic Usage with `Concrete`
21//!
22//! ```rust,ignore
23//! use concrete_type::Concrete;
24//!
25//! #[derive(Concrete, Clone, Copy)]
26//! enum Exchange {
27//! #[concrete = "exchanges::Binance"]
28//! Binance,
29//! #[concrete = "exchanges::Coinbase"]
30//! Coinbase,
31//! }
32//!
33//! mod exchanges {
34//! pub struct Binance;
35//! pub struct Coinbase;
36//!
37//! impl Binance {
38//! pub fn new() -> Self { Binance }
39//! pub fn name(&self) -> &'static str { "binance" }
40//! }
41//!
42//! impl Coinbase {
43//! pub fn new() -> Self { Coinbase }
44//! pub fn name(&self) -> &'static str { "coinbase" }
45//! }
46//! }
47//!
48//! // Use the auto-generated exchange! macro for type-level dispatch
49//! let exchange = Exchange::Binance;
50//! let name = exchange!(exchange; ExchangeImpl => {
51//! // ExchangeImpl is aliased to the concrete type (exchanges::Binance)
52//! let instance = ExchangeImpl::new();
53//! instance.name()
54//! });
55//! assert_eq!(name, "binance");
56//! ```
57//!
58//! ### Using `ConcreteConfig` with Configuration Data
59//!
60//! ```rust,ignore
61//! use concrete_type::ConcreteConfig;
62//!
63//! // Define concrete types and configuration types
64//! mod exchanges {
65//! pub trait ExchangeApi {
66//! type Config;
67//! fn new(config: Self::Config) -> Self;
68//! fn name(&self) -> &'static str;
69//! }
70//!
71//! pub struct Binance;
72//! pub struct BinanceConfig {
73//! pub api_key: String,
74//! }
75//!
76//! impl ExchangeApi for Binance {
77//! type Config = BinanceConfig;
78//! fn new(_: Self::Config) -> Self { Self }
79//! fn name(&self) -> &'static str { "binance" }
80//! }
81//! }
82//!
83//! // Define the enum with concrete type mappings and config data
84//! #[derive(ConcreteConfig)]
85//! enum ExchangeConfig {
86//! #[concrete = "exchanges::Binance"]
87//! Binance(exchanges::BinanceConfig),
88//! }
89//!
90//! // Using the auto-generated macro with access to both type and config
91//! let config = ExchangeConfig::Binance(
92//! exchanges::BinanceConfig { api_key: "secret".to_string() }
93//! );
94//!
95//! let name = exchange_config!(config; (Exchange, cfg) => {
96//! // Inside this block:
97//! // - Exchange is the concrete type (exchanges::Binance)
98//! // - cfg is the configuration instance (BinanceConfig)
99//! use exchanges::ExchangeApi;
100//! Exchange::new(cfg).name()
101//! });
102//! ```
103//!
104//! See the crate documentation and examples for more details.
105
106extern crate proc_macro;
107
108use convert_case::{Case, Casing};
109use proc_macro::TokenStream;
110use quote::quote;
111use syn::{Attribute, DeriveInput, Expr, Fields, Lit, Meta, parse_macro_input};
112
113/// Helper function to extract concrete type path from an attribute
114fn extract_concrete_type_path(attrs: &[Attribute]) -> Option<syn::Path> {
115 for attr in attrs {
116 if attr.path().is_ident("concrete") {
117 if let Meta::NameValue(meta) = &attr.meta {
118 if let Expr::Lit(expr_lit) = &meta.value {
119 if let Lit::Str(lit_str) = &expr_lit.lit {
120 return syn::parse_str::<syn::Path>(&lit_str.value()).ok();
121 }
122 }
123 }
124 }
125 }
126 None
127}
128
129/// A derive macro that implements the mapping between enum variants and concrete types.
130///
131/// This macro is designed for enums where each variant maps to a specific concrete type.
132/// Each variant must be annotated with the `#[concrete = "path::to::Type"]` attribute that
133/// specifies the concrete type that the variant represents.
134///
135/// # Generated Code
136///
137/// The macro generates a macro with the snake_case name of the enum
138/// (e.g., `exchange!` for `Exchange`, `strategy_kind!` for `StrategyKind`) that can be used
139/// to execute code with the concrete type.
140///
141/// # Example
142///
143/// ```rust,ignore
144/// use concrete_type::Concrete;
145///
146/// #[derive(Concrete)]
147/// enum StrategyKind {
148/// #[concrete = "strategies::StrategyA"]
149/// StrategyA,
150/// #[concrete = "strategies::StrategyB"]
151/// StrategyB,
152/// }
153///
154/// // The generated macro is named after the enum in snake_case
155/// let strategy = StrategyKind::StrategyA;
156/// let result = strategy_kind!(strategy; T => {
157/// // T is aliased to strategies::StrategyA here
158/// std::any::type_name::<T>()
159/// });
160/// ```
161///
162/// This enables type-level programming with enums, where you can define enum variants and
163/// map them to concrete type implementations.
164#[proc_macro_derive(Concrete, attributes(concrete))]
165pub fn derive_concrete(input: TokenStream) -> TokenStream {
166 // Parse the input tokens into a syntax tree
167 let input = parse_macro_input!(input as DeriveInput);
168
169 // Extract the name of the type
170 let type_name = &input.ident;
171
172 // Create a snake_case version of the type name for the macro_rules! name
173 let type_name_str = type_name.to_string();
174 let macro_name_str = type_name_str.to_case(Case::Snake);
175 let macro_name = syn::Ident::new(¯o_name_str, type_name.span());
176
177 // Handle enum case
178 let data_enum = match &input.data {
179 syn::Data::Enum(data_enum) => data_enum,
180 _ => {
181 return syn::Error::new_spanned(
182 type_name,
183 "Concrete can only be derived for enums or structs with type parameters",
184 )
185 .to_compile_error()
186 .into();
187 }
188 };
189
190 // Extract variant names and their concrete types
191 let mut variant_mappings = Vec::new();
192
193 for variant in &data_enum.variants {
194 let variant_name = &variant.ident;
195
196 // Extract the concrete type path from the variant's attributes
197 if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
198 variant_mappings.push((variant_name, concrete_type));
199 } else {
200 // Variant is missing the #[concrete = "..."] attribute
201 return syn::Error::new_spanned(
202 variant_name,
203 format!(
204 "Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
205 variant_name
206 ),
207 )
208 .to_compile_error()
209 .into();
210 }
211 }
212
213 // Generate match arms for the macro_rules! version
214 let macro_match_arms = variant_mappings
215 .iter()
216 .map(|(variant_name, concrete_type)| {
217 quote! {
218 #type_name::#variant_name => {
219 type $type_param = #concrete_type;
220 $code_block
221 }
222 }
223 });
224
225 // Generate a top-level macro with the snake_case name of the enum
226 let macro_def = quote! {
227 #[macro_export]
228 macro_rules! #macro_name {
229 ($enum_instance:expr; $type_param:ident => $code_block:block) => {
230 match $enum_instance {
231 #(#macro_match_arms),*
232 }
233 };
234 }
235 };
236
237 // Combine the macro definition and methods implementation
238 let expanded = quote! {
239 // Define the macro outside any module to make it directly accessible
240 #macro_def
241 };
242
243 // Return the generated implementation
244 TokenStream::from(expanded)
245}
246
247/// A derive macro that implements the mapping between enum variants with associated data and
248/// concrete types.
249///
250/// This macro is designed for enums where each variant has associated configuration data and maps
251/// to a specific concrete type. Each variant must be annotated with the
252/// `#[concrete = "path::to::Type"]` attribute and contain a single field (no tuples)
253/// that holds the configuration data for that concrete type. If the variant has no data, then it
254/// defaults to the unit type `()`.
255///
256/// # Generated Code
257///
258/// The macro generates:
259/// 1. A `config` method that returns a reference to the configuration data.
260/// 2. A macro with the snake_case name of the enum + "_config" (with "Config" suffix removed if present)
261/// that allows access to both the concrete type and configuration data
262///
263/// # Example
264///
265/// ```rust,ignore
266/// use concrete_type::ConcreteConfig;
267///
268/// // Define concrete types and configuration types
269/// #[derive(Debug)]
270/// struct BinanceConfig {
271/// api_key: String,
272/// }
273///
274/// struct Binance;
275///
276/// struct Okx;
277///
278/// #[derive(ConcreteConfig)]
279/// enum ExchangeConfig {
280/// #[concrete = "Binance"]
281/// Binance(BinanceConfig),
282/// #[concrete = "Okx"]
283/// Okx,
284/// }
285///
286/// // Using the generated macro for a variant with config data
287/// let config = ExchangeConfig::Binance(BinanceConfig { api_key: "key".to_string() });
288/// let result = exchange_config!(config; (Exchange, cfg) => {
289/// // "Exchange" symbol is concrete type Binance
290/// // "cfg" symbol is a reference to the BinanceConfig instance
291/// format!("{} with config: {:?}", std::any::type_name::<Exchange>(), cfg)
292/// });
293///
294/// // Using the generated macro for a variant without config data
295/// let config = ExchangeConfig::Okx;
296/// let result = exchange_config!(config; (Exchange, cfg) => {
297/// // "Exchange" symbol is concrete type Okx
298/// // "cfg" symbol is a reference to the unit type () (since the Okx variant doesn't have config)
299/// format!("{} with config: {:?}", std::any::type_name::<Exchange>(), cfg)
300/// });
301/// ```
302#[proc_macro_derive(ConcreteConfig, attributes(concrete))]
303pub fn derive_concrete_config(input: TokenStream) -> TokenStream {
304 // Parse the input tokens into a syntax tree
305 let input = parse_macro_input!(input as DeriveInput);
306
307 // Extract the name of the type
308 let type_name = &input.ident;
309
310 // Create a snake_case version of the type name for the macro_rules! name
311 let type_name_str = type_name.to_string();
312 // Strip "Config" suffix if present for cleaner macro names
313 let base_name = if type_name_str.ends_with("Config") {
314 &type_name_str[0..type_name_str.len() - 6]
315 } else {
316 &type_name_str
317 };
318 let macro_name_str = format!("{}_config", base_name.to_case(Case::Snake));
319 let macro_name = syn::Ident::new(¯o_name_str, type_name.span());
320
321 // Ensure we're dealing with an enum
322 let data_enum = match &input.data {
323 syn::Data::Enum(data_enum) => data_enum,
324 _ => {
325 return syn::Error::new_spanned(
326 type_name,
327 "ConcreteConfig can only be derived for enums with data",
328 )
329 .to_compile_error()
330 .into();
331 }
332 };
333
334 // Extract variant names, their concrete types, and field types
335 // We now include a boolean flag to indicate if the variant has config data
336 let mut variant_mappings = Vec::new();
337
338 for variant in &data_enum.variants {
339 let variant_name = &variant.ident;
340
341 // Extract the concrete type path from the variant's attributes
342 if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
343 // Check variant field type - now accepting both unit variants and single-field variants
344 match &variant.fields {
345 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
346 // Variant with config data
347 variant_mappings.push((variant_name, concrete_type, true));
348 }
349 Fields::Unit => {
350 // Unit variant (no config data)
351 variant_mappings.push((variant_name, concrete_type, false));
352 }
353 _ => {
354 return syn::Error::new_spanned(
355 variant_name,
356 format!(
357 "Enum variant `{}` must either be a unit variant or have exactly one unnamed field for config",
358 variant_name
359 ),
360 )
361 .to_compile_error()
362 .into();
363 }
364 }
365 } else {
366 // Variant is missing the #[concrete = "..."] attribute
367 return syn::Error::new_spanned(
368 variant_name,
369 format!(
370 "Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
371 variant_name
372 ),
373 )
374 .to_compile_error()
375 .into();
376 }
377 }
378
379 // Generate match arms for the config method
380 let config_arms = variant_mappings
381 .iter()
382 .map(|(variant_name, _concrete_type, has_config)| {
383 if *has_config {
384 quote! {
385 #type_name::#variant_name(config) => config
386 }
387 } else {
388 quote! {
389 #type_name::#variant_name => &() // Return unit type for variants w/o config
390 }
391 }
392 });
393
394 // Generate match arms for the macro_rules! version
395 let macro_match_arms =
396 variant_mappings
397 .iter()
398 .map(|(variant_name, concrete_type, has_config)| {
399 if *has_config {
400 quote! {
401 #type_name::#variant_name(config) => {
402 type $type_param = #concrete_type;
403 let $config_param = config;
404 $code_block
405 }
406 }
407 } else {
408 quote! {
409 #type_name::#variant_name => {
410 type $type_param = #concrete_type;
411 let $config_param = (); // Use unit type
412 $code_block
413 }
414 }
415 }
416 });
417
418 // Generate a top-level macro with the snake_case name of the enum + "_config"
419 let macro_def = quote! {
420 #[macro_export]
421 macro_rules! #macro_name {
422 ($enum_instance:expr; ($type_param:ident, $config_param:ident) => $code_block:block) => {
423 match $enum_instance {
424 #(#macro_match_arms),*
425 }
426 };
427 }
428 };
429
430 // Generate the methods implementation
431 let methods_impl = quote! {
432 impl #type_name {
433 /// Returns a reference to the configuration data associated with this enum variant
434 /// Unit variants return a reference to the unit type `()`
435 pub fn config(&self) -> &dyn std::any::Any {
436 match self {
437 #(#config_arms),*
438 }
439 }
440 }
441 };
442
443 // Combine the macro definition and methods implementation
444 let expanded = quote! {
445 // Define the macro
446 #macro_def
447
448 // Implement methods on the enum
449 #methods_impl
450 };
451
452 TokenStream::from(expanded)
453}