1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
extern crate alloc;
extern crate proc_macro;
use alloc::format;
use alloc::string::ToString;
use alloc::vec::Vec;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Punct};
use quote::quote;
use syn::{Attribute, DataEnum, DeriveInput, Error, parse_macro_input, Type, Variant};
use syn::Data;
use syn::parse::ParseStream;
use utils::{ExpectElseOption, ExpectElseResult};
/// Implements the 'Indexed' and 'Valued' traits for an enum, allowing to get a discriminant / index
/// and a value for each variant through the functions 'discriminant' and 'value', and get this
/// variant back using the functions 'from_discriminant_opt' and 'value_to_variant_opt'. <br><br>
///
/// **Note**: This requires the 'derive' feature on your Cargo.toml, like
/// ```indexed_valued_enums = { version = "1.0.0", features=["derive", ...] }```.<br><br>
///
/// Attributes:
///
/// | Attribute | Target | Contents description |
/// |---|---|---|
/// | #[enum_valued_as(type)] | Enum | Type of your variant’s values. <br><br> This is silently an Attribute macro that adds ‘#[repr(usize)]’ to your enum, rather than a simple attribute, it’s used is also reserved if in the future new features should be born that require to modify your enum silently, if so, changes will appear both here and in the [enum_valued_as] documentation. |
/// | #[unvalued_default<br>(default value)] | Enum | Default value for variants whose value isn’t specified. |
/// | #[enum_valued_features<br>(extra features)] | Enum | List of extra features, you can find a detailed list of every extra feature in this crate’s index. |
/// | #[value(This variant’s value)] | Variant | Value this variant will resolve to when calling the ‘value’ function. |
/// | #[variant_initialize_uses<br>(Field default values)] | Variant with fields | Specifies the contents of the field of said. |
///
/// <br>
///
/// ## Step-by-step detailed explanation
///
/// **Basic implementation**: Add the derive [indexed_valued_enums::Valued] macro and then write the
/// #[enum_valued_as(*Value type*)] attribute indicating the type your variants will resolve to,
/// then on each variant write an attribute #[value(*this variants value*)]. this way: <br><br>
///
/// ```rust ignore
/// use indexed_valued_enums::{Valued, enum_valued_as};
///
/// #[derive(Valued)]
/// #[enum_valued_as(u8)]
/// pub enum MyEnum{
/// #[value(10)]
/// Variant1,
/// #[value(20)]
/// Variant2,
/// }
/// ```
/// <br>
///
/// **Add extra functionality**: Below the Derive declaration you can write the attribute
/// #[enum_valued_features(*Your desired features*)] which will automatically implement certain
/// traits or functions which will become helpful, you can check these features on the section
/// [extra features](#extra-features).<br>
///
/// ```rust ignore
/// ...
/// /// Adding 'Delegators' allows to call most of functions at
/// /// compile-time, being able to get values and variants easily
/// #[enum_valued_features(DerefToValue, Delegators)]
/// pub enum MyEnum{
/// ...
/// }
/// ```
/// <br>
///
/// **Don't repeat yourself**: For variants whose variants values are often repeated or irrelevant
/// you can use the attribute #[unvalued_default(*Your default value*)] which will make all these
/// unvalued variants to resolve into said value.<br>
///
/// ```rust ignore
/// ...
/// #[unvalued_default(50)]
/// pub enum MyEnum{
/// /// This variant's value will resolve to 10 as it is specified.
/// #[value(10)]
/// Variant1,
/// /// This variant's value will resolve to the default of 50 as a value it is not specified.
/// Variant2,
/// }
/// ```
/// <br>
///
/// **Variant's with fields can be added too!** Unlike the declarative macro, this one is compatible
/// with variants with fields, be them named or unnamed, but they have a downside: since the
/// [Indexed::from_discriminant] function must return a constant value for each variants, we also
/// need to create those variants with values at compile, when this situation arises you have two
/// options:
///
/// * Use the #[variant_initialize_uses(*Your default value*)]: Here you write the default contents
/// for these variants, for example, if one was ```IP{host: &'static str, port: u16}```, you could
/// write: #[variant_initialize_uses(host: "localhost", port: 8080)]<br><br>
/// * If the values on of the variant implement [const_default::ConstDefault]: You can simply add
/// const-default in your Cargo.toml like ```const-default = { version = "1.0.0" }``` and when this
/// variant gets resolved from [Indexed::from_discriminant], it will return it with all fields as
/// specified in [const_default::ConstDefault].
///
/// ```rust ignore
/// ...
/// pub enum MyEnum{
/// /// When applying [from::discriminant] to 0, it will return MyEnum::Variant1(23, "Jorge")
/// #[variant_initialize_uses(23, "Jorge")]
/// Variant1(u8, &'static str),
/// /// Since the attribute #[variant_initialize_uses] isn't specified, when applying
/// /// [from::discriminant] to 1, it will return MyEnum::Variant2{age: 0}, as ConstDefault
/// /// for u8 returns 0
/// Variant2{age:u8},
/// }
/// ```
/// <br>
///
/// ## Examples
///
/// A simple example using this macro could look like this:
///
/// ```rust ignore
/// use indexed_valued_enums::{Valued, enum_valued_as};
///
/// #[derive(Valued)]
/// #[enum_valued_as(&'static str)]
/// pub enum Number{
/// #[value("Zero position")]
/// Zero,
/// #[value("First position")]
/// First,
/// #[value("Second position")]
/// Second,
/// #[value("Third position")]
/// Third,
/// }
/// ```
/// <br>
/// A more complex example could look like:
///
/// ```rust ignore
/// use indexed_valued_enums::{Valued, enum_valued_as};
///
/// #[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug)]
/// #[derive(Valued)]
/// #[enum_valued_as(&'static str)]
/// #[enum_valued_features(Clone, DerefToValue, Delegators, ValueToVariantDelegators)]
/// #[unvalued_default("My default string")]
/// pub enum Number{
/// /// Zero doesn't have a value, so it's value will resolve to "My default string"
/// Zero,
/// #[value("First position")]
/// First,
/// /// Second is a variant with fields: u8 and u16, since it's not specified, when calling
/// /// [Indexed::from_discriminant] the values for both will be 0, which are their default
/// /// values on [const_default::ConstDefault::DEFAULT]
/// #[value("Second position")]
/// Second(u8, u16),
/// /// Third is a variant with fields: my_age: u8 and my_name:&'static str, as specified,
/// /// calling [Indexed::from_discriminant] will result in those fields contanining
/// /// my_age: 23, my_name: "Jorge"
/// #[variant_initialize_uses(my_age: 23, my_name: "Jorge")]
/// #[value("Third position")]
/// Third{my_age: u8, my_name:&'static str},
/// }
///
///
/// ```
#[proc_macro_derive(Valued, attributes(enum_valued_features, unvalued_default, variant_initialize_uses, value))]
pub fn derive_macro_describe(input: TokenStream) -> TokenStream {
/* let cloned_input = input.clone();
print_info("Derive input info", &*format!("{:#?}\n", parse_macro_input!(cloned_input as DeriveInput)));*/
let DeriveInput { attrs, ident, data, .. } = parse_macro_input!(input as DeriveInput);
match data {
Data::Struct(_) | Data::Union(_) => panic!("The 'Valued' derive macro targets enums, not structs or union, consider removing '#[Derive(Valued)]' for this type"),
Data::Enum(my_enum) => return derive_enum(&attrs, &ident, my_enum),
};
}
fn derive_enum(attrs: &Vec<Attribute>, enum_name: &Ident, my_enum: DataEnum) -> TokenStream {
let valued_as_attribute = find_attribute_last_in_path(&attrs, "enum_valued_as")
.expect_else(|| format!("Could not find attribute 'valued_as(*type*)'\nRemember '#[derive(Valued)]' must appear before before #[valued_as(*your type*)], like:\n\n\
#[derive(Valued)]\n#[enum_valued_as(*your type*)]\nenum {enum_name} {{\n\t...\n}} "));
let valued_as = valued_as_attribute.parse_args::<Type>()
.expect_else(|_| format!("Wrong syntax of attribute '#[valued_as(*type*)]', it must have one and just one type as content, like:\n\n\
#[derive(Valued)]\n#[enum_valued_as(*your type*)]\nenum {enum_name} {{\n\t...\n}} "));
let unvalued_default = find_attribute(&attrs, "unvalued_default")
.map(|unvalued_default| { &unvalued_default.tokens });
let features = find_attribute(&attrs, "enum_valued_features")
.map(|features_attr| features_attr.parse_args_with(parse_separated_idents)
.expect_else(|_| format!("Wrong syntax of attribute '#[enum_valued_features(*desired features*)]', it must contain just a set of your desired features, which can be consulted on the indexed_valued_enums::create_indexed_valued_enum macro\n\
Your enum's should look like this, like:\n\n\
#[derive(Valued)]\n#[enum_valued_as({valued_as:?})]\n#[value(...)] <------- Your features here, like 'Delegators, ValueToVariantDelegators...' \nenum {enum_name} {{\n\t...\n}} ")))
.unwrap_or(Vec::new());
let mut variants = Vec::with_capacity(my_enum.variants.len());
let mut variants_values = Vec::with_capacity(my_enum.variants.len());
let mut variants_fields_initializer = Vec::with_capacity(my_enum.variants.len());
my_enum.variants.iter().for_each(|variant| {
//print_info("variants", &format!("{variant:#?}"));
let variant_name = &variant.ident;
let variant_value = find_attribute(&variant.attrs, "value")
.map(|variants_value_attr| { &variants_value_attr.tokens })
.or_else(|| unvalued_default.clone())
.expect_else(|| format!("Could not find value for variant {variant_name}\n\n Consider adding a value like:\n\n\
#[value(...)] <------- Your value of type {valued_as:?}\n{variant_name}\n\n\n Or add a default value for variants without values, like\n\n\
#[derive(Valued)]\n#[enum_valued_as(*your type*)]\n#[unvalued_default(...)] <------- Your value of type\nenum {{\n\t...\n}} ", ));
let variant_initialize_uses = find_attribute(&variant.attrs, "variant_initialize_uses")
.map(|variants_value_attr| extract_token_stream_of_attribute(variants_value_attr));
utils::print_info(|| format!("variant_initialize_uses of variant {enum_name}::{variant_name}"), || format!("{:#?}", variant_initialize_uses));
let first_field_is_named = variant.fields
.iter()
.next()
.map(|first_field| first_field.ident.is_some())
.unwrap_or(false);
variants.push(&variant.ident);
variants_values.push(variant_value);
variants_fields_initializer.push(
variant_initialize_uses.map(From::from).or_else(|| fields_as_const_defaults_tokens(variant))
.map(|initializers| if first_field_is_named {
quote!(; named_field_initializers #initializers ;)
} else {
quote!(; unnamed_field_initializers #initializers ;)
})
.unwrap_or_else(|| quote!())
);
});
let output = quote! {
indexed_valued_enums::create_indexed_valued_enum !(impl traits #enum_name #valued_as; #(#variants, #variants_values #variants_fields_initializer),*);
indexed_valued_enums::create_indexed_valued_enum !(process features #enum_name, #valued_as; #(#features);*);
};
utils::print_info(|| "output_str", || format!("{:#?}", output.to_string()));
output.into()
}
fn extract_token_stream_of_attribute(variants_value_attr: &Attribute) -> TokenStream {
let mut token_stream = None;
let _ = variants_value_attr.parse_args_with(|input: ParseStream| {
token_stream = Some(TokenStream::from(input.cursor().token_stream()));
Ok(())
});
token_stream.unwrap()
}
fn fields_as_const_defaults_tokens(variant: &Variant) -> Option<proc_macro2::TokenStream> {
let internal_fields_as_default = variant.fields
.iter()
.map(|field| {
field.ident.as_ref()
.map(|field_name| quote!(#field_name (const_default::ConstDefault::DEFAULT)))
.unwrap_or_else(|| quote!((const_default::ConstDefault::DEFAULT)))
})
.reduce(|prev_token, next_token| quote!(#prev_token, #next_token));
internal_fields_as_default
}
fn parse_separated_idents(input: ParseStream) -> Result<Vec<Ident>, Error> {
let mut idents = Vec::new();
while !input.is_empty() {
match input.parse::<Ident>() {
Ok(ident) => idents.push(ident),
Err(_) => {
if input.parse::<Punct>().is_err() {
return Err(Error::new(input.span(), "Not a feature or a punctuation sign"));
}
}
}
}
Ok(idents)
}
fn find_attribute_last_in_path<'attr>(attrs: &'attr Vec<Attribute>, attribute_ident: &str) -> Option<&'attr Attribute> {
attrs.iter()
.filter(|attribute| attribute.path.segments.iter().last().is_some_and(|segment| segment.ident.to_string().eq(attribute_ident)))
.next()
}
fn find_attribute<'attr>(attrs: &'attr Vec<Attribute>, attribute_ident: &str) -> Option<&'attr Attribute> {
attrs.iter()
.filter(|attribute| attribute.path.is_ident(attribute_ident))
.next()
}
/// Attribute macro used by the 'Valued' derive macro to indicate the type of your variant's values,
/// it poses as a simple derive macro, but it is used to modify your enum and prepare it for the
/// Indexed and Valued traits, currently, this only means adding '#[repr(usize)]' to your enum, and
/// while it is unprobable, this macro is still reserved for manipulating your enum if new features
/// were to need it, for this reason, this attribute should appear right after #[derive(Valued)] and
/// before any other attributes.
#[proc_macro_attribute]
pub fn enum_valued_as(_attr: TokenStream, item: TokenStream) -> TokenStream {
let item = proc_macro2::TokenStream::from(item);
let mut res = quote!(#[repr(usize)]);
res.extend(item);
res.into()
}
mod utils;