blob: 5edf0efe687f4245baabd5d5bbbb7aa7b13bd721 [file] [log] [blame]
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<Ident>,
}
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<Self> {
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<Type> = None;
let mut try_into_type: Option<Type> = 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<Self> {
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<MigrationStateField>,
container_attrs: ContainerAttrs,
}
impl MigrationStateDerive {
fn parse(input: DeriveInput) -> Result<Self> {
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<Field, syn::token::Comma>,
) -> Result<Vec<MigrationStateField>> {
let processed = fields
.iter()
.map(|field| {
let attrs = FieldAttrs::parse(&field.attrs)?;
Ok((field, attrs))
})
.collect::<Result<Vec<_>>>()?
.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<TokenStream> {
let tokens = Self::parse(input)?.generate();
Ok(tokens)
}
}