Compare commits

...

3 Commits

Author SHA1 Message Date
pantonshire a3137d86c2 rename and rename_all attributes 2 years ago
pantonshire 5bd28d6774 use core::compile_error instead of std::compile_error 2 years ago
pantonshire 16ae2d403e case conversion functions 2 years ago

@ -7,8 +7,9 @@ use syn::{DataEnum, Fields, Attribute};
use crate::attribute::{Dict, Value};
use crate::error::{MacroError, MacroResult};
use crate::rename::RenameVariant;
use crate::{TokenStream2, CASE_SENSITIVE};
use crate::{CASE_INSENSITIVE, CRATE_ATTR, IGNORE, NAME, OTHER};
use crate::{CASE_INSENSITIVE, RENAME, RENAME_ALL, CRATE_ATTR, IGNORE, NAME, OTHER};
#[derive(Clone)]
pub(crate) struct Enum<'a> {
@ -192,19 +193,20 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
let mut taken_sensitive_names = HashSet::new();
let mut other_variant = false;
let global_case_insensitive = {
let mut dict = Dict::from_attrs(CRATE_ATTR, attrs)?;
let mut global_dict = Dict::from_attrs(CRATE_ATTR, attrs)?;
let (global_case_insensitive, _) = dict.remove_typed_or_default(
let (global_case_insensitive, _) = global_dict.remove_typed_or_default(
CASE_INSENSITIVE,
(false, data.enum_token.span()),
Value::value_bool,
)?;
dict.assert_empty()?;
let global_rename = global_dict.remove_typed(RENAME_ALL, Value::value_string)?
.map(|(global_rename, span)| RenameVariant::from_str(&global_rename, span))
.transpose()?;
global_case_insensitive
};
global_dict.assert_empty()?;
drop(global_dict);
for variant in data.variants.iter() {
let variant_span = variant.span();
@ -213,10 +215,7 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
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 name_opt = dict.remove_typed(NAME, Value::value_string)?;
let (other, other_span) = dict.remove_typed_or_default(
OTHER,
@ -257,6 +256,11 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
}
};
let rename = dict.remove_typed(RENAME, Value::value_string)?
.map(|(rename, span)| RenameVariant::from_str(&rename, span))
.transpose()?
.or(global_rename);
// Return an error if there are any unrecognised keys in the Dict
dict.assert_empty()?;
@ -318,7 +322,14 @@ pub(crate) fn parse_enum<'a>(data: &'a DataEnum, attrs: &'a [Attribute]) -> Macr
// 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()),
None => {
let name_span = variant.ident.span();
let mut name = variant.ident.to_string();
if let Some(rename) = rename {
name = rename.apply(&name);
}
(name, name_span)
},
};
// Do not allow duplicate names

