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 tuple field
253/// that holds the configuration data for that concrete type.
254///
255/// # Generated Code
256///
257/// The macro generates:
258/// 1. A `config` method that returns a reference to the configuration data
259/// 2. A macro with the snake_case name of the enum + "_config" (with "Config" suffix removed if present)
260/// that allows access to both the concrete type and configuration data
261///
262/// # Example
263///
264/// ```rust,ignore
265/// use concrete_type::ConcreteConfig;
266///
267/// // Define concrete types and configuration types
268/// struct BinanceConfig {
269/// api_key: String,
270/// }
271///
272/// struct Binance;
273///
274/// #[derive(ConcreteConfig)]
275/// enum ExchangeConfig {
276/// #[concrete = "Binance"]
277/// Binance(BinanceConfig),
278/// }
279///
280/// // Using the generated macro
281/// let config = ExchangeConfig::Binance(BinanceConfig { api_key: "key".to_string() });
282/// let result = exchange_config!(config; (Exchange, cfg) => {
283/// // Exchange is aliased to Binance
284/// // cfg is a reference to BinanceConfig
285/// format!("{} with key: {}", std::any::type_name::<Exchange>(), cfg.api_key)
286/// });
287/// ```
288#[proc_macro_derive(ConcreteConfig, attributes(concrete))]
289pub fn derive_concrete_config(input: TokenStream) -> TokenStream {
290 // Parse the input tokens into a syntax tree
291 let input = parse_macro_input!(input as DeriveInput);
292
293 // Extract the name of the type
294 let type_name = &input.ident;
295
296 // Create a snake_case version of the type name for the macro_rules! name
297 let type_name_str = type_name.to_string();
298 // Strip "Config" suffix if present for cleaner macro names
299 let base_name = if type_name_str.ends_with("Config") {
300 &type_name_str[0..type_name_str.len() - 6]
301 } else {
302 &type_name_str
303 };
304 let macro_name_str = format!("{}_config", base_name.to_case(Case::Snake));
305 let macro_name = syn::Ident::new(¯o_name_str, type_name.span());
306
307 // Ensure we're dealing with an enum
308 let data_enum = match &input.data {
309 syn::Data::Enum(data_enum) => data_enum,
310 _ => {
311 return syn::Error::new_spanned(
312 type_name,
313 "ConcreteConfig can only be derived for enums with data",
314 )
315 .to_compile_error()
316 .into();
317 }
318 };
319
320 // Extract variant names, their concrete types, and field types
321 let mut variant_mappings = Vec::new();
322
323 for variant in &data_enum.variants {
324 let variant_name = &variant.ident;
325
326 // Extract the concrete type path from the variant's attributes
327 if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
328 // Verify the variant has a tuple field
329 match &variant.fields {
330 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
331 variant_mappings.push((variant_name, concrete_type));
332 }
333 _ => {
334 return syn::Error::new_spanned(
335 variant_name,
336 format!(
337 "Enum variant `{}` must have exactly one unnamed field for the config",
338 variant_name
339 ),
340 )
341 .to_compile_error()
342 .into();
343 }
344 }
345 } else {
346 // Variant is missing the #[concrete = "..."] attribute
347 return syn::Error::new_spanned(
348 variant_name,
349 format!(
350 "Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
351 variant_name
352 ),
353 )
354 .to_compile_error()
355 .into();
356 }
357 }
358
359 // Generate match arms for the config method
360 let config_arms = variant_mappings
361 .iter()
362 .map(|(variant_name, _concrete_type)| {
363 quote! {
364 #type_name::#variant_name(config) => config
365 }
366 });
367
368 // Generate match arms for the macro_rules! version
369 let macro_match_arms = variant_mappings
370 .iter()
371 .map(|(variant_name, concrete_type)| {
372 quote! {
373 #type_name::#variant_name(config) => {
374 type $type_param = #concrete_type;
375 let $config_param = config;
376 $code_block
377 }
378 }
379 });
380
381 // Generate a top-level macro with the snake_case name of the enum + "_config"
382 let macro_def = quote! {
383 #[macro_export]
384 macro_rules! #macro_name {
385 ($enum_instance:expr; ($type_param:ident, $config_param:ident) => $code_block:block) => {
386 match $enum_instance {
387 #(#macro_match_arms),*
388 }
389 };
390 }
391 };
392
393 // Generate the methods implementation
394 let methods_impl = quote! {
395 impl #type_name {
396 /// Returns a reference to the configuration data associated with this enum variant
397 pub fn config(&self) -> &dyn std::any::Any {
398 match self {
399 #(#config_arms),*
400 }
401 }
402 }
403 };
404
405 // Combine the macro definition and methods implementation
406 let expanded = quote! {
407 // Define the macro
408 #macro_def
409
410 // Implement methods on the enum
411 #methods_impl
412 };
413
414 TokenStream::from(expanded)
415}