feat(db): add backend declaration macro

Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
Brother Darryl
2026-08-10 14:24:17 -04:00
parent f4c51f87aa
commit e832df9178
14 changed files with 252 additions and 0 deletions

46
Cargo.lock generated
View File

@@ -1021,6 +1021,16 @@ dependencies = [
"uuid",
]
[[package]]
name = "buzz-db-backend-macro"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"trybuild",
]
[[package]]
name = "buzz-dev-mcp"
version = "0.1.0"
@@ -3259,6 +3269,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "glob"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]]
name = "globset"
version = "0.4.18"
@@ -9488,6 +9504,12 @@ dependencies = [
"xattr",
]
[[package]]
name = "target-triple"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -9501,6 +9523,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termina"
version = "0.3.3"
@@ -10178,6 +10209,21 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "trybuild"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml 1.1.2+spec-1.1.0",
]
[[package]]
name = "tungstenite"
version = "0.26.2"

View File

@@ -5,6 +5,7 @@ members = [
"crates/buzz-conformance",
"crates/buzz-push-gateway",
"crates/buzz-db",
"crates/buzz-db-backend-macro",
"crates/buzz-pubsub",
"crates/buzz-auth",
"crates/buzz-search",

View File

@@ -0,0 +1,19 @@
[package]
name = "buzz-db-backend-macro"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
trybuild = "1"

View File

@@ -0,0 +1,104 @@
//! Compile-time enforcement for explicit SQLite decisions on public `Db` methods.
//!
//! The impl-level attribute inspects only public associated methods. Private and
//! `pub(crate)` plumbing is intentionally outside this policy boundary.
use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{parse_macro_input, Attribute, ImplItem, ItemImpl, LitStr, Visibility};
#[proc_macro_attribute]
pub fn enforce_sqlite_backend_declarations(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut item = parse_macro_input!(input as ItemImpl);
let mut errors = Vec::new();
let mut inventory = Vec::new();
for impl_item in &mut item.items {
let ImplItem::Fn(method) = impl_item else {
continue;
};
if !matches!(method.vis, Visibility::Public(_)) {
continue;
}
let mut markers = Vec::new();
let mut retained = Vec::new();
for attr in method.attrs.drain(..) {
if attr.path().is_ident("sqlite_backend") {
markers.push(attr);
} else {
retained.push(attr);
}
}
method.attrs = retained;
let name = method.sig.ident.to_string();
match markers.as_slice() {
[] => errors.push(syn::Error::new(
method.sig.ident.span(),
format!("public Db method `{name}` is missing #[sqlite_backend(...)]"),
)),
[marker] => match parse_marker(marker) {
Ok(reason) => inventory.push((name, reason)),
Err(error) => errors.push(error),
},
_ => errors.push(syn::Error::new(
method.sig.ident.span(),
format!("public Db method `{name}` has duplicate #[sqlite_backend] markers"),
)),
}
}
inventory.sort_by(|a, b| a.0.cmp(&b.0));
let names = inventory
.iter()
.map(|(name, _)| LitStr::new(name, proc_macro2::Span::call_site()));
let reasons = inventory.iter().map(|(_, reason)| match reason {
Some(reason) => quote! { Some(#reason) },
None => quote! { None },
});
let errors = errors.into_iter().map(|e| e.to_compile_error());
quote! {
#(#errors)*
#item
#[doc(hidden)]
pub const SQLITE_BACKEND_INVENTORY: &[(&str, Option<&str>)] = &[#((#names, #reasons)),*];
}
.into()
}
fn parse_marker(attr: &Attribute) -> syn::Result<Option<LitStr>> {
let mut result = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("implemented") {
if result.is_some() {
return Err(meta.error("duplicate backend decision"));
}
result = Some(None);
return Ok(());
}
if meta.path.is_ident("unsupported") {
if result.is_some() {
return Err(meta.error("duplicate backend decision"));
}
let value = meta.value()?.parse::<LitStr>()?;
if value.value().trim().is_empty() {
return Err(syn::Error::new(
value.span(),
"unsupported reason must not be empty",
));
}
result = Some(Some(value));
return Ok(());
}
Err(meta.error("expected `implemented` or `unsupported = \"reason\"`"))
})?;
result.ok_or_else(|| {
syn::Error::new(
attr.span(),
"backend decision must be `implemented` or `unsupported = \"reason\"`",
)
})
}

View File

@@ -0,0 +1,9 @@
#[test]
fn ui() {
let tests = trybuild::TestCases::new();
tests.compile_fail("tests/ui/missing_marker.rs");
tests.compile_fail("tests/ui/duplicate_marker.rs");
tests.compile_fail("tests/ui/empty_reason.rs");
tests.compile_fail("tests/ui/malformed_marker.rs");
tests.pass("tests/ui/valid_markers.rs");
}

View File

@@ -0,0 +1,9 @@
use buzz_db_backend_macro::enforce_sqlite_backend_declarations;
struct Db;
#[enforce_sqlite_backend_declarations]
impl Db {
#[sqlite_backend(implemented)]
#[sqlite_backend(implemented)]
pub fn duplicated(&self) {}
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: public Db method `duplicated` has duplicate #[sqlite_backend] markers
--> tests/ui/duplicate_marker.rs:7:12
|
7 | pub fn duplicated(&self) {}
| ^^^^^^^^^^

View File

@@ -0,0 +1,8 @@
use buzz_db_backend_macro::enforce_sqlite_backend_declarations;
struct Db;
#[enforce_sqlite_backend_declarations]
impl Db {
#[sqlite_backend(unsupported = " ")]
pub fn empty(&self) {}
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: unsupported reason must not be empty
--> tests/ui/empty_reason.rs:5:36
|
5 | #[sqlite_backend(unsupported = " ")]
| ^^^^

View File

@@ -0,0 +1,8 @@
use buzz_db_backend_macro::enforce_sqlite_backend_declarations;
struct Db;
#[enforce_sqlite_backend_declarations]
impl Db {
#[sqlite_backend(sometimes)]
pub fn malformed(&self) {}
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: expected `implemented` or `unsupported = "reason"`
--> tests/ui/malformed_marker.rs:5:22
|
5 | #[sqlite_backend(sometimes)]
| ^^^^^^^^^

View File

@@ -0,0 +1,10 @@
use buzz_db_backend_macro::enforce_sqlite_backend_declarations;
struct Db;
#[enforce_sqlite_backend_declarations]
impl Db {
pub fn omitted(&self) {}
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: public Db method `omitted` is missing #[sqlite_backend(...)]
--> tests/ui/missing_marker.rs:7:12
|
7 | pub fn omitted(&self) {}
| ^^^^^^^

View File

@@ -0,0 +1,18 @@
use buzz_db_backend_macro::enforce_sqlite_backend_declarations;
struct Db;
#[enforce_sqlite_backend_declarations]
impl Db {
#[sqlite_backend(implemented)]
#[allow(dead_code)]
pub fn implemented(&self) {}
/// Deliberately unsupported operation.
#[sqlite_backend(unsupported = "requires postgres-only advisory locks")]
pub async fn unsupported(&self) {}
fn private(&self) {}
}
fn main() {}