use proc_macro::TokenStream;
use quote::quote;
use unsynn::{LiteralString, Parse, ToTokens, TokenIter};
#[proc_macro_attribute]
pub fn migration(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr2: proc_macro2::TokenStream = attr.into();
let mut tokens = TokenIter::new(attr2);
let explicit_version = LiteralString::parse(&mut tokens).ok();
let item: proc_macro2::TokenStream = item.into();
let item_str = item.to_string();
let fn_name = match extract_fn_name(&item_str) {
Some(name) => name,
None => {
return quote! { compile_error!("expected function"); }.into();
}
};
let fn_ident = quote::format_ident!("{}", fn_name);
let version_expr = if let Some(version) = explicit_version {
let version_lit = version.to_token_stream();
quote! { #version_lit }
} else {
quote! {
{
const FILE: &str = file!();
const fn find_last_slash(s: &[u8]) -> usize {
let mut i = s.len();
while i > 0 {
i -= 1;
if s[i] == b'/' || s[i] == b'\\' {
return i + 1;
}
}
0
}
const SLASH_POS: usize = find_last_slash(FILE.as_bytes());
const FILENAME: &str = unsafe {
std::str::from_utf8_unchecked(FILE.as_bytes().split_at(SLASH_POS).1)
};
::dibs::__derive_migration_version(FILENAME)
}
}
};
quote! {
#item
::dibs::inventory::submit! {
::dibs::Migration {
version: #version_expr,
name: stringify!(#fn_ident),
run: |ctx| Box::pin(#fn_ident(ctx)),
source_file: (env!("CARGO_MANIFEST_DIR"), file!()),
}
}
}
.into()
}
fn extract_fn_name(s: &str) -> Option<&str> {
let idx = s.find("fn ")?;
let rest = &s[idx + 3..].trim_start();
let end = rest.find(|c: char| !c.is_alphanumeric() && c != '_')?;
Some(&rest[..end])
}