@ -36,7 +36,7 @@ impl MacroError {
pub(crate) fn to_token_stream2(&self) -> TokenStream2 {
let message = &self.message;
quote_spanned! {
self.span => ::std::compile_error!(#message);
self.span => ::core::compile_error!(#message);
}
}
}

@ -19,6 +19,7 @@ use crate::enums::{Enum, Variant, VariantType};
mod attribute;
mod enums;
mod error;
mod rename;
const CRATE_ATTR: &'static str = "enumscribe";
@ -27,6 +28,8 @@ const OTHER: &'static str = "other";
const IGNORE: &'static str = "ignore";
const CASE_INSENSITIVE: &'static str = "case_insensitive";
const CASE_SENSITIVE: &'static str = "case_sensitive";
const RENAME: &'static str = "rename";
const RENAME_ALL: &'static str = "rename_all";
type TokenStream2 = proc_macro2::TokenStream;

@ -0,0 +1,236 @@
use proc_macro2::Span;
use crate::error::{MacroResult, MacroError};
#[derive(Clone, Copy, Debug)]
pub(crate) enum RenameVariant {
Lower,
Upper,
Pascal,
Camel,
Snake,
ScreamingSnake,
Kebab,
ScreamingKebab,
}
impl RenameVariant {
pub(crate) fn from_str(s: &str, span: Span) -> MacroResult<Self> {
// Shame we can't use enumscribe for this...
match s {
"lowercase" => Ok(Self::Lower),
"UPPERCASE" => Ok(Self::Upper),
"PascalCase" => Ok(Self::Pascal),
"camelCase" => Ok(Self::Camel),
"snake_case" => Ok(Self::Snake),
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnake),
"kebab-case" => Ok(Self::Kebab),
"SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebab),
_ => Err(MacroError::new(
format!(
"invalid case {:?} (allowed values are: \
lowercase, \
UPPERCASE, \
PascalCase, \
camelCase, \
snake_case, \
SCREAMING_SNAKE_CASE, \
kebab-case, \
SCREAMING-KEBAB-CASE)",
s
),
span
)),
}
}
pub(crate) fn apply(self, s: &str) -> String {
match self {
RenameVariant::Lower => s.to_lowercase(),
RenameVariant::Upper => s.to_uppercase(),
RenameVariant::Pascal => PascalCase.convert_enum_variant(s),
RenameVariant::Camel => CamelCase.convert_enum_variant(s),
RenameVariant::Snake => SnakeCase(CharCase::Lower).convert_enum_variant(s),
RenameVariant::ScreamingSnake => SnakeCase(CharCase::Upper).convert_enum_variant(s),
RenameVariant::Kebab => KebabCase(CharCase::Lower).convert_enum_variant(s),
RenameVariant::ScreamingKebab => KebabCase(CharCase::Upper).convert_enum_variant(s),
}
}
}
trait WordAwareCase {
fn convert_enum_variant(&self, s: &str) -> String {
let mut converted = String::new();
let mut component = String::new();
let mut prev_case = Option::None;
for c in s.chars() {
let case = CharCase::of(c);
let (push_component, push_char) = {
if matches!((prev_case, case), (Some(CharCase::Lower), Some(CharCase::Upper))) {
(true, true)
} else if c == '_' {
(true, false)
} else {
(false, true)
}
};
if push_component && !component.is_empty() {
self.push_word(&mut converted, &component);
component.clear();
}
if push_char {
component.push(c);
}
prev_case = case;
}
if !component.is_empty() {
self.push_word(&mut converted, &component);
}
converted
}
fn push_word(&self, buf: &mut String, word: &str);
}
struct PascalCase;
impl WordAwareCase for PascalCase {
fn push_word(&self, buf: &mut String, word: &str) {
if let Some((head, tail)) = str_head_tail(word) {
buf.extend(head.to_uppercase());
buf.push_str(&tail.to_lowercase());
}
}
}
struct CamelCase;
impl WordAwareCase for CamelCase {
fn push_word(&self, buf: &mut String, word: &str) {
if buf.is_empty() {
buf.push_str(&word.to_lowercase());
} else if let Some((head, tail)) = str_head_tail(word) {
buf.extend(head.to_uppercase());
buf.push_str(&tail.to_lowercase());
}
}
}
struct SnakeCase(CharCase);
impl WordAwareCase for SnakeCase {
fn push_word(&self, buf: &mut String, word: &str) {
if !buf.is_empty() {
buf.push('_');
}
buf.push_str(&self.0.convert(word));
}
}
struct KebabCase(CharCase);
impl WordAwareCase for KebabCase {
fn push_word(&self, buf: &mut String, word: &str) {
if !buf.is_empty() {
buf.push('-');
}
buf.push_str(&self.0.convert(word));
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum CharCase {
Upper,
Lower,
}
impl CharCase {
fn of(c: char) -> Option<Self> {
if c.is_uppercase() {
Some(Self::Upper)
} else if c.is_lowercase() {
Some(Self::Lower)
} else {
None
}
}
fn convert(self, s: &str) -> String {
match self {
Self::Upper => s.to_uppercase(),
Self::Lower => s.to_lowercase(),
}
}
}
fn str_head_tail(s: &str) -> Option<(char, &str)> {
let head = s.chars().next()?;
let tail = &s[head.len_utf8()..];
Some((head, tail))
}
#[cfg(test)]
mod test {
use super::{PascalCase, CamelCase, SnakeCase, KebabCase, CharCase, WordAwareCase};
#[test]
fn test_pascal_case() {
assert_eq!(PascalCase.convert_enum_variant(""), "");
assert_eq!(PascalCase.convert_enum_variant("foo"), "Foo");
assert_eq!(PascalCase.convert_enum_variant("fooBaa"), "FooBaa");
assert_eq!(PascalCase.convert_enum_variant("FooBaa"), "FooBaa");
assert_eq!(PascalCase.convert_enum_variant("foo_baa"), "FooBaa");
assert_eq!(PascalCase.convert_enum_variant("FOO_BAA"), "FooBaa");
}
#[test]
fn test_camel_case() {
assert_eq!(CamelCase.convert_enum_variant(""), "");
assert_eq!(CamelCase.convert_enum_variant("foo"), "foo");
assert_eq!(CamelCase.convert_enum_variant("fooBaa"), "fooBaa");
assert_eq!(CamelCase.convert_enum_variant("FooBaa"), "fooBaa");
assert_eq!(CamelCase.convert_enum_variant("foo_baa"), "fooBaa");
assert_eq!(CamelCase.convert_enum_variant("FOO_BAA"), "fooBaa");
}
#[test]
fn test_snake_case() {
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant(""), "");
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("foo"), "foo");
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("fooBaa"), "foo_baa");
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("FooBaa"), "foo_baa");
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("foo_baa"), "foo_baa");
assert_eq!(SnakeCase(CharCase::Lower).convert_enum_variant("FOO_BAA"), "foo_baa");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant(""), "");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("foo"), "FOO");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("fooBaa"), "FOO_BAA");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("FooBaa"), "FOO_BAA");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("foo_baa"), "FOO_BAA");
assert_eq!(SnakeCase(CharCase::Upper).convert_enum_variant("FOO_BAA"), "FOO_BAA");
}
#[test]
fn test_kebab_case() {
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant(""), "");
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("foo"), "foo");
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("fooBaa"), "foo-baa");
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("FooBaa"), "foo-baa");
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("foo_baa"), "foo-baa");
assert_eq!(KebabCase(CharCase::Lower).convert_enum_variant("FOO_BAA"), "foo-baa");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant(""), "");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("foo"), "FOO");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("fooBaa"), "FOO-BAA");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("FooBaa"), "FOO-BAA");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("foo_baa"), "FOO-BAA");
assert_eq!(KebabCase(CharCase::Upper).convert_enum_variant("FOO_BAA"), "FOO-BAA");
}
}

@ -0,0 +1,21 @@
use enumscribe::*;
#[derive(ScribeStaticStr, TryUnscribe, PartialEq, Eq, Debug)]
#[enumscribe(rename_all = "snake_case")]
enum Bird {
BlackRedstart,
#[enumscribe(case_insensitive)]
GardenWarbler,
#[enumscribe(rename = "SCREAMING-KEBAB-CASE")]
BarnacleGoose,
}
fn main() {
assert_eq!(Bird::BlackRedstart.scribe(), "black_redstart");
assert_eq!(Bird::GardenWarbler.scribe(), "garden_warbler");
assert_eq!(Bird::BarnacleGoose.scribe(), "BARNACLE-GOOSE");
assert_eq!(Bird::try_unscribe("black_redstart").unwrap(), Bird::BlackRedstart);
assert_eq!(Bird::try_unscribe("gArDeN_wArBlEr").unwrap(), Bird::GardenWarbler);
assert_eq!(Bird::try_unscribe("BARNACLE-GOOSE").unwrap(), Bird::BarnacleGoose);
}
Loading…
Cancel
Save