use crate::util::to_snake_case;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::parse::Parser;
use syn::spanned::Spanned;
use syn::{Fields, ItemStruct, Path};
struct DatabaseArgs {
entities: Vec<Path>,
daos: Vec<Path>,
version: u32,
}
fn parse_args(args: TokenStream, span: proc_macro2::Span) -> syn::Result<DatabaseArgs> {
let mut entities = Vec::new();
let mut daos = Vec::new();
let mut version: Option<u32> = None;
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("entities") {
meta.parse_nested_meta(|inner| {
entities.push(inner.path.clone());
Ok(())
})
} else if meta.path.is_ident("daos") {
meta.parse_nested_meta(|inner| {
daos.push(inner.path.clone());
Ok(())
})
} else if meta.path.is_ident("version") {
let lit: syn::LitInt = meta.value()?.parse()?;
version = Some(lit.base10_parse()?);
Ok(())
} else {
Err(meta.error("알 수 없는 database 인자 — entities/daos/version 만 지원"))
}
});
parser.parse2(args)?;
if entities.is_empty() {
return Err(syn::Error::new(
span,
"entities(...)에 엔티티를 1개 이상 지정해야 합니다",
));
}
let entity_keys: Vec<String> = entities
.iter()
.map(|entity| entity.to_token_stream().to_string())
.collect();
for (index, entity) in entities.iter().enumerate() {
if entity_keys[..index].contains(&entity_keys[index]) {
return Err(syn::Error::new(
entity.span(),
"entities(...)에 같은 엔티티를 중복 지정할 수 없습니다",
));
}
}
let Some(version) = version else {
return Err(syn::Error::new(span, "version = N 이 필요합니다"));
};
if version == 0 {
return Err(syn::Error::new(
span,
"version은 1 이상이어야 합니다 (0 = 신규 DB 마커)",
));
}
Ok(DatabaseArgs {
entities,
daos,
version,
})
}
struct SnapshotMeta {
snapshot_hash: TokenStream,
embedded_entries: Vec<TokenStream>,
dep_consts: Vec<TokenStream>,
file_seen: bool,
}
fn scan_snapshots(
db_snake: &str,
version: u32,
span: proc_macro2::Span,
) -> syn::Result<SnapshotMeta> {
let mut meta = SnapshotMeta {
snapshot_hash: quote! { None },
embedded_entries: Vec::new(),
dep_consts: Vec::new(),
file_seen: false,
};
let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
syn::Error::new(
span,
"CARGO_MANIFEST_DIR 없음 — #[database]는 cargo 빌드에서만 사용할 수 있습니다",
)
})?;
let dir = roomrs_migrate::resolve_schema_dir(&manifest);
let files = roomrs_migrate::list_snapshot_versions(&dir, db_snake).map_err(|e| {
syn::Error::new(
span,
format!("스냅샷 디렉토리 읽기 실패: {} — {e}", dir.display()),
)
})?;
for (ver, path) in &files {
if *ver > version {
return Err(syn::Error::new(
span,
format!(
"스냅샷 버전이 database version보다 큽니다: {} (version = {version})",
path.display()
),
));
}
let raw = std::fs::read(path).map_err(|e| {
syn::Error::new(
span,
format!("스냅샷 파일 읽기 실패: {} — {e}", path.display()),
)
})?;
let snap = roomrs_migrate::SchemaSnapshot::from_slice(&raw).map_err(|e| {
syn::Error::new(
span,
format!(
"스냅샷 파일 파손: {} — 파스 실패: {e} (명세 §7.4)",
path.display()
),
)
})?;
if snap.version != *ver {
return Err(syn::Error::new(
span,
format!(
"스냅샷 내부 version({})이 파일명 버전({ver})과 다릅니다: {}",
snap.version,
path.display()
),
));
}
if *ver == version {
let h = snap.hash();
meta.snapshot_hash = quote! { Some(#h) };
meta.file_seen = true;
}
let path_str = path.to_string_lossy().replace('\\', "/");
meta.dep_consts
.push(quote! { const _: &[u8] = ::core::include_bytes!(#path_str); });
let compressed = roomrs_migrate::compress_snapshot(&raw);
let bytes = proc_macro2::Literal::byte_string(&compressed);
let v = *ver;
meta.embedded_entries
.push(quote! { ::roomrs::EmbeddedSchema { version: #v, compressed: #bytes } });
}
Ok(meta)
}
pub fn expand(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let item: ItemStruct = syn::parse2(input)?;
let args = parse_args(args, item.span())?;
if !matches!(item.fields, Fields::Unit) {
return Err(syn::Error::new(
item.span(),
"#[database]는 유닛 구조체에만 사용할 수 있습니다: struct AppDb;",
));
}
let db_ident = item.ident.clone();
let vis = item.vis.clone();
let attrs: Vec<&syn::Attribute> = item.attrs.iter().collect();
let sync_ident = format_ident!("{}Sync", db_ident);
let tx_ext_ident = format_ident!("{}TxDaos", db_ident);
let version = args.version;
let entities = &args.entities;
let async_ident = format_ident!("{}Async", db_ident);
let db_snake = to_snake_case(&db_ident.to_string());
let snap_meta = scan_snapshots(&db_snake, version, item.span())?;
let snapshot_hash = snap_meta.snapshot_hash;
let embedded_entries = snap_meta.embedded_entries;
let dep_consts = snap_meta.dep_consts;
let file_seen = snap_meta.file_seen;
let export_fn = format_ident!("__roomrs_schema_export_{}", db_snake);
let mut sync_accessors: Vec<TokenStream> = Vec::new();
let mut async_accessors: Vec<TokenStream> = Vec::new();
let mut tx_decls: Vec<TokenStream> = Vec::new();
let mut tx_impls: Vec<TokenStream> = Vec::new();
for dao in &args.daos {
let dao_name = dao
.segments
.last()
.ok_or_else(|| syn::Error::new(dao.span(), "빈 DAO 경로"))?
.ident
.clone();
let method = format_ident!("{}", to_snake_case(&dao_name.to_string()));
let on_ident = {
let mut p = dao.clone();
let last = p.segments.last_mut().expect("위에서 검증");
last.ident = format_ident!("{}On", last.ident);
p
};
let async_on_ident = {
let mut p = dao.clone();
let last = p.segments.last_mut().expect("위에서 검증");
last.ident = format_ident!("{}AsyncOn", last.ident);
p
};
sync_accessors.push(quote! {
#vis fn #method(&self) -> #on_ident<::roomrs::SyncHandle<'_>> {
#on_ident::__new(self.h)
}
});
async_accessors.push(quote! {
#vis fn #method(&self) -> #async_on_ident {
#async_on_ident::__new(self.h.clone())
}
});
tx_decls.push(quote! {
fn #method(&self) -> #on_ident<&Self>;
});
tx_impls.push(quote! {
fn #method(&self) -> #on_ident<&Self> {
#on_ident::__new(self)
}
});
}
Ok(quote! {
#(#attrs)*
#vis struct #db_ident {
inner: ::roomrs::Database,
}
#(#dep_consts)*
#[cfg(test)]
#[test]
fn #export_fn() {
::roomrs::export_schema_for_test::<#db_ident>(::core::env!("CARGO_MANIFEST_DIR"))
.unwrap();
}
impl ::roomrs::DatabaseSpec for #db_ident {
const VERSION: u32 = #version;
const DB_NAME: &'static str = #db_snake;
const SNAPSHOT_HASH: Option<u64> = #snapshot_hash;
const SNAPSHOT_FILE_SEEN: bool = #file_seen;
const EMBEDDED_SCHEMAS: &'static [::roomrs::EmbeddedSchema] = &[
#(#embedded_entries,)*
];
fn schema() -> ::roomrs::SchemaDef {
let mut ddl: Vec<&'static str> = Vec::new();
#(ddl.extend_from_slice(<#entities as ::roomrs::Entity>::DDL);)*
let tables = vec![
#(::roomrs::TableMeta {
name: <#entities as ::roomrs::Entity>::TABLE,
columns: <#entities as ::roomrs::Entity>::COLUMNS_META,
ddl: <#entities as ::roomrs::Entity>::DDL,
},)*
];
::roomrs::SchemaDef { version: #version, ddl, tables }
}
fn from_database(db: ::roomrs::Database) -> Self {
Self { inner: db }
}
}
impl #db_ident {
#vis fn builder() -> ::roomrs::DatabaseBuilder<#db_ident> {
::core::default::Default::default()
}
#vis fn run_sync(&self) -> #sync_ident<'_> {
#sync_ident { h: self.inner.run_sync() }
}
}
#vis struct #sync_ident<'a> {
h: ::roomrs::SyncHandle<'a>,
}
impl<'a> ::core::ops::Deref for #sync_ident<'a> {
type Target = ::roomrs::SyncHandle<'a>;
fn deref(&self) -> &Self::Target {
&self.h
}
}
impl #sync_ident<'_> {
#(#sync_accessors)*
}
impl ::roomrs::Execute for #sync_ident<'_> {
type Out<R: Send + 'static> = ::roomrs::Result<R>;
fn run_all<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<Vec<T>> {
::roomrs::Execute::run_all(self.h, sql, params)
}
fn run_optional<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<Option<T>> {
::roomrs::Execute::run_optional(self.h, sql, params)
}
fn run_one<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<T> {
::roomrs::Execute::run_one(self.h, sql, params)
}
fn run_scalar(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<i64> {
::roomrs::Execute::run_scalar(self.h, sql, params)
}
fn fail<R: Send + 'static>(e: ::roomrs::Error) -> Self::Out<R> {
Err(e)
}
}
#vis trait #tx_ext_ident {
#(#tx_decls)*
}
impl #tx_ext_ident for ::roomrs::Tx<'_> {
#(#tx_impls)*
}
::roomrs::__if_async! {
impl #db_ident {
#vis fn run_async(&self) -> #async_ident {
#async_ident { h: ::roomrs::AsyncHandle::from_database(&self.inner) }
}
}
#vis struct #async_ident {
h: ::roomrs::AsyncHandle,
}
impl ::core::ops::Deref for #async_ident {
type Target = ::roomrs::AsyncHandle;
fn deref(&self) -> &Self::Target {
&self.h
}
}
impl #async_ident {
#(#async_accessors)*
}
impl ::roomrs::Execute for #async_ident {
type Out<R: Send + 'static> = ::std::pin::Pin<
Box<dyn ::core::future::Future<Output = ::roomrs::Result<R>> + Send + 'static>,
>;
fn run_all<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<Vec<T>> {
::roomrs::Execute::run_all(&self.h, sql, params)
}
fn run_optional<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<Option<T>> {
::roomrs::Execute::run_optional(&self.h, sql, params)
}
fn run_one<T: ::roomrs::FromRow + Send + 'static>(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<T> {
::roomrs::Execute::run_one(&self.h, sql, params)
}
fn run_scalar(
self,
sql: String,
params: Vec<::roomrs::rusqlite::types::Value>,
) -> Self::Out<i64> {
::roomrs::Execute::run_scalar(&self.h, sql, params)
}
fn fail<R: Send + 'static>(e: ::roomrs::Error) -> Self::Out<R> {
Box::pin(async move { Err(e) })
}
}
}
})
}