use std::borrow::Cow; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::{spanned::Spanned, DeriveInput, Error, Field, Ident, Result, Type}; use crate::get_fields; #[derive(Debug, Default)] enum ConversionMode { #[default] None, Omit, Into(Type), TryInto(Type), ToMigrationState, } impl ConversionMode { fn target_type(&self, original_type: &Type) -> TokenStream { match self { ConversionMode::Into(ty) | ConversionMode::TryInto(ty) => ty.to_token_stream(), ConversionMode::ToMigrationState => { quote! { <#original_type as ToMigrationState>::Migrated } } _ => original_type.to_token_stream(), } } } #[derive(Debug, Default)] struct ContainerAttrs { rename: Option, } impl ContainerAttrs { fn parse_from(&mut self, attrs: &[syn::Attribute]) -> Result<()> { use attrs::{set, with, Attrs}; Attrs::new() .once("rename", with::eq(set::parse(&mut self.rename))) .parse_attrs("migration_state", attrs)?; Ok(()) } fn parse(attrs: &[syn::Attribute]) -> Result { let mut container_attrs = Self::default(); container_attrs.parse_from(attrs)?; Ok(container_attrs) } } #[derive(Debug, Default)] struct FieldAttrs { conversion: ConversionMode, clone: bool, } impl FieldAttrs { fn parse_from(&mut self, attrs: &[syn::Attribute]) -> Result<()> { let mut omit_flag = false; let mut into_type: Option = None; let mut try_into_type: Option = None; use attrs::{set, with, Attrs}; Attrs::new() .once("omit", set::flag(&mut omit_flag)) .once("into", with::paren(set::parse(&mut into_type))) .once("try_into", with::paren(set::parse(&mut try_into_type))) .once("clone", set::flag(&mut self.clone)) .parse_attrs("migration_state", attrs)?; self.conversion = match (omit_flag, into_type, try_into_type, self.clone) { // Valid combinations of attributes first... (true, None, None, false) => ConversionMode::Omit, (false, Some(ty), None, _) => ConversionMode::Into(ty), (false, None, Some(ty), _) => ConversionMode::TryInto(ty), (false, None, None, true) => ConversionMode::None, // clone without conversion (false, None, None, false) => ConversionMode::ToMigrationState, // default behavior // ... then the error cases (true, _, _, _) => { return Err(Error::new( attrs[0].span(), "ToMigrationState: omit cannot be used with other attributes", )); } (_, Some(_), Some(_), _) => { return Err(Error::new( attrs[0].span(), "ToMigrationState: into and try_into attributes cannot be used together", )); } }; Ok(()) } fn parse(attrs: &[syn::Attribute]) -> Result { let mut field_attrs = Self::default(); field_attrs.parse_from(attrs)?; Ok(field_attrs) } } #[derive(Debug)] struct MigrationStateField { name: Ident, original_type: Type, attrs: FieldAttrs, } impl MigrationStateField { fn maybe_clone(&self, mut value: TokenStream) -> TokenStream { if self.attrs.clone { value = quote! { #value.clone() }; } value } fn generate_migration_state_field(&self) -> TokenStream { let name = &self.name; let field_type = self.attrs.conversion.target_type(&self.original_type); quote! { pub #name: #field_type, } } fn generate_snapshot_field(&self) -> TokenStream { let name = &self.name; let value = self.maybe_clone(quote! { self.#name }); match &self.attrs.conversion { ConversionMode::Omit => { unreachable!("Omitted fields are filtered out during processing") } ConversionMode::None => quote! { target.#name = #value; }, ConversionMode::Into(_) => quote! { target.#name = #value.into(); }, ConversionMode::TryInto(_) => quote! { target.#name = #value.try_into().map_err(|_| migration::InvalidError)?; }, ConversionMode::ToMigrationState => quote! { self.#name.snapshot_migration_state(&mut target.#name)?; }, } } fn generate_restore_field(&self) -> TokenStream { let name = &self.name; match &self.attrs.conversion { ConversionMode::Omit => { unreachable!("Omitted fields are filtered out during processing") } ConversionMode::None => quote! { self.#name = #name; }, ConversionMode::Into(_) => quote! { self.#name = #name.into(); }, ConversionMode::TryInto(_) => quote! { self.#name = #name.try_into().map_err(|_| migration::InvalidError)?; }, ConversionMode::ToMigrationState => quote! { self.#name.restore_migrated_state_mut(#name, _version_id)?; }, } } } #[derive(Debug)] pub struct MigrationStateDerive { input: DeriveInput, fields: Vec, container_attrs: ContainerAttrs, } impl MigrationStateDerive { fn parse(input: DeriveInput) -> Result { let container_attrs = ContainerAttrs::parse(&input.attrs)?; let fields = get_fields(&input, "ToMigrationState")?; let fields = Self::process_fields(fields)?; Ok(Self { input, fields, container_attrs, }) } fn process_fields( fields: &syn::punctuated::Punctuated, ) -> Result> { let processed = fields .iter() .map(|field| { let attrs = FieldAttrs::parse(&field.attrs)?; Ok((field, attrs)) }) .collect::>>()? .into_iter() .filter(|(_, attrs)| !matches!(attrs.conversion, ConversionMode::Omit)) .map(|(field, attrs)| MigrationStateField { name: field.ident.as_ref().unwrap().clone(), original_type: field.ty.clone(), attrs, }) .collect(); Ok(processed) } fn migration_state_name(&self) -> Cow<'_, Ident> { match &self.container_attrs.rename { Some(rename) => Cow::Borrowed(rename), None => Cow::Owned(format_ident!("{}Migration", &self.input.ident)), } } fn generate_migration_state_struct(&self) -> TokenStream { let name = self.migration_state_name(); let fields = self .fields .iter() .map(MigrationStateField::generate_migration_state_field); quote! { #[derive(Default)] pub struct #name { #(#fields)* } } } fn generate_snapshot_migration_state(&self) -> TokenStream { let fields = self .fields .iter() .map(MigrationStateField::generate_snapshot_field); quote! { fn snapshot_migration_state(&self, target: &mut Self::Migrated) -> Result<(), migration::InvalidError> { #(#fields)* Ok(()) } } } fn generate_restore_migrated_state(&self) -> TokenStream { let names: Vec<_> = self.fields.iter().map(|f| &f.name).collect(); let fields = self .fields .iter() .map(MigrationStateField::generate_restore_field); // version_id could be used or not depending on conversion attributes quote! { #[allow(clippy::used_underscore_binding)] fn restore_migrated_state_mut(&mut self, source: Self::Migrated, _version_id: u8) -> Result<(), migration::InvalidError> { let Self::Migrated { #(#names),* } = source; #(#fields)* Ok(()) } } } fn generate(&self) -> TokenStream { let struct_name = &self.input.ident; let generics = &self.input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let name = self.migration_state_name(); let migration_state_struct = self.generate_migration_state_struct(); let snapshot_impl = self.generate_snapshot_migration_state(); let restore_impl = self.generate_restore_migrated_state(); quote! { #migration_state_struct impl #impl_generics ToMigrationState for #struct_name #ty_generics #where_clause { type Migrated = #name; #snapshot_impl #restore_impl } } } pub fn expand(input: DeriveInput) -> Result { let tokens = Self::parse(input)?.generate(); Ok(tokens) } }