use crate::crate_paths::{get_linkme_crate, get_reinhardt_migrations_crate};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
Ident, LitStr, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
struct CollectMigrationsInput {
app_label: String,
migrations: Vec<Ident>,
}
impl Parse for CollectMigrationsInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let app_label_ident: Ident = input.parse()?;
if app_label_ident != "app_label" {
return Err(syn::Error::new(
app_label_ident.span(),
"expected `app_label`",
));
}
let _eq: Token![=] = input.parse()?;
let app_label_lit: LitStr = input.parse()?;
let app_label = app_label_lit.value();
let _comma: Token![,] = input.parse()?;
let migrations: Punctuated<Ident, Token![,]> = Punctuated::parse_terminated(input)?;
let migrations: Vec<Ident> = migrations.into_iter().collect();
if migrations.is_empty() {
return Err(syn::Error::new(
input.span(),
"at least one migration module is required",
));
}
Ok(CollectMigrationsInput {
app_label,
migrations,
})
}
}
pub(crate) fn collect_migrations_impl(input: TokenStream) -> Result<TokenStream, syn::Error> {
let migrations_crate = get_reinhardt_migrations_crate();
let linkme = get_linkme_crate();
let input: CollectMigrationsInput = syn::parse2(input)?;
let app_label = &input.app_label;
let migrations = &input.migrations;
let struct_name = format_ident!("{}Migrations", to_pascal_case(app_label));
let static_name = format_ident!("__{}_MIGRATIONS_PROVIDER", app_label.to_uppercase());
let migration_calls = migrations.iter().map(|m| {
quote! { #m::migration() }
});
let expanded = quote! {
pub struct #struct_name;
impl #migrations_crate::MigrationProvider for #struct_name {
fn migrations() -> Vec<#migrations_crate::Migration> {
vec![
#(#migration_calls),*
]
}
}
impl #struct_name {
pub fn all() -> Vec<#migrations_crate::Migration> {
<Self as #migrations_crate::MigrationProvider>::migrations()
}
}
#[#linkme::distributed_slice(#migrations_crate::registry::global::MIGRATION_PROVIDERS)]
static #static_name: #migrations_crate::registry::global::MigrationProvider =
<#struct_name as #migrations_crate::MigrationProvider>::migrations;
};
Ok(expanded)
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("polls"), "Polls");
assert_eq!(to_pascal_case("user_profile"), "UserProfile");
assert_eq!(to_pascal_case("my-app"), "MyApp");
assert_eq!(to_pascal_case("UPPER"), "UPPER");
assert_eq!(to_pascal_case("camelCase"), "CamelCase");
}
}