use itertools::Itertools;
use proc_macro2::Span;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Attribute, Error, Expr, LitBool, LitStr, Meta, Token, Visibility,
};
mod kw {
use syn::custom_keyword;
custom_keyword!(vis);
custom_keyword!(rename_all);
custom_keyword!(rename);
custom_keyword!(default);
custom_keyword!(trim);
custom_keyword!(name);
}
#[derive(Clone, Debug)]
pub struct ConfigOption {
pub span: Span,
pub data: ConfigOptionData,
}
impl Parse for ConfigOption {
fn parse(input: ParseStream) -> syn::Result<Self> {
use ConfigOptionData as Data;
use ConfigOptionKind as Kind;
let span = input.span();
let kind = input.parse::<ConfigOptionKind>()?;
input.parse::<Token![=]>()?;
let data = match kind {
Kind::Vis => Data::Vis(input.parse()?),
Kind::RenameAll => Data::RenameAll(input.parse()?),
Kind::Rename => Data::Rename(input.parse()?),
Kind::Default => Data::Default(input.parse()?),
Kind::Trim => Data::Trim(input.parse()?),
};
Ok(Self { span, data })
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LitCase(convert_case::Case<'static>);
impl Parse for LitCase {
fn parse(input: ParseStream) -> syn::Result<Self> {
use convert_case::Case as C;
const SUPPORTED_CASES: [(&str, C); 8] = [
("lowercase", C::Lower),
("UPPERCASE", C::Upper),
("PascalCase", C::Pascal),
("camelCase", C::Camel),
("snake_case", C::Snake),
("SCREAMING_SNAKE_CASE", C::UpperSnake),
("kebab-case", C::Kebab),
("SCREAMING-KEBAB-CASE", C::UpperKebab),
];
let arg = input.parse::<LitStr>()?;
let Some(case) = SUPPORTED_CASES
.into_iter()
.find_map(|(name, case)| (name == arg.value()).then_some(case))
else {
let options = SUPPORTED_CASES.map(|(name, _)| name).join(", ");
Err(Error::new(
arg.span(),
format!("Case must be one of {options}."),
))?
};
Ok(Self(case))
}
}
impl LitCase {
pub fn value(&self) -> convert_case::Case<'static> {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, strum::EnumDiscriminants)]
#[strum_discriminants(
vis(pub(self)),
name(ConfigOptionKind),
derive(strum::Display, Hash),
strum(serialize_all = "snake_case")
)]
pub enum ConfigOptionData {
Vis(Visibility),
RenameAll(LitCase),
Rename(LitStr),
Default(Expr),
Trim(LitBool),
}
impl Parse for ConfigOptionKind {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
let ty = if lookahead.peek(kw::vis) {
input.parse::<kw::vis>()?;
Self::Vis
} else if lookahead.peek(kw::rename_all) {
input.parse::<kw::rename_all>()?;
Self::RenameAll
} else if lookahead.peek(kw::rename) {
input.parse::<kw::rename>()?;
Self::Rename
} else if lookahead.peek(kw::default) {
input.parse::<kw::default>()?;
Self::Default
} else if lookahead.peek(kw::trim) {
input.parse::<kw::trim>()?;
Self::Trim
} else if lookahead.peek(kw::name) {
Err(Error::new(
input.span(),
"`name` has been removed; use `rename` instead",
))?
} else {
Err(lookahead.error())?
};
Ok(ty)
}
}
pub fn ensure_unique_options(opts: &[ConfigOption]) -> syn::Result<()> {
for (kind, opts) in opts
.iter()
.into_group_map_by(|opt| ConfigOptionKind::from(&opt.data))
.into_iter()
{
match &opts[..] {
[] => unreachable!(), [_unique] => continue,
[first, rest @ ..] => {
let initial_error = Error::new(
first.span,
format!("Option {kind} can only be declaration once"),
);
let final_error = rest.iter().fold(initial_error, |mut err, opt| {
err.combine(Error::new(opt.span, "Duplicate declaration here"));
err
});
Err(final_error)?
}
}
}
Ok(())
}
pub fn get_customisations_from_attrs<T>(attrs: &[Attribute], attr_name: &str) -> syn::Result<T>
where
T: TryFrom<Vec<ConfigOption>, Error = syn::Error>,
{
let options = attrs
.iter()
.filter(|attr| attr.path().is_ident(attr_name))
.map(|attr| match &attr.meta {
Meta::List(attr_inner) => {
attr_inner.parse_args_with(Punctuated::<ConfigOption, Token![,]>::parse_terminated)
}
other_form => Err(syn::Error::new(
other_form.span(),
format!("{attr_name} is not list-like. Expecting `{attr_name}(...)`"),
)),
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
ensure_unique_options(&options)?;
options.try_into()
}