use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
braced,
parse::{Parse, ParseStream},
parse_macro_input, Error, LitStr, Token, Type,
};
mod kw {
syn::custom_keyword!(Current);
syn::custom_keyword!(version_field);
syn::custom_keyword!(steps);
}
struct StepDef {
step_ty: Type,
older_ty: Type,
}
impl Parse for StepDef {
fn parse(input: ParseStream) -> syn::Result<Self> {
let step_ty: Type = input.parse()?;
input.parse::<Token![:]>()?;
let older_ty: Type = input.parse()?;
input.parse::<Token![=>]>()?;
let _newer_ty: Type = input.parse()?;
Ok(StepDef { step_ty, older_ty })
}
}
struct MigrationChainInput {
current: Type,
version_field: LitStr,
steps: Vec<StepDef>,
}
impl Parse for MigrationChainInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<Token![type]>()?;
input.parse::<kw::Current>()?;
input.parse::<Token![=]>()?;
let current: Type = input.parse()?;
input.parse::<Token![;]>()?;
input.parse::<kw::version_field>()?;
input.parse::<Token![=]>()?;
let version_field: LitStr = input.parse()?;
input.parse::<Token![;]>()?;
input.parse::<kw::steps>()?;
let content;
braced!(content in input);
let mut steps = Vec::new();
while !content.is_empty() {
steps.push(content.parse::<StepDef>()?);
if content.is_empty() {
break;
}
content.parse::<Token![,]>()?;
}
if steps.is_empty() {
return Err(Error::new(
proc_macro2::Span::call_site(),
"migration_chain! requires at least one step",
));
}
Ok(MigrationChainInput {
current,
version_field,
steps,
})
}
}
pub fn migration_chain(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as MigrationChainInput);
expand(parsed).into()
}
fn expand(input: MigrationChainInput) -> TokenStream2 {
let MigrationChainInput {
current,
version_field,
steps,
} = input;
let n = steps.len();
let mut asserts = Vec::new();
for (i, step) in steps.iter().enumerate() {
let step_ty = &step.step_ty;
asserts.push(quote! {
assert!(
<#step_ty as ::aimdb_data_contracts::MigrationStep>::TO_VERSION
== <#step_ty as ::aimdb_data_contracts::MigrationStep>::FROM_VERSION + 1,
"migration step must increment version by exactly 1"
);
});
if i + 1 < n {
let next_step_ty = &steps[i + 1].step_ty;
asserts.push(quote! {
assert!(
<#step_ty as ::aimdb_data_contracts::MigrationStep>::TO_VERSION
== <#next_step_ty as ::aimdb_data_contracts::MigrationStep>::FROM_VERSION,
"migration steps must be sequential"
);
});
}
}
let first_step_ty = &steps[0].step_ty;
asserts.push(quote! {
assert!(
<#first_step_ty as ::aimdb_data_contracts::MigrationStep>::FROM_VERSION == 1,
"first migration step must start at version 1"
);
});
let last_step_ty = &steps[n - 1].step_ty;
asserts.push(quote! {
assert!(
<#last_step_ty as ::aimdb_data_contracts::MigrationStep>::TO_VERSION
== <#current as ::aimdb_data_contracts::SchemaType>::VERSION,
"last migration step must end at current VERSION"
);
});
let mut up_fns = Vec::new();
let mut down_fns = Vec::new();
for (i, step) in steps.iter().enumerate() {
let k = i + 1;
let step_ty = &step.step_ty;
let older_ty = &step.older_ty;
let up_fn = format_ident!("__up_{}", k);
let up_body = if i + 1 == n {
quote! { <#step_ty as ::aimdb_data_contracts::MigrationStep>::up(older) }
} else {
let next_up_fn = format_ident!("__up_{}", k + 1);
quote! {
let newer = <#step_ty as ::aimdb_data_contracts::MigrationStep>::up(older)?;
#next_up_fn(newer)
}
};
up_fns.push(quote! {
fn #up_fn(older: #older_ty) -> Result<#current, ::aimdb_data_contracts::MigrationError> {
#up_body
}
});
let down_fn = format_ident!("__down_{}", k);
let down_body = if i + 1 == n {
quote! { <#step_ty as ::aimdb_data_contracts::MigrationStep>::down(current) }
} else {
let next_down_fn = format_ident!("__down_{}", k + 1);
quote! {
let newer = #next_down_fn(current)?;
<#step_ty as ::aimdb_data_contracts::MigrationStep>::down(newer)
}
};
down_fns.push(quote! {
fn #down_fn(current: #current) -> Result<#older_ty, ::aimdb_data_contracts::MigrationError> {
#down_body
}
});
}
let mut from_arms = Vec::new();
let mut to_arms = Vec::new();
for (i, step) in steps.iter().enumerate() {
let k = i + 1;
let k_lit = k as u32;
let older_ty = &step.older_ty;
let up_fn = format_ident!("__up_{}", k);
let down_fn = format_ident!("__down_{}", k);
from_arms.push(quote! {
#k_lit => {
let older: #older_ty = ::aimdb_data_contracts::__private::serde_json::from_slice(data).map_err(|_| {
::aimdb_data_contracts::MigrationError::DeserializationFailed(concat!(
"failed to parse as ", stringify!(#older_ty)
))
})?;
#up_fn(older)
}
});
to_arms.push(quote! {
#k_lit => {
let older = #down_fn(self.clone())?;
::aimdb_data_contracts::__private::serde_json::to_vec(&older).map_err(|_| {
::aimdb_data_contracts::MigrationError::SerializationFailed(concat!(
"failed to serialize as ", stringify!(#older_ty)
))
})
}
});
}
quote! {
const _: () = {
#(#asserts)*
#(#up_fns)*
#(#down_fns)*
impl ::aimdb_data_contracts::MigrationChain for #current {
const MIN_VERSION: u32 = 1;
fn migrate_from_bytes(data: &[u8]) -> Result<Self, ::aimdb_data_contracts::MigrationError> {
#[derive(::serde::Deserialize)]
struct __VersionProbe {
#[serde(rename = #version_field)]
version: u32,
}
let version = match ::aimdb_data_contracts::__private::serde_json::from_slice::<__VersionProbe>(data) {
Ok(probe) => probe.version,
Err(e) if e.is_data() => {
return Err(::aimdb_data_contracts::MigrationError::MissingVersion);
}
Err(_) => {
return Err(::aimdb_data_contracts::MigrationError::DeserializationFailed(
"invalid JSON",
));
}
};
if version > <#current as ::aimdb_data_contracts::SchemaType>::VERSION {
return Err(::aimdb_data_contracts::MigrationError::VersionTooNew {
source: version,
current: <#current as ::aimdb_data_contracts::SchemaType>::VERSION,
});
}
match version {
#(#from_arms)*
v if v == <#current as ::aimdb_data_contracts::SchemaType>::VERSION => {
::aimdb_data_contracts::__private::serde_json::from_slice(data).map_err(|_| {
::aimdb_data_contracts::MigrationError::DeserializationFailed(concat!(
"failed to parse as ", stringify!(#current)
))
})
}
_ => Err(::aimdb_data_contracts::MigrationError::VersionTooOld {
target: version,
minimum: Self::MIN_VERSION,
}),
}
}
fn migrate_to_version(
&self,
target_version: u32,
) -> Result<::aimdb_data_contracts::__private::alloc::vec::Vec<u8>, ::aimdb_data_contracts::MigrationError> {
if target_version < Self::MIN_VERSION {
return Err(::aimdb_data_contracts::MigrationError::VersionTooOld {
target: target_version,
minimum: Self::MIN_VERSION,
});
}
if target_version > <#current as ::aimdb_data_contracts::SchemaType>::VERSION {
return Err(::aimdb_data_contracts::MigrationError::VersionTooNew {
source: target_version,
current: <#current as ::aimdb_data_contracts::SchemaType>::VERSION,
});
}
match target_version {
#(#to_arms)*
v if v == <#current as ::aimdb_data_contracts::SchemaType>::VERSION => {
::aimdb_data_contracts::__private::serde_json::to_vec(self).map_err(|_| {
::aimdb_data_contracts::MigrationError::SerializationFailed(concat!(
"failed to serialize as ", stringify!(#current)
))
})
}
_ => unreachable!(),
}
}
}
};
}
}