Browse Source
Add a macro that recursively builds the "migrated" version of a struct. Reviewed-by: Zhao Liu <zhao1.liu@intel.com> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>pull/305/head
8 changed files with 512 additions and 5 deletions
@ -0,0 +1,298 @@ |
|||
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) |
|||
} |
|||
} |
|||
Loading…
Reference in new issue