#![doc(html_root_url = "https://docs.rs/concrete-type")]
#![warn(missing_docs)]
extern crate proc_macro;
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use quote::quote;
use syn::{Attribute, DeriveInput, Expr, Fields, Lit, Meta, parse_macro_input};
fn extract_concrete_type_path(attrs: &[Attribute]) -> Option<syn::Path> {
for attr in attrs {
if attr.path().is_ident("concrete") {
if let Meta::NameValue(meta) = &attr.meta {
if let Expr::Lit(expr_lit) = &meta.value {
if let Lit::Str(lit_str) = &expr_lit.lit {
return syn::parse_str::<syn::Path>(&lit_str.value()).ok();
}
}
}
}
}
None
}
fn transform_path_for_macro(path: &syn::Path) -> proc_macro2::TokenStream {
let starts_with_crate = path
.segments
.first()
.map(|s| s.ident == "crate")
.unwrap_or(false);
let transformed_segments: Vec<proc_macro2::TokenStream> = path
.segments
.iter()
.enumerate()
.filter_map(|(i, segment)| {
if starts_with_crate && i == 0 {
return None;
}
let ident = &segment.ident;
let args = transform_path_arguments(&segment.arguments);
Some(quote! { #ident #args })
})
.collect();
if starts_with_crate && !transformed_segments.is_empty() {
quote! { $crate :: #(#transformed_segments)::* }
} else if transformed_segments.is_empty() {
quote! { #path }
} else {
quote! { #(#transformed_segments)::* }
}
}
fn transform_path_arguments(args: &syn::PathArguments) -> proc_macro2::TokenStream {
match args {
syn::PathArguments::None => quote! {},
syn::PathArguments::AngleBracketed(angle) => {
let transformed_args: Vec<proc_macro2::TokenStream> = angle
.args
.iter()
.map(|arg| match arg {
syn::GenericArgument::Type(ty) => transform_type(ty),
syn::GenericArgument::Lifetime(lt) => quote! { #lt },
syn::GenericArgument::Const(expr) => quote! { #expr },
other => quote! { #other },
})
.collect();
quote! { < #(#transformed_args),* > }
}
syn::PathArguments::Parenthesized(paren) => {
let inputs: Vec<_> = paren.inputs.iter().map(transform_type).collect();
let output = match &paren.output {
syn::ReturnType::Default => quote! {},
syn::ReturnType::Type(arrow, ty) => {
let transformed = transform_type(ty);
quote! { #arrow #transformed }
}
};
quote! { ( #(#inputs),* ) #output }
}
}
}
fn transform_type(ty: &syn::Type) -> proc_macro2::TokenStream {
match ty {
syn::Type::Path(type_path) => {
let transformed = transform_path_for_macro(&type_path.path);
if let Some(qself) = &type_path.qself {
let qself_ty = transform_type(&qself.ty);
quote! { < #qself_ty > :: #transformed }
} else {
transformed
}
}
syn::Type::Reference(ref_type) => {
let lifetime = &ref_type.lifetime;
let mutability = &ref_type.mutability;
let elem = transform_type(&ref_type.elem);
quote! { & #lifetime #mutability #elem }
}
syn::Type::Tuple(tuple) => {
let elems: Vec<_> = tuple.elems.iter().map(transform_type).collect();
quote! { ( #(#elems),* ) }
}
syn::Type::Slice(slice) => {
let elem = transform_type(&slice.elem);
quote! { [ #elem ] }
}
syn::Type::Array(array) => {
let elem = transform_type(&array.elem);
let len = &array.len;
quote! { [ #elem ; #len ] }
}
syn::Type::Ptr(ptr) => {
let mutability = if ptr.mutability.is_some() {
quote! { mut }
} else {
quote! { const }
};
let elem = transform_type(&ptr.elem);
quote! { * #mutability #elem }
}
other => quote! { #other },
}
}
#[proc_macro_derive(Concrete, attributes(concrete))]
pub fn derive_concrete(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let type_name = &input.ident;
let type_name_str = type_name.to_string();
let macro_name_str = type_name_str.to_case(Case::Snake);
let macro_name = syn::Ident::new(¯o_name_str, type_name.span());
let data_enum = match &input.data {
syn::Data::Enum(data_enum) => data_enum,
_ => {
return syn::Error::new_spanned(
type_name,
"Concrete can only be derived for enums or structs with type parameters",
)
.to_compile_error()
.into();
}
};
let mut variant_mappings = Vec::new();
for variant in &data_enum.variants {
let variant_name = &variant.ident;
if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
variant_mappings.push((variant_name, concrete_type));
} else {
return syn::Error::new_spanned(
variant_name,
format!(
"Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
variant_name
),
)
.to_compile_error()
.into();
}
}
let macro_match_arms = variant_mappings
.iter()
.map(|(variant_name, concrete_type)| {
let transformed_path = transform_path_for_macro(concrete_type);
quote! {
#type_name::#variant_name => {
type $type_param = #transformed_path;
$code_block
}
}
});
let macro_def = quote! {
#[macro_export]
macro_rules! #macro_name {
($enum_instance:expr; $type_param:ident => $code_block:block) => {
match $enum_instance {
#(#macro_match_arms),*
}
};
}
};
let expanded = quote! {
#macro_def
};
TokenStream::from(expanded)
}
#[proc_macro_derive(ConcreteConfig, attributes(concrete))]
pub fn derive_concrete_config(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let type_name = &input.ident;
let type_name_str = type_name.to_string();
let base_name = if type_name_str.ends_with("Config") {
&type_name_str[0..type_name_str.len() - 6]
} else {
&type_name_str
};
let macro_name_str = format!("{}_config", base_name.to_case(Case::Snake));
let macro_name = syn::Ident::new(¯o_name_str, type_name.span());
let data_enum = match &input.data {
syn::Data::Enum(data_enum) => data_enum,
_ => {
return syn::Error::new_spanned(
type_name,
"ConcreteConfig can only be derived for enums with data",
)
.to_compile_error()
.into();
}
};
let mut variant_mappings = Vec::new();
for variant in &data_enum.variants {
let variant_name = &variant.ident;
if let Some(concrete_type) = extract_concrete_type_path(&variant.attrs) {
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
variant_mappings.push((variant_name, concrete_type, true));
}
Fields::Unit => {
variant_mappings.push((variant_name, concrete_type, false));
}
_ => {
return syn::Error::new_spanned(
variant_name,
format!(
"Enum variant `{}` must either be a unit variant or have exactly one unnamed field for config",
variant_name
),
)
.to_compile_error()
.into();
}
}
} else {
return syn::Error::new_spanned(
variant_name,
format!(
"Enum variant `{}` is missing the #[concrete = \"...\"] attribute",
variant_name
),
)
.to_compile_error()
.into();
}
}
let config_arms = variant_mappings
.iter()
.map(|(variant_name, _concrete_type, has_config)| {
if *has_config {
quote! {
#type_name::#variant_name(config) => config
}
} else {
quote! {
#type_name::#variant_name => &() }
}
});
let macro_match_arms =
variant_mappings
.iter()
.map(|(variant_name, concrete_type, has_config)| {
let transformed_path = transform_path_for_macro(concrete_type);
if *has_config {
quote! {
#type_name::#variant_name(config) => {
type $type_param = #transformed_path;
let $config_param = config;
$code_block
}
}
} else {
quote! {
#type_name::#variant_name => {
type $type_param = #transformed_path;
let $config_param = (); $code_block
}
}
}
});
let macro_def = quote! {
#[macro_export]
macro_rules! #macro_name {
($enum_instance:expr; ($type_param:ident, $config_param:ident) => $code_block:block) => {
match $enum_instance {
#(#macro_match_arms),*
}
};
}
};
let methods_impl = quote! {
impl #type_name {
pub fn config(&self) -> &dyn std::any::Any {
match self {
#(#config_arms),*
}
}
}
};
let expanded = quote! {
#macro_def
#methods_impl
};
TokenStream::from(expanded)
}