use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{
Attribute, FnArg, GenericArgument, Ident, Path, PathArguments, Token, Type, TypeArray,
TypePath, TypeReference, TypeTuple, parenthesized,
parse::Parse,
punctuated::Punctuated,
spanned::Spanned,
token::{Comma, Paren},
};
#[derive(Debug, Clone)]
pub(crate) struct Variant {
pub(crate) attrs: Vec<Attribute>,
pub(crate) into: Vec<Type>,
pub(crate) ident: Ident,
pub(crate) ty: Type,
}
impl Variant {
pub(crate) fn enum_variant_tokens(&self) -> TokenStream {
let attrs = &self.attrs;
let ty = &self.ty;
let ident = &self.ident;
quote! {
#(#attrs)*
#ident(#ty)
}
}
pub(crate) fn try_from_arm_tokens(&self, other: &Self, wrapper: &Ident) -> TokenStream {
let ident = &self.ident;
if self.ident == other.ident {
quote! { #wrapper::#ident(value) => Ok(value), }
} else if self.into.contains(&other.ty) {
quote! { #wrapper::#ident(value) => Ok(value.into()),}
} else {
let message = format!(
"No conversion from '{}' to {}",
self.type_to_string(),
other.type_to_string()
);
quote! { #wrapper::#ident(_) => Err(#message), }
}
}
#[allow(clippy::match_wildcard_for_single_variants)]
pub(crate) fn fn_call_arm_tokens(
&self,
wrapper: &Ident,
function: &Ident,
inputs: &Punctuated<FnArg, Comma>,
) -> TokenStream {
let ident = &self.ident;
let args = inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Typed(typed) => Some(&typed.pat),
_ => None,
})
.collect::<Punctuated<_, Comma>>();
quote! { #wrapper::#ident(value) => value.#function(#args), }
}
pub(crate) fn type_as_str_arm_tokens(&self, wrapper: &Ident) -> TokenStream {
let type_string = self.type_to_string();
let ident = &self.ident;
quote! {
#wrapper::#ident(_) => #type_string,
}
}
pub(crate) fn is_type_arm_tokens(&self, wrapper: &Ident, ty: &Type) -> TokenStream {
let ident = &self.ident;
if &self.ty == ty {
quote! { #wrapper::#ident(_) => true, }
} else {
quote! {}
}
}
pub(crate) fn as_type_arm_tokens(&self, wrapper: &Ident, ty: &Type) -> TokenStream {
let ident = &self.ident;
if &self.ty == ty {
quote! { #wrapper::#ident(value) => Some(value), }
} else if self.into.contains(ty) {
quote! { #wrapper::#ident(value) => Some(value.into()),}
} else {
quote! {}
}
}
pub(crate) fn as_ref_arm_tokens(&self, wrapper: &Ident, ty: &Type) -> TokenStream {
let ident = &self.ident;
if &self.ty == ty {
quote! { #wrapper::#ident(value) => Some(value), }
} else {
quote! {}
}
}
pub(crate) fn as_mut_arm_tokens(&self, wrapper: &Ident, ty: &Type) -> TokenStream {
let ident = &self.ident;
if &self.ty == ty {
quote! { #wrapper::#ident(value) => Some(value), }
} else {
quote! {}
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn vec_methods_tokens(&self, enum_ident: &Ident, vec_field: &Ident) -> TokenStream {
let ident = &self.ident;
let ty = &self.ty;
let snake = self.ident_to_snake();
let type_name = self.type_to_string();
let fn_first = Ident::new(&format!("first_{snake}"), ty.span());
let fn_first_doc = format!("Returns the first `{ident}` as `Option<&{type_name}>`.");
let fn_first_mut = Ident::new(&format!("first_{snake}_mut"), ty.span());
let fn_first_mut_doc =
format!("Returns the first `{ident}` as `Option<&mut {type_name}>`.");
let fn_last = Ident::new(&format!("last_{snake}"), ty.span());
let fn_last_doc = format!("Returns the last `{ident}` as `Option<&{type_name}>`.");
let fn_last_mut = Ident::new(&format!("last_{snake}_mut"), ty.span());
let fn_last_mut_doc = format!("Returns the last `{ident}` as `Option<&mut {type_name}>`.");
let fn_iter = Ident::new(&format!("iter_{snake}"), ty.span());
let fn_iter_doc = format!("Returns an iterator over `{ident}` as `&{type_name}`.");
let fn_iter_mut = Ident::new(&format!("iter_{snake}_mut"), ty.span());
let fn_iter_mut_doc =
format!("Returns a mutable iterator over `{ident}` as `&mut {type_name}`.");
let fn_enumerate = Ident::new(&format!("enumerate_{snake}"), ty.span());
let fn_enumerate_doc =
format!("Returns an iterator over `{ident}` as (index, `&{type_name}`).");
let fn_enumerate_mut = Ident::new(&format!("enumerate_{snake}_mut"), ty.span());
let fn_enumerate_mut_doc =
format!("Returns a mutable iterator over `{ident}` as (index, `&mut {type_name}`).");
let fn_count = Ident::new(&format!("count_{snake}"), ty.span());
let fn_count_doc = format!("Counts the number of `{ident}` variants in `{enum_ident}`.");
let fn_all = Ident::new(&format!("all_{snake}"), ty.span());
let fn_all_doc =
format!("Returns true if all variants are `{ident}` variants in `{enum_ident}`.");
let fn_any = Ident::new(&format!("any_{snake}"), ty.span());
let fn_any_doc = format!("Returns true there is a `{ident}` variants in `{enum_ident}`.");
quote! {
#[doc = #fn_first_doc]
pub fn #fn_first(&self) -> ::core::option::Option<&#ty> {
self.#vec_field.iter().find_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_first_mut_doc]
pub fn #fn_first_mut(&mut self) -> ::core::option::Option<&mut #ty> {
self.#vec_field.iter_mut().find_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_last_doc]
pub fn #fn_last(&self) -> ::core::option::Option<&#ty> {
self.#vec_field.iter().rev().find_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_last_mut_doc]
pub fn #fn_last_mut(&mut self) -> ::core::option::Option<&mut #ty> {
self.#vec_field.iter_mut().rev().find_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_iter_doc]
pub fn #fn_iter(&self) -> impl ::core::iter::Iterator<Item = &#ty> {
self.#vec_field.iter().filter_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_iter_mut_doc]
pub fn #fn_iter_mut(&mut self) -> impl ::core::iter::Iterator<Item = &mut #ty> {
self.#vec_field.iter_mut().filter_map(|item| {
if let #enum_ident::#ident(value) = item {
Some(value)
} else {
None
}
})
}
#[doc = #fn_enumerate_doc]
pub fn #fn_enumerate(&self) -> impl ::core::iter::Iterator<Item = (usize, &#ty)> {
self.#vec_field.iter().enumerate().filter_map(|(i, item)| {
if let #enum_ident::#ident(value) = item {
Some((i, value))
} else {
None
}
})
}
#[doc = #fn_enumerate_mut_doc]
pub fn #fn_enumerate_mut(&mut self) -> impl ::core::iter::Iterator<Item = (usize, &mut #ty)> {
self.#vec_field.iter_mut().enumerate().filter_map(|(i, item)| {
if let #enum_ident::#ident(value) = item {
Some((i, value))
} else {
None
}
})
}
#[doc = #fn_count_doc]
pub fn #fn_count(&self) -> usize {
self.#vec_field.iter().filter(|item| matches!(item, #enum_ident::#ident(_))).count()
}
#[doc = #fn_all_doc]
pub fn #fn_all(&self) -> bool {
self.#vec_field.iter().all(|item| ::std::matches!(item, #enum_ident::#ident(_)))
}
#[doc = #fn_any_doc]
pub fn #fn_any(&self) -> bool {
self.#vec_field.iter().any(|item| ::std::matches!(item, #enum_ident::#ident(_)))
}
}
}
pub(crate) fn type_to_string(&self) -> String {
self.ty
.clone()
.into_token_stream()
.to_string()
.replace("& ", "&")
.replace("& '", "&'")
.replace(" < ", "<")
.replace(" > ", ">")
.replace(" >", ">")
}
pub(crate) fn ident_to_snake(&self) -> String {
camel_to_snake(&self.ident.to_string())
}
}
impl Parse for Variant {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let ty = input.parse::<Type>()?;
let (ident, ty) = if input.peek(Paren) {
let content;
parenthesized!(content in input);
(ident_from_type(&ty)?, content.parse::<Type>()?)
} else {
(ident_from_type(&ty)?, ty)
};
let (into, other_attrs): (Vec<_>, Vec<_>) = attrs
.into_iter()
.partition(|attr| attr.path().is_ident("into"));
let into_types = into
.into_iter()
.flat_map(|attr| {
attr.parse_args_with(Punctuated::<Type, Token![,]>::parse_terminated)
.map(|p| p.into_iter().collect::<Vec<_>>())
.unwrap_or_default()
})
.collect::<Vec<_>>();
Ok(Self {
attrs: other_attrs,
into: into_types,
ident,
ty,
})
}
}
pub(crate) fn camel_to_snake(camel: &str) -> String {
let mut snake = String::new();
let mut first = true;
for c in camel.chars() {
if c.is_uppercase() {
if !first {
snake.push('_');
}
snake.push_str(&c.to_lowercase().to_string());
} else {
snake.push(c);
}
first = false;
}
snake
}
fn camel_case_ident(path: &Path, extension: &str) -> Ident {
let idents = path
.segments
.iter()
.map(|segment| {
let ident = segment.ident.to_string();
let extra_idents = match &segment.arguments {
PathArguments::AngleBracketed(args) => args
.args
.iter()
.filter_map(|arg| match arg {
GenericArgument::Type(ty) => {
ident_from_type(ty).ok().map(|i| i.to_string())
}
GenericArgument::AssocType(assoc) => {
ident_from_type(&assoc.ty).ok().map(|i| i.to_string())
}
_ => None,
})
.collect::<String>(),
_ => String::new(),
};
let mut chars = ident.chars();
chars
.next()
.map(|first| {
format!(
"{}{}{extra_idents}{extension}",
first.to_uppercase(),
chars.as_str()
)
})
.unwrap_or_default()
})
.collect::<Vec<_>>();
Ident::new(&idents.join(""), path.span())
}
fn camel_case_tokens<T: ToTokens>(tokens: T) -> String {
tokens
.to_token_stream()
.to_string()
.split_whitespace()
.map(|word| {
let filtered = word
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>();
let mut chars = filtered.chars();
chars
.next()
.map(|first| format!("{}{}", first.to_uppercase(), chars.as_str()))
.unwrap_or_default()
})
.collect::<String>()
}
fn extract_path(ty: &Type) -> Option<&syn::Path> {
match ty {
Type::Path(TypePath { path, .. }) => Some(path),
Type::Reference(TypeReference { elem, .. }) | Type::Array(TypeArray { elem, .. }) => {
extract_path(elem)
}
_ => None,
}
}
fn ident_from_type(ty: &Type) -> syn::Result<Ident> {
match ty {
Type::Path(TypePath { path, .. }) => Ok(camel_case_ident(path, "")),
Type::Reference(TypeReference { elem, .. }) => extract_path(elem)
.map(|path| camel_case_ident(path, "Ref"))
.ok_or_else(|| syn::Error::new(ty.span(), "Unsupported reference type")),
Type::Array(TypeArray { elem, len, .. }) => {
let ext = format!("Array{}", camel_case_tokens(len));
extract_path(elem)
.map(|path| camel_case_ident(path, &ext))
.ok_or_else(|| syn::Error::new(ty.span(), "Unsupported array type"))
}
Type::Tuple(TypeTuple { elems, .. }) => {
let ident = elems
.iter()
.map(|t| extract_path(t).map(|p| camel_case_ident(p, "").to_string()))
.collect::<Option<Vec<_>>>()
.map(|mut names| {
names.push("Tuple".to_string());
Ident::new(&names.join(""), elems.span())
});
ident.ok_or_else(|| syn::Error::new(ty.span(), "Unsupported tuple type"))
}
_ => Err(syn::Error::new(
ty.span(),
"Unsupported type for variant identifier",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_str;
#[test]
fn test_camel_to_snake() {
assert_eq!(camel_to_snake("MyVariant"), "my_variant");
assert_eq!(camel_to_snake("HTTPResponse"), "h_t_t_p_response");
assert_eq!(camel_to_snake("lowercase"), "lowercase");
}
#[test]
fn test_variant_parsing() {
let input = parse_str::<Variant>("#[into(i32, f32)] String").unwrap();
assert_eq!(input.ident.to_string(), "String");
assert_eq!(input.into.len(), 2);
assert_eq!(input.type_to_string(), "String");
let input = parse_str::<Variant>("#[into(i32)] i32 (u32)").unwrap();
assert_eq!(input.ident.to_string(), "I32");
assert_eq!(input.type_to_string(), "u32");
assert_eq!(input.into.len(), 1);
}
#[test]
fn test_ident_from_type() {
let ty: Type = parse_str("std::string::String").unwrap();
let ident = ident_from_type(&ty).unwrap();
assert_eq!(ident.to_string(), "StdStringString");
let ty: Type = parse_str("&str").unwrap();
let ident = ident_from_type(&ty).unwrap();
assert_eq!(ident.to_string(), "StrRef");
let ty: Type = parse_str("[i32; 4]").unwrap();
let ident = ident_from_type(&ty).unwrap();
assert_eq!(ident.to_string(), "I32Array4");
let ty: Type = parse_str("(i32, String)").unwrap();
let ident = ident_from_type(&ty).unwrap();
assert_eq!(ident.to_string(), "I32StringTuple");
}
#[test]
fn test_type_to_string() {
let variant = Variant {
attrs: vec![],
into: vec![],
ident: Ident::new("Test", proc_macro2::Span::call_site()),
ty: parse_str::<Type>("&str").unwrap(),
};
assert_eq!(variant.type_to_string(), "&str");
let variant = Variant {
attrs: vec![],
into: vec![],
ident: Ident::new("Test", proc_macro2::Span::call_site()),
ty: parse_str::<Type>("Vec<i32>").unwrap(),
};
assert_eq!(variant.type_to_string(), "Vec<i32>");
}
}