Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 111 additions & 158 deletions src/attributes.rs
Original file line number Diff line number Diff line change
@@ -1,91 +1,103 @@
use std::borrow::Cow;
use std::collections::HashMap;

use crate::case::Case;
use proc_macro2::{Ident, TokenStream};
use syn::{DeriveInput, Meta};

/// The attribute name used for enum variant renaming.
static ATTRIBUTE_NAME: &str = "enum_stringify";

/// Parses a string literal by removing surrounding quotes if present.
fn parse_string(s: &str) -> Result<String, ()> {
if s.starts_with('"') && s.ends_with('"') {
Ok(s[1..s.len() - 1].to_string())
} else {
Err(())
/// Parses a string literal by removing surrounding double quotes if present.
///
/// # Arguments
/// * `s` - A string slice containing the quoted string.
///
/// # Returns
/// * `Ok(String)` if the string is correctly formatted.
/// * `Err(&'static str)` if the string is not enclosed in double quotes.
fn parse_string(s: &str) -> Result<String, &'static str> {
s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.map(std::string::ToString::to_string)
.ok_or("String must be enclosed in double quotes")
}

/// Parses a list of attribute tokens into a vector of type `T`.
///
/// # Arguments
/// * `tokens` - A reference to a token stream containing attributes.
///
/// # Returns
/// * `Ok(Vec<T>)` if parsing succeeds.
/// * `Err(String)` if parsing fails due to incorrect syntax.
fn parse_token_list<T>(tokens: &TokenStream) -> Result<Vec<T>, String>
where
T: TryFrom<(String, String)>,
{
let mut result = Vec::new();
let mut tokens = tokens.clone().into_iter();

while let Some(attribute_type) = tokens.next() {
let attribute_type = attribute_type.to_string();

let Some(eq_token) = tokens.next() else {
return Err(format!("Expected '=' after '{attribute_type}'"));
};
if eq_token.to_string() != "=" {
return Err(format!("Unexpected token '{eq_token}', expected '='"));
}

let value = tokens.next().ok_or("Value must be specified")?.to_string();

match T::try_from((attribute_type.clone(), value)) {
Ok(value) => result.push(value),
Err(_) => return Err(format!("Invalid argument: {attribute_type}")),
}

if let Some(comma_separator) = tokens.next() {
if comma_separator.to_string() != "," {
return Err("Expected a comma-separated attribute list".to_string());
}
}
}
Ok(result)
}

/// Represents a rename attribute for an enum variant.
#[derive(Clone)]
/// Represents a rename attribute applied to an enum variant.
struct VariantRename(String);

impl TryFrom<(String, String)> for VariantRename {
type Error = ();
type Error = &'static str;

fn try_from(value: (String, String)) -> Result<Self, Self::Error> {
if value.0 == "rename" {
Ok(Self(parse_string(value.1.as_str())?))
Ok(Self(parse_string(&value.1)?))
} else {
Err(())
Err("Not a rename string")
}
}
}

impl VariantRename {
/// Parses an attribute to determine if it is a rename directive.
/// Parses the rename attribute from a given `syn::Attribute`.
fn parse_args(attribute: &syn::Attribute) -> Option<Self> {
if !attribute.path().is_ident(ATTRIBUTE_NAME) {
return None;
}

match &attribute.meta {
Meta::List(list) => {
let path = list
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>();

if path == vec![ATTRIBUTE_NAME] {
Some(
Attributes::parse_token_list::<Self>(&list.tokens)
.ok()?
.first()?
.clone(),
)
} else {
None
}
}
Meta::List(list) => parse_token_list::<Self>(&list.tokens)
.ok()?
.first()
.cloned(),
_ => None,
}
}
}

// Represents different renaming attributes that can be applied to enum variants.
enum RenameAttribute {
Case(Case),
Prefix(String),
Suffix(String),
}

impl TryFrom<(String, String)> for RenameAttribute {
type Error = ();

fn try_from(value: (String, String)) -> Result<Self, Self::Error> {
if value.0 == "prefix" {
Ok(Self::Prefix(parse_string(value.1.as_str())?))
} else if value.0 == "suffix" {
Ok(Self::Suffix(parse_string(value.1.as_str())?))
} else if value.0 == "case" {
Ok(Self::Case(Case::try_from(value)?))
} else {
Err(())
}
}
}

/// Represents attribute configurations for renaming enum variants.
#[derive(Default)]
pub(crate) struct Attributes {
case: Option<Case>,
Expand All @@ -94,107 +106,70 @@ pub(crate) struct Attributes {
}

impl Attributes {
/// Constructs an `Attributes` instance by parsing derive attributes from an AST.
/// Constructs an `Attributes` instance by parsing the attributes of a derive input.
pub(crate) fn new(ast: &DeriveInput) -> Self {
let mut new = Self {
case: None,
prefix: None,
suffix: None,
};

let mut new = Self::default();
ast.attrs.iter().for_each(|attr| {
let rename_rules = Self::parse_args(attr);
if let Some(rename_rules) = rename_rules {
if let Some(rename_rules) = Self::parse_args(attr) {
new.prefix = rename_rules.prefix;
new.suffix = rename_rules.suffix;
new.case = rename_rules.case;
};
}
});

new
}

/// Parses attributes related to casing, prefixes, and suffixes.
fn parse_args(attribute: &syn::Attribute) -> Option<Self> {
if !attribute.path().is_ident(ATTRIBUTE_NAME) {
return None;
}

let mut new = Self::default();

match &attribute.meta {
Meta::List(list) => {
let path = list
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>();

if path == vec![ATTRIBUTE_NAME] {
let attributes =
Attributes::parse_token_list::<RenameAttribute>(&list.tokens).ok()?;
for attr in attributes {
new.merge_attribute(attr);
}
Some(new)
} else {
None
let attributes = parse_token_list::<(String, String)>(&list.tokens).ok()?;
for value in attributes {
new.update_attribute(value);
}
Some(new)
}
_ => None,
}
}

/// Merges parsed attribute into the struct.
fn merge_attribute(&mut self, attr: RenameAttribute) {
match attr {
RenameAttribute::Prefix(s) => self.prefix = Some(s),
RenameAttribute::Suffix(s) => self.suffix = Some(s),
RenameAttribute::Case(s) => self.case = Some(s),
fn update_attribute(&mut self, value: (String, String)) {
match value.0.as_str() {
"prefix" => self.prefix = parse_string(&value.1).ok(),
"suffix" => self.suffix = parse_string(&value.1).ok(),
"case" => self.case = Case::try_from(value).ok(),
_ => {}
}
}

/// Parses tokens into attributes.
fn parse_token_list<T>(tokens: &TokenStream) -> Result<Vec<T>, String>
where
T: TryFrom<(String, String)>,
{
let mut result = Vec::new();
let mut tokens = tokens.clone().into_iter();

while let Some(attribute_type) = tokens.next() {
let attribute_type = attribute_type.to_string();

assert!(
tokens.next().expect("type must be specified").to_string() == "=",
"too many arguments"
);
let value = tokens.next().expect("value must be specified").to_string();

match T::try_from((attribute_type.clone(), value)) {
Ok(value) => result.push(value),
Err(_) => return Err(format!("Invalid argument: {attribute_type}")),
}
/// Applies renaming rules (prefix, suffix, case) to a given string.
fn rename<'a>(&self, s: &'a str) -> Cow<'a, str> {
let mut new_name = Cow::Borrowed(s);

if let Some(comma_separator) = tokens.next() {
assert!(
comma_separator.to_string() == ",",
"Expected a comma separated attribute list"
);
}
if let Some(prefix) = &self.prefix {
new_name = Cow::Owned(format!("{prefix}{new_name}"));
}
if let Some(suffix) = &self.suffix {
new_name = Cow::Owned(format!("{new_name}{suffix}"));
}
Ok(result)
if let Some(case) = &self.case {
new_name = Cow::Owned(case.to_case(&new_name));
}
new_name
}
}

/// Stores enum variants and their optional renaming attributes.
/// Stores renaming information for enum variants.
pub(crate) struct Variants {
variant_renames: HashMap<Ident, Option<VariantRename>>,
}

impl Variants {
/// Parses an AST to extract enum variants and their attributes.
/// Constructs a `Variants` instance by parsing the derive input.
pub(crate) fn new(ast: &DeriveInput) -> Self {
let mut new = Self {
variant_renames: HashMap::new(),
Expand All @@ -208,53 +183,31 @@ impl Variants {
variants
.iter()
.for_each(|variant| new.parse_variant_attribute(variant));

new
}

/// Extracts renaming attributes from an enum variant.
/// Parses attributes for a given enum variant.
fn parse_variant_attribute(&mut self, variant: &syn::Variant) {
let attribute_renames = variant.attrs.iter().filter_map(VariantRename::parse_args);

let rename = attribute_renames.last();

let rename = variant
.attrs
.iter()
.filter_map(VariantRename::parse_args)
.reduce(|_, new| new);
self.variant_renames.insert(variant.ident.clone(), rename);
}

/// Applies attributes (prefix, suffix, case) to enum variant names.
/// Applies renaming rules to each enum variant name.
pub(crate) fn apply(&self, attributes: &Attributes) -> Vec<(syn::Ident, String)> {
let mut new_names = Vec::new();

for (name, rename) in &self.variant_renames {
if let Some(rename) = rename {
new_names.push(rename.0.clone());
continue;
}
let mut new_name = String::new();
if let Some(prefix) = &attributes.prefix {
new_name.push_str(prefix);
}

new_name.push_str(&name.to_string());

if let Some(suffix) = &attributes.suffix {
new_name.push_str(suffix);
}

if let Some(case) = &attributes.case {
new_name = case.to_case(&new_name);
}

new_names.push(new_name);
}

let tmp = self
.variant_renames
.keys()
.cloned()
.zip(new_names)
.collect::<Vec<_>>();

tmp
self.variant_renames
.iter()
.map(|(ident, rename)| {
let new_name = if let Some(rename) = rename {
rename.0.clone()
} else {
attributes.rename(ident.to_string().as_str()).into_owned()
};
(ident.clone(), new_name)
})
.collect()
}
}
8 changes: 4 additions & 4 deletions src/case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ pub(crate) struct Case(convert_case::Case);

// This is used to check if the first string is "case" and then attempt conversion of the second string.
impl TryFrom<(String, String)> for Case {
type Error = ();
type Error = &'static str;

fn try_from(value: (String, String)) -> Result<Self, Self::Error> {
if value.0 == "case" {
value.1.try_into()
} else {
Err(())
Err("The first string is not \"case\"")
}
}
}

// Maps specific string values to their corresponding `convert_case::Case` variant.
impl TryFrom<String> for Case {
type Error = ();
type Error = &'static str;

fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self(match value.as_str() {
Expand All @@ -39,7 +39,7 @@ impl TryFrom<String> for Case {
"\"flat\"" => convert_case::Case::Flat,
"\"upper_flat\"" => convert_case::Case::UpperFlat,
"\"alternating\"" => convert_case::Case::Alternating,
_ => Err(())?,
_ => Err("Invalid case")?,
}))
}
}
Expand Down
Loading