use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::Result;
use super::TernDeriveAttr;
use crate::internal::ast::{Container, SkipParseAttr};
use crate::internal::parse;
pub type MigrationSourceContainer<'a> =
Container<'a, TernDeriveAttr, SkipParseAttr>;
impl<'a> MigrationSourceContainer<'a> {
pub fn new(input: &'a syn::DeriveInput) -> Result<Self> {
Container::from_ast(input)
}
pub fn quote_impl_migration_source(&self) -> Result<TokenStream> {
let ident = &self.ty.ident;
let source = &self.attrs.source;
let migration_set = MigrationSetContainer::new(ident, source)?;
let quote_migration_source_impl =
migration_set.quote_migration_source_impl();
let quote_migration_mods = migration_set.quote_migration_modules();
let quote_migration_impls = migration_set.quote_migration_impls();
let quote_impl_migration_source = quote! {
#quote_migration_source_impl
#quote_migration_mods
#[doc(hidden)]
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const _: () = {
#quote_migration_impls
};
};
Ok(quote_impl_migration_source)
}
}
struct MigrationSetContainer {
ident: syn::Ident,
migrations: Vec<MigrationContainer>,
}
enum MigrationContainer {
Sql(SqlSourceContainer),
Rs(RustSourceContainer),
}
struct SqlSourceContainer {
module: syn::Ident,
version: syn::LitInt,
description: syn::LitStr,
content: syn::LitStr,
no_tx: syn::LitBool,
}
struct RustSourceContainer {
module: syn::Ident,
version: syn::LitInt,
description: syn::LitStr,
content: syn::LitStr,
}
impl MigrationSetContainer {
fn new(ident: &syn::Ident, source: &Option<syn::LitStr>) -> Result<Self> {
let src = source.as_ref().map(|s| s.value()).ok_or_else(|| {
syn::Error::new(
ident.span(),
"missing required `source` attribute containing the path to the migration files",
)
})?;
let migration_dir = parse::cargo_manifest_dir().join(src);
let migrations =
parse::MigrationSource::from_migration_dir(migration_dir)
.map_err(|e| {
syn::Error::new(
ident.span(),
format!("error with migration source: {e:?}"),
)
})?
.into_iter()
.map(MigrationContainer::from)
.collect::<Vec<_>>();
Ok(Self { ident: ident.clone(), migrations })
}
fn quote_migration_source_impl(&self) -> TokenStream {
let ctx = &self.ident;
let boxed_migrations = self.quote_boxed_qualified_migration_types();
quote! {
#[automatically_derived]
impl ::tern::migration::MigrationSource for #ctx {
type Ctx = #ctx;
fn migration_set(
&self,
last_applied: Option<i64>,
) -> ::tern::migration::MigrationSet<Self::Ctx>
{
let all: Vec<Box<dyn ::tern::migration::Migration<Ctx = Self::Ctx>>> = vec![#(#boxed_migrations),*];
let Some(v) = last_applied else {
return ::tern::migration::MigrationSet::new(all);
};
let migrations: Vec<Box<dyn ::tern::migration::Migration<Ctx = Self::Ctx>>> = all
.into_iter()
.skip_while(|m| m.as_ref().version() <= v)
.collect::<Vec<_>>();
::tern::migration::MigrationSet::new(migrations)
}
}
}
}
fn quote_migration_modules(&self) -> TokenStream {
let ctx = &self.ident;
self.migrations.iter().fold(quote! {}, |acc, src| {
let quote_migration_mod = src.quote_migration_module(ctx);
quote! {
#acc
#quote_migration_mod
}
})
}
fn quote_migration_impls(&self) -> TokenStream {
let ctx = &self.ident;
self.migrations.iter().fold(quote! {}, |acc, src| {
let quote_impl_migration = src.quote_impl_migration(ctx);
quote! {
#acc
#quote_impl_migration
}
})
}
fn quote_boxed_qualified_migration_types(&self) -> Vec<TokenStream> {
self.migrations
.iter()
.map(|s| s.quote_boxed_qualified_migration_type())
.collect::<Vec<_>>()
}
}
impl MigrationContainer {
fn quote_boxed_qualified_migration_type(&self) -> TokenStream {
let module = self.module();
quote! {Box::new(#module::TernMigration)}
}
fn quote_migration_module(&self, ctx: &syn::Ident) -> TokenStream {
let module = self.module();
match self {
Self::Sql(s) => {
let quote_impl_query_builder = s.quote_impl_query_builder(ctx);
quote! {
mod #module {
use super::#ctx;
#[derive(Debug, Clone)]
pub struct TernMigration;
#quote_impl_query_builder
}
}
},
Self::Rs(_) => {
quote! {
mod #module;
}
},
}
}
fn quote_impl_migration(&self, ctx: &syn::Ident) -> TokenStream {
let module = self.module();
let quote_common = self.quote_common_migration_fns();
let no_tx_body = match self {
Self::Sql(s) => {
let no_tx = &s.no_tx;
quote! { #no_tx }
},
_ => quote! { self.no_tx() },
};
quote! {
impl ::tern::migration::Migration for #module::TernMigration {
type Ctx = #ctx;
#quote_common
fn no_tx(&self) -> bool {
#no_tx_body
}
}
}
}
fn quote_common_migration_fns(&self) -> TokenStream {
let description = self.description();
let version = self.version();
let content = self.content();
quote! {
fn migration_id(&self) -> ::tern::migration::MigrationId {
let description = #description.to_string();
::tern::migration::MigrationId::new(#version, description)
}
fn content(&self) -> String {
#content.to_string()
}
fn build<'a>(
&'a self,
ctx: &'a mut Self::Ctx,
) -> ::tern::future::BoxFuture<'a, ::tern::error::TernResult<::tern::migration::Query>>
{
Box::pin(<Self as ::tern::migration::QueryBuilder>::build(self, ctx))
}
}
}
fn module(&self) -> &syn::Ident {
match self {
Self::Sql(s) => &s.module,
Self::Rs(s) => &s.module,
}
}
fn version(&self) -> &syn::LitInt {
match self {
Self::Sql(s) => &s.version,
Self::Rs(s) => &s.version,
}
}
fn description(&self) -> &syn::LitStr {
match self {
Self::Sql(s) => &s.description,
Self::Rs(s) => &s.description,
}
}
fn content(&self) -> &syn::LitStr {
match self {
Self::Sql(s) => &s.content,
Self::Rs(s) => &s.content,
}
}
}
impl SqlSourceContainer {
fn quote_impl_query_builder(&self, ctx: &syn::Ident) -> TokenStream {
let content = &self.content;
quote! {
#[automatically_derived]
impl ::tern::migration::QueryBuilder for TernMigration {
type Ctx = #ctx;
async fn build(
&self,
ctx: &mut Self::Ctx,
) -> ::tern::error::TernResult<::tern::migration::Query>
{
let sql = #content.to_string();
Ok(::tern::migration::Query::new(sql))
}
}
}
}
}
impl From<parse::SqlSource> for SqlSourceContainer {
fn from(value: parse::SqlSource) -> Self {
Self {
module: syn::Ident::new(&value.module, Span::call_site()),
version: syn::LitInt::new(
&format!("{}", value.version),
Span::call_site(),
),
description: syn::LitStr::new(
&value.description,
Span::call_site(),
),
content: syn::LitStr::new(&value.content, Span::call_site()),
no_tx: syn::LitBool::new(value.no_tx, Span::call_site()),
}
}
}
impl From<parse::RustSource> for RustSourceContainer {
fn from(value: parse::RustSource) -> Self {
Self {
module: syn::Ident::new(&value.module, Span::call_site()),
version: syn::LitInt::new(
&format!("{}", value.version),
Span::call_site(),
),
description: syn::LitStr::new(
&value.description,
Span::call_site(),
),
content: syn::LitStr::new(&value.content, Span::call_site()),
}
}
}
impl From<parse::MigrationSource> for MigrationContainer {
fn from(value: parse::MigrationSource) -> Self {
match value {
parse::MigrationSource::Sql(s) => {
Self::Sql(SqlSourceContainer::from(s))
},
parse::MigrationSource::Rs(s) => {
Self::Rs(RustSourceContainer::from(s))
},
}
}
}