use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DeriveInput, Fields};
pub fn expand_tokens(input: DeriveInput) -> TokenStream2 {
match expand_checked(&input) {
Ok(tokens) => tokens,
Err(error) => error.to_compile_error(),
}
}
struct Choice {
ident: syn::Ident,
value: String,
label: String,
}
fn expand_checked(input: &DeriveInput) -> syn::Result<TokenStream2> {
if !input.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&input.generics,
"#[derive(Options)] does not support generic or lifetime parameters",
));
}
let Data::Enum(data) = &input.data else {
return Err(syn::Error::new_spanned(
&input.ident,
"#[derive(Options)] supports an enum of unit variants",
));
};
if data.variants.is_empty() {
return Err(syn::Error::new_spanned(
&input.ident,
"#[derive(Options)] needs at least one variant",
));
}
let mut choices = Vec::with_capacity(data.variants.len());
for variant in &data.variants {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new_spanned(
variant,
"#[derive(Options)] supports unit variants only: an option is one stored value",
));
}
let name = variant.ident.to_string();
let mut value = snake_case(&name);
let mut label = sentence_case(&value);
for attr in &variant.attrs {
if !attr.path().is_ident("option") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("value") {
value = meta.value()?.parse::<syn::LitStr>()?.value();
} else if meta.path.is_ident("label") {
label = meta.value()?.parse::<syn::LitStr>()?.value();
} else {
return Err(meta.error(
"unknown `#[option(..)]` key: expected `value = \"…\"` or `label = \"…\"`",
));
}
Ok(())
})?;
}
if let Some(earlier) = choices.iter().find(|c: &&Choice| c.value == value) {
return Err(syn::Error::new_spanned(
variant,
format!(
"`{}` and `{}` both store \"{value}\": each option needs a distinct value",
earlier.ident, variant.ident
),
));
}
choices.push(Choice {
ident: variant.ident.clone(),
value,
label,
});
}
let krate = crate::tablo_core_path(&input.ident, "Options")?;
let ident = &input.ident;
let idents: Vec<&syn::Ident> = choices.iter().map(|c| &c.ident).collect();
let values: Vec<&str> = choices.iter().map(|c| c.value.as_str()).collect();
let labels: Vec<&str> = choices.iter().map(|c| c.label.as_str()).collect();
Ok(quote! {
impl #krate::__macro::Options for #ident {
fn options() -> ::std::vec::Vec<(::std::string::String, ::std::string::String)> {
::std::vec![
#((
::std::string::String::from(#values),
::std::string::String::from(#labels),
)),*
]
}
}
impl #ident {
pub const fn value(&self) -> &'static str {
match self {
#(Self::#idents => #values,)*
}
}
pub const fn label(&self) -> &'static str {
match self {
#(Self::#idents => #labels,)*
}
}
pub fn from_value(value: &str) -> ::std::option::Option<Self> {
match value {
#(#values => ::std::option::Option::Some(Self::#idents),)*
_ => ::std::option::Option::None,
}
}
}
})
}
fn snake_case(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 4);
for (index, ch) in name.trim_start_matches("r#").chars().enumerate() {
if ch.is_uppercase() {
if index > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
fn sentence_case(value: &str) -> String {
let spaced = value.replace('_', " ");
let mut chars = spaced.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::{sentence_case, snake_case};
#[test]
fn a_variant_stores_its_snake_case_name_and_reads_in_sentence_case() {
assert_eq!(snake_case("Draft"), "draft");
assert_eq!(snake_case("PublishedLate"), "published_late");
assert_eq!(sentence_case("published_late"), "Published late");
}
}