use convert_case::{Case, Casing};
use proc_macro::{Span, TokenStream};
use proc_macro_error::abort;
use quote::ToTokens;
use syn::{Item, Type};
fn generate_from_fields<'r, I>(fields: I) -> (Vec<String>, Vec<String>)
where
I: Iterator<Item = (&'r Type, String, bool)>,
{
fields.map(|(ty, name, convert_case)| {
let pascal_name = if convert_case {name.to_case(Case::Pascal)} else {name.clone()};
(format!(r#"updates.append(&mut <{field_type} as mutable::Mutable>::cmp(&self.{field_name}, &new_value.{field_name}).into_iter().map(Self::Mutation::{mutation_name}).collect())"#,
field_type = ty.to_token_stream(),
field_name = name,
mutation_name = pascal_name),
format!(r#"{}(<{} as mutable::Mutable>::Mutation)"#,
pascal_name,
ty.to_token_stream()
))
}).collect::<Vec<(String, String)>>().into_iter().unzip()
}
pub(crate) fn try_(item: TokenStream) -> Result<TokenStream, ()> {
let item =
syn::parse::<Item>(item).map_err(|_| abort!(Span::call_site(), "Could not parse item"))?;
match item {
Item::Struct(struct_) => {
let (mutation_checks, mutation_variants) = generate_from_fields(
struct_
.fields
.iter()
.map(|f| (&f.ty, f.ident.to_token_stream().to_string(), true)),
);
let mutable_impl = format!(
r#"
#[derive(std::fmt::Debug, std::cmp::PartialEq, Clone)]
{vis} enum {struct_name}Mutation{{
{mutation_variants}
}}
impl mutable::Mutable for {struct_name}{{
type Mutation={struct_name}Mutation;
fn cmp(&self, new_value: &Self) -> Vec<Self::Mutation>{{
let mut updates: Vec<Self::Mutation> = Vec::new();
{mutation_checks};
updates
}}
}}"#,
struct_name = struct_.ident,
mutation_variants = mutation_variants.join(",\n"),
mutation_checks = mutation_checks.join(";\n"),
vis = struct_.vis.to_token_stream()
);
Ok(mutable_impl.parse().unwrap())
}
Item::Enum(enum_) => {
let enum_name = enum_.ident;
let (mutations, arms): (Vec<String>, Vec<String>) = {
enum_
.variants
.iter()
.map(|v| {
let variant_name = v.ident.to_string();
if v.fields.is_empty() {
("".into(), format!("({enum_name}::{variant_name}, {enum_name}::{variant_name}) => {{}}"))
} else {
let named = v.fields.iter().all(|f| f.ident.is_some());
let (checks, names, variants): (Vec<String>, Vec<String>, Vec<String>) = v.fields.iter().enumerate().map(|(i, f)| {
let (name, pascal_name) = if named {
(f.ident.as_ref().unwrap().to_string(), f.ident.as_ref().unwrap().to_string().to_case(Case::Pascal))
} else {
(format!("{i}"), format!("{i}"))
};
let mutation_name = format!("Variant{variant_name}Field{pascal_name}");
(
format!(
r#"updates.append(&mut <{field_type} as mutable::Mutable>::cmp(&old_{name}, &new_{name}).into_iter().map({enum_name}Mutation::{mutation_name}).collect())"#,
field_type = f.ty.to_token_stream()
),
name,
format!(r#"{mutation_name}(<{} as mutable::Mutable>::Mutation)"#, f.ty.to_token_stream())
)
}).collect::<Vec<(String, String, String)>>().into_iter().fold((vec![], vec![], vec![]), |acc, val| {
([acc.0, vec![val.0]].concat(), [acc.1, vec![val.1]].concat(), [acc.2, vec![val.2]].concat())
});
(variants.join(",\n"),
format!("({enum_name}::{variant_name}{o}{}{c}, {enum_name}::{variant_name}{o}{}{c}) => {{
{checks}
}}",
names.iter().map(|n| if named {format!("{n}: old_{n}")} else {format!("old_{n}")}).collect::<Vec<_>>().join(","),
names.iter().map(|n| if named {format!("{n}: new_{n}")} else {format!("new_{n}")}).collect::<Vec<_>>().join(","),
o = if named { "{" }else {"("},
c = if named {"}"} else {")"},
checks = checks.join(";\n")
))
}
})
.collect::<Vec<(String, String)>>().into_iter().unzip()
};
let mutable_impl = format!(
r#"
#[derive(std::fmt::Debug, std::cmp::PartialEq, Clone)]
{vis} enum {enum_name}Mutation{{
NewVariant({enum_name}),
{mutations}
}}
impl mutable::Mutable for {enum_name}{{
type Mutation={enum_name}Mutation;
fn cmp(&self, new_value: &Self) -> Vec<Self::Mutation>{{
let mut updates: Vec<Self::Mutation> = Vec::new();
match (self, new_value) {{
{arms},
(_, n) => updates.push(Self::Mutation::NewVariant(n.clone())),
}}
updates
}}
}}"#,
mutations = mutations.into_iter().filter(|v| !v.is_empty()).collect::<Vec<_>>().join(",\n"),
arms = arms.join(",\n"),
vis = enum_.vis.to_token_stream()
);
Ok(mutable_impl.parse().unwrap())
}
_ => abort!(item, "mutable may only be derived on structures"),
}
}