use proc_macro2::{Ident, Span as Span2};
use quote::ToTokens;
use std::fmt::Display;
use std::str::FromStr;
use syn::ext::IdentExt;
use syn::spanned::Spanned;
use syn::{Attribute, Expr, ExprLit, Lit, MetaNameValue};
pub(crate) type SpannedResult<T> = Result<T, (Span2, String)>;
pub(crate) fn error<T>(span: &impl Spanned, message: impl Display) -> SpannedResult<T> {
Err((span.span(), message.to_string()))
}
pub(crate) fn take_path_attr(attrs: &mut Vec<Attribute>, name: &str) -> bool {
if let Some((i, _)) = attrs
.iter()
.enumerate()
.filter(|(_, a)| a.path().is_ident("boa"))
.filter_map(|(i, a)| a.meta.require_list().ok().map(|nv| (i, nv)))
.filter_map(|(i, m)| m.parse_args_with(Ident::parse_any).ok().map(|p| (i, p)))
.find(|(_, path)| path == name)
{
attrs.remove(i);
true
} else {
false
}
}
pub(crate) fn take_name_value_attr(attrs: &mut Vec<Attribute>, name: &str) -> Option<Lit> {
if let Some((i, lit)) = attrs
.iter()
.enumerate()
.filter(|(_, a)| a.meta.path().is_ident("boa"))
.filter_map(|(i, a)| a.meta.require_list().ok().map(|nv| (i, nv)))
.filter_map(|(i, a)| {
syn::parse2::<MetaNameValue>(a.tokens.to_token_stream())
.ok()
.map(|nv| (i, nv))
})
.filter(|(_, nv)| nv.path.is_ident(name))
.find_map(|(i, nv)| match &nv.value {
Expr::Lit(ExprLit { lit, .. }) => Some((i, lit.clone())),
_ => None,
})
{
attrs.remove(i);
Some(lit)
} else {
None
}
}
pub(crate) fn take_length_from_attrs(attrs: &mut Vec<Attribute>) -> SpannedResult<Option<usize>> {
match take_name_value_attr(attrs, "length") {
None => Ok(None),
Some(lit) => match lit {
Lit::Int(int) if int.base10_parse::<usize>().is_ok() => int
.base10_parse::<usize>()
.map(Some)
.map_err(|e| (int.span(), format!("Invalid literal: {e}"))),
l => error(&l, "Invalid literal type. Was expecting a number")?,
},
}
}
pub(crate) fn take_name_value_string(
attrs: &mut Vec<Attribute>,
name: &str,
) -> SpannedResult<Option<String>> {
match take_name_value_attr(attrs, name) {
None => Ok(None),
Some(lit) => match lit {
Lit::Str(s) => Ok(Some(s.value())),
l => Err((
l.span(),
"Invalid literal type. Was expecting a string".to_string(),
)),
},
}
}
pub(crate) fn take_error_from_attrs(attrs: &mut Vec<Attribute>) -> SpannedResult<Option<String>> {
take_name_value_string(attrs, "error")
}
#[derive(Copy, Clone, Debug, Default)]
pub(crate) enum RenameScheme {
#[default]
None,
CamelCase,
PascalCase,
}
impl FromStr for RenameScheme {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("none") {
Ok(Self::None)
} else if s.eq_ignore_ascii_case("camelcase") {
Ok(Self::CamelCase)
} else if s.eq_ignore_ascii_case("pascalcase") {
Ok(Self::PascalCase)
} else {
Err(format!(
r#"Invalid rename scheme: {s:?}. Accepted values are "none" or "camelCase"."#
))
}
}
}
impl RenameScheme {
pub(crate) fn from_named_attrs(
attrs: &mut Vec<Attribute>,
name: &str,
) -> SpannedResult<Option<Self>> {
match take_name_value_attr(attrs, name) {
None => Ok(None),
Some(Lit::Str(lit_str)) => Self::from_str(lit_str.value().as_str())
.map_err(|e| (lit_str.span(), e))
.map(Some),
Some(lit) => Err((
lit.span(),
"Invalid attribute value literal, expected a string.".to_string(),
)),
}
}
fn camel_case(s: &str) -> String {
#[derive(Debug, PartialEq)]
enum State {
First,
Middle,
NextOfUpper,
NextOfContinuedUpper(char),
NextOfSepMark,
Other,
}
let mut result = String::with_capacity(s.len());
let mut state = State::First;
for ch in s.chars() {
let is_upper = ch.is_ascii_uppercase();
let is_lower = ch.is_ascii_lowercase();
match (&state, is_upper, is_lower) {
(State::First | State::Middle, true, false) => {
state = State::NextOfUpper;
result.push(ch.to_ascii_lowercase());
}
(State::First | State::Middle, false, true) => {
state = State::Other;
result.push(ch);
}
(State::First, false, false) => {}
(State::NextOfUpper, true, false) => {
state = State::NextOfContinuedUpper(ch);
}
(State::NextOfUpper, false, true) => {
state = State::Middle;
result.push(ch);
}
(State::NextOfContinuedUpper(last), true, false) => {
result.push(last.to_ascii_lowercase());
state = State::NextOfContinuedUpper(ch);
}
(State::NextOfContinuedUpper(last), false, true) => {
result.push(last.to_ascii_uppercase());
state = State::Middle;
result.push(ch);
}
(State::NextOfContinuedUpper(last), false, false) => {
result.push(last.to_ascii_lowercase());
state = State::NextOfSepMark;
}
(State::NextOfSepMark, true, false) => {
state = State::NextOfUpper;
result.push(ch);
}
(State::NextOfSepMark, false, true) | (State::Other, true, false) => {
state = State::NextOfUpper;
result.push(ch.to_ascii_uppercase());
}
(State::Other, false, true) => {
result.push(ch);
}
(_, false, false) => {
state = State::NextOfSepMark;
}
(_, _, _) => {}
}
}
if let State::NextOfContinuedUpper(last) = state {
result.push(last.to_ascii_lowercase());
}
result
}
fn pascal_case(s: &str) -> String {
let mut result = Self::camel_case(s);
if let Some(ch) = result.get_mut(..1) {
ch.make_ascii_uppercase();
}
result
}
pub(crate) fn rename(self, s: String) -> String {
match self {
Self::None => s,
Self::CamelCase => Self::camel_case(&s),
Self::PascalCase => Self::pascal_case(&s),
}
}
}
#[cfg(test)]
mod tests {
use super::RenameScheme;
use test_case::test_case;
#[rustfmt::skip]
#[test_case("HelloWorld", "helloWorld" ; "camel_case_1")]
#[test_case("Hello_World", "helloWorld" ; "camel_case_2")]
#[test_case("hello_world", "helloWorld" ; "camel_case_3")]
#[test_case("__hello_world__", "helloWorld" ; "camel_case_4")]
#[test_case("HELLOWorld", "helloWorld" ; "camel_case_5")]
#[test_case("helloWORLD", "helloWorld" ; "camel_case_6")]
#[test_case("HELLO_WORLD", "helloWorld" ; "camel_case_7")]
#[test_case("hello_beautiful_world", "helloBeautifulWorld" ; "camel_case_8")]
#[test_case("helloBeautifulWorld", "helloBeautifulWorld" ; "camel_case_9")]
#[test_case("switch_to_term", "switchToTerm" ; "camel_case_10")]
#[test_case("_a_b_c_", "aBC" ; "camel_case_11")]
fn camel_case(input: &str, expected: &str) {
assert_eq!(RenameScheme::camel_case(input).as_str(), expected);
}
#[rustfmt::skip]
#[test_case("HelloWorld", "HelloWorld" ; "pascal_case_1")]
#[test_case("Hello_World", "HelloWorld" ; "pascal_case_2")]
#[test_case("hello_world", "HelloWorld" ; "pascal_case_3")]
#[test_case("__hello_world__", "HelloWorld" ; "pascal_case_4")]
#[test_case("HELLOWorld", "HelloWorld" ; "pascal_case_5")]
#[test_case("helloWORLD", "HelloWorld" ; "pascal_case_6")]
#[test_case("HELLO_WORLD", "HelloWorld" ; "pascal_case_7")]
fn pascal_case(input: &str, expected: &str) {
assert_eq!(RenameScheme::pascal_case(input).as_str(), expected);
}
}