use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, punctuated::Punctuated, token::Comma, ItemFn, ItemStruct, Meta};
#[proc_macro_attribute]
pub fn crdt_schema(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);
let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
let mut version: Option<u32> = None;
let mut table: Option<String> = None;
let mut min_version: Option<u32> = None;
for meta in &args {
if let Meta::NameValue(nv) = meta {
let key = nv
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match key.as_str() {
"version" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = &nv.value
{
version = lit.base10_parse().ok();
}
}
"table" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = &nv.value
{
table = Some(lit.value());
}
}
"min_version" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = &nv.value
{
min_version = lit.base10_parse().ok();
}
}
_ => {
return syn::Error::new_spanned(&nv.path, format!("unknown attribute `{key}`"))
.to_compile_error()
.into();
}
}
}
}
let version = match version {
Some(v) => v,
None => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"missing required attribute `version`",
)
.to_compile_error()
.into();
}
};
let table = match table {
Some(t) => t,
None => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"missing required attribute `table`",
)
.to_compile_error()
.into();
}
};
let min_ver = min_version.unwrap_or(1);
let version_u8 = version as u8;
let struct_name = &input.ident;
let expanded = quote! {
#input
impl crdt_store::CrdtVersioned for #struct_name {
const SCHEMA_VERSION: u8 = #version_u8;
}
impl crdt_migrate::Schema for #struct_name {
const VERSION: u32 = #version;
const MIN_SUPPORTED_VERSION: u32 = #min_ver;
const NAMESPACE: &'static str = #table;
}
};
expanded.into()
}
#[proc_macro_attribute]
pub fn migration(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
let mut from_version: Option<u32> = None;
let mut to_version: Option<u32> = None;
for meta in &args {
if let Meta::NameValue(nv) = meta {
let key = nv
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match key.as_str() {
"from" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = &nv.value
{
from_version = lit.base10_parse().ok();
}
}
"to" => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = &nv.value
{
to_version = lit.base10_parse().ok();
}
}
_ => {
return syn::Error::new_spanned(&nv.path, format!("unknown attribute `{key}`"))
.to_compile_error()
.into();
}
}
}
}
let from_ver = match from_version {
Some(v) => v,
None => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"missing required attribute `from`",
)
.to_compile_error()
.into();
}
};
let to_ver = match to_version {
Some(v) => v,
None => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"missing required attribute `to`",
)
.to_compile_error()
.into();
}
};
let fn_name = &input.sig.ident;
let input_type = match input.sig.inputs.first() {
Some(syn::FnArg::Typed(pat_type)) => &pat_type.ty,
_ => {
return syn::Error::new_spanned(
&input.sig,
"migration function must take exactly one argument",
)
.to_compile_error()
.into();
}
};
let output_type = match &input.sig.output {
syn::ReturnType::Type(_, ty) => ty,
syn::ReturnType::Default => {
return syn::Error::new_spanned(
&input.sig,
"migration function must have a return type",
)
.to_compile_error()
.into();
}
};
let struct_name = {
let name = fn_name.to_string();
let pascal: String = name
.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect();
syn::Ident::new(&format!("{pascal}Migration"), fn_name.span())
};
let register_fn = syn::Ident::new(&format!("register_{fn_name}"), fn_name.span());
let expanded = quote! {
#input
pub struct #struct_name;
impl crdt_migrate::MigrationStep for #struct_name {
fn source_version(&self) -> u32 {
#from_ver
}
fn target_version(&self) -> u32 {
#to_ver
}
fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, crdt_migrate::MigrationError> {
let old: #input_type = postcard::from_bytes(data)
.map_err(|e| crdt_migrate::MigrationError::Deserialization(
e.to_string()
))?;
let new: #output_type = #fn_name(old);
postcard::to_allocvec(&new)
.map_err(|e| crdt_migrate::MigrationError::Serialization(
e.to_string()
))
}
}
pub fn #register_fn() -> Box<dyn crdt_migrate::MigrationStep> {
Box::new(#struct_name)
}
};
expanded.into()
}