From 1a6feab1f2b33509f8b852c7a755d0ded64b2df7 Mon Sep 17 00:00:00 2001 From: Pantonshire Date: Tue, 18 May 2021 13:29:38 +0100 Subject: [PATCH] case_insensitive key, refactoring --- examples/airports.rs | 1 + src/attribute.rs | 13 +++- src/enums.rs | 165 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 156 ++-------------------------------------- 4 files changed, 185 insertions(+), 150 deletions(-) create mode 100644 src/enums.rs diff --git a/examples/airports.rs b/examples/airports.rs index 2d1482d..ba07eb9 100644 --- a/examples/airports.rs +++ b/examples/airports.rs @@ -3,6 +3,7 @@ extern crate enumscribe; #[derive(EnumToString)] enum Foo { + #[enumscribe(str = "b", case_insensitive)] Baa, #[enumscribe(ignore)] Baz(), diff --git a/src/attribute.rs b/src/attribute.rs index 8716b08..6a153dc 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -125,7 +125,7 @@ impl Dict { Ok(dict) } - pub(crate) fn remove_typed_value(&mut self, key: &str, converter: F) -> MacroResult> + pub(crate) fn remove_typed(&mut self, key: &str, converter: F) -> MacroResult> where F: Fn(&Value) -> ValueTypeResult { @@ -141,6 +141,17 @@ impl Dict { } } + pub(crate) fn remove_typed_or_default(&mut self, key: &str, default: (T, Span), converter: F) -> MacroResult<(T, Span)> + where + F: Fn(&Value) -> ValueTypeResult + { + match self.remove_typed(key, converter) { + Ok(Some(value)) => Ok(value), + Ok(None) => Ok(default), + Err(err) => Err(err), + } + } + pub(crate) fn assert_empty(&self) -> MacroResult<()> { if self.inner.is_empty() { Ok(()) diff --git a/src/enums.rs b/src/enums.rs new file mode 100644 index 0000000..c9dac1f --- /dev/null +++ b/src/enums.rs @@ -0,0 +1,165 @@ +use std::collections::HashSet; + +use proc_macro2::Span; +use syn::{Attribute, Data, DataEnum, DeriveInput, Fields, LitStr}; +use syn::parse::{ParseBuffer, ParseStream}; +use syn::spanned::Spanned; + +use crate::{CRATE_ATTR, NAME, OTHER, IGNORE, CASE_INSENSITIVE}; +use crate::attribute::*; +use crate::error::{MacroError, MacroResult}; + +#[derive(Clone, Debug)] +pub(crate) struct Enum { + pub(crate) variants: Vec, +} + +#[derive(Clone, Debug)] +pub(crate) struct Variant { + pub(crate) ident: String, + pub(crate) v_type: VariantType, + pub(crate) span: Span, +} + +#[derive(Clone, Debug)] +pub(crate) enum VariantType { + Ignore, + Named { + name: String, + constructor: VariantConstructor, + case_insensitive: bool, + }, + Other { + field_name: Option //use {} for constructor if Some, use () if None + }, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum VariantConstructor { + None, + Paren, + Brace, +} + +pub(crate) fn parse_enum(data: DataEnum) -> MacroResult { + let mut variants = Vec::with_capacity(data.variants.len()); + let mut taken_names = HashSet::new(); + let mut other_variant = false; + + for variant in data.variants { + let variant_ident = variant.ident.to_string(); + let variant_span = variant.span(); + + // Parse the `#[enumscribe(...)]` attributes for this variant into a single Dict + let mut dict = Dict::from_attrs(CRATE_ATTR, &variant.attrs)?; + + // Convert the values in the Dict to the appropriate types + let name_opt = dict.remove_typed(NAME, Value::value_string)?; + let (other, other_span) = dict.remove_typed_or_default(OTHER, (false, variant_span), Value::value_bool)?; + let (ignore, _) = dict.remove_typed_or_default(IGNORE, (false, variant_span), Value::value_bool)?; + let (case_insensitive, _) = dict.remove_typed_or_default(CASE_INSENSITIVE, (false, variant_span), Value::value_bool)?; + + // Return an error if there are any unrecognised keys in the Dict + dict.assert_empty()?; + + let scribe_variant = if ignore { + Variant { + ident: variant_ident, + v_type: VariantType::Ignore, + span: variant_span, + } + } else if other { + // Return an error if there is already an "other" variant for this enum + if other_variant { + return Err(MacroError::new( + format!( + "cannot have multiple variants marked as {}", + OTHER + ), + other_span, + )); + } + + other_variant = true; + + // Return an error if a str name is provided for this variant + if let Some((_, name_span)) = name_opt { + return Err(MacroError::new( + format!( + "cannot use {} for variant {} because it is marked as {}", + NAME, variant_ident, OTHER + ), + name_span, + )); + } + + // Return an error if this variant doesn't have exactly one field + if variant.fields.len() != 1 { + return Err(MacroError::new( + format!( + "the variant {} must have exactly one field because it is marked as {}", + variant_ident, OTHER + ), + variant_span, + )); + } + + // Get the name of the variant's field (or None if it is unnamed) + let field_name = variant.fields.iter().next() + .and_then(|field| field.ident.as_ref().map(|ident| ident.to_string())); + + Variant { + ident: variant_ident, + v_type: VariantType::Other { field_name }, + span: variant_span, + } + } else { + // Use the str name if one is provided, otherwise use the variant's name + let (name, name_span) = match name_opt { + Some((name, name_span)) => (name, name_span), + None => (variant.ident.to_string(), variant.ident.span()) + }; + + // Do not allow duplicate names + if taken_names.contains(&name) { + return Err(MacroError::new( + format!("duplicate name \"{}\"", name), + name_span, + )); + } + + // Return an error if the variant has any fields + if variant.fields.len() != 0 { + return Err(MacroError::new( + format!( + "the variant {} must not have any fields\n\ + hint: if you do not want to remove {}\'s fields, try using \ + #[enumscribe(ignore)] for {}", + variant_ident, variant_ident, variant_ident + ), + variant_span, + )); + } + + // The variant is allowed to have an empty constructor, so find out if it has one + // and, if so, what type of constructor (parentheses or braces) + let constructor = match variant.fields { + Fields::Named(_) => VariantConstructor::Brace, + Fields::Unnamed(_) => VariantConstructor::Paren, + Fields::Unit => VariantConstructor::None, + }; + + taken_names.insert(name.clone()); + + Variant { + ident: variant_ident, + v_type: VariantType::Named { name, constructor, case_insensitive }, + span: variant_span, + } + }; + + variants.push(scribe_variant); + } + + Ok(Enum { variants }) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3ee7ac0..812c6e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,158 +10,16 @@ use syn::spanned::Spanned; use attribute::*; use error::{MacroError, MacroResult}; -mod error; +mod enums; mod attribute; +mod error; const CRATE_ATTR: &'static str = "enumscribe"; -#[derive(Clone, Debug)] -struct Enum { - variants: Vec, -} - -#[derive(Clone, Debug)] -struct Variant { - ident: String, - v_type: VariantType, - span: Span, -} - -#[derive(Clone, Debug)] -enum VariantType { - Ignore, - Named { name: String, constructor: VariantConstructor }, - Other { field_name: Option }, //use {} for constructor if Some, use () if None -} - -#[derive(Clone, Copy, Debug)] -enum VariantConstructor { - None, - Paren, - Brace, -} - -fn parse_enum(data: DataEnum) -> MacroResult { - const NAME: &'static str = "str"; - const OTHER: &'static str = "other"; - const IGNORE: &'static str = "ignore"; - - let mut variants = Vec::with_capacity(data.variants.len()); - let mut taken_names = HashSet::new(); - let mut other_variant = false; - - for variant in data.variants { - let variant_ident = variant.ident.to_string(); - let variant_span = variant.span(); - - let mut dict = Dict::from_attrs(CRATE_ATTR, &variant.attrs)?; - - let name_opt = dict.remove_typed_value(NAME, Value::value_string)?; - - let other = match dict.remove_typed_value(OTHER, Value::value_bool)? { - Some((other, _)) => other, - None => false - }; - - let ignore = match dict.remove_typed_value(IGNORE, Value::value_bool)? { - Some((ignore, _)) => ignore, - None => false - }; - - dict.assert_empty()?; - - let scribe_variant = if ignore { - Variant { - ident: variant_ident, - v_type: VariantType::Ignore, - span: variant_span, - } - } else if other { - if other_variant { - return Err(MacroError::new( - format!( - "cannot have multiple variants marked as {}", - OTHER - ), - variant_span, - )); - } - - other_variant = true; - - if let Some((_, name_span)) = name_opt { - return Err(MacroError::new( - format!( - "cannot use {} for variant {} because it is marked as {}", - NAME, variant_ident, OTHER - ), - name_span, - )); - } - - if variant.fields.len() != 1 { - return Err(MacroError::new( - format!( - "the variant {} must have exactly one field because it is marked as {}", - variant_ident, OTHER - ), - variant_span, - )); - } - - let field_name = variant.fields.iter().next() - .and_then(|field| field.ident.as_ref().map(|ident| ident.to_string())); - - Variant { - ident: variant_ident, - v_type: VariantType::Other { field_name }, - span: variant_span, - } - } else { - let (name, name_span) = match name_opt { - Some((name, name_span)) => (name, name_span), - None => (variant.ident.to_string(), variant.ident.span()) - }; - - if taken_names.contains(&name) { - return Err(MacroError::new( - format!("duplicate name \"{}\"", name), - name_span, - )); - } - - if variant.fields.len() != 0 { - return Err(MacroError::new( - format!( - "the variant {} must not have any fields\n\ - hint: if you do not want to remove {}\'s fields, try using \ - #[enumscribe(ignore)] for {}", - variant_ident, variant_ident, variant_ident - ), - variant_span, - )); - } - - let constructor = match variant.fields { - Fields::Named(_) => VariantConstructor::Brace, - Fields::Unnamed(_) => VariantConstructor::Paren, - Fields::Unit => VariantConstructor::None, - }; - - taken_names.insert(name.clone()); - - Variant { - ident: variant_ident, - v_type: VariantType::Named { name, constructor }, - span: variant_span, - } - }; - - variants.push(scribe_variant); - } - - Ok(Enum { variants }) -} +const NAME: &'static str = "str"; +const OTHER: &'static str = "other"; +const IGNORE: &'static str = "ignore"; +const CASE_INSENSITIVE: &'static str = "case_insensitive"; #[proc_macro_derive(EnumToString, attributes(enumscribe))] pub fn derive_enum_to_string(input: TokenStream) -> TokenStream { @@ -173,7 +31,7 @@ pub fn derive_enum_to_string(input: TokenStream) -> TokenStream { Data::Union(_) => return MacroError::new("cannot use enumscribe for unions", input.ident.span()).to_token_stream() }; - let variants = match parse_enum(enum_data) { + let variants = match enums::parse_enum(enum_data) { Ok(variants) => variants, Err(err) => return err.to_token_stream() };