mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(db): require explicit SQLite backend declarations
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
parent
336787a368
commit
fe440a6afd
Generated
+47
@@ -1005,6 +1005,7 @@ name = "buzz-db"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"buzz-core",
|
||||
"buzz-db-backend-macro",
|
||||
"chrono",
|
||||
"hex",
|
||||
"metrics",
|
||||
@@ -1021,6 +1022,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"
|
||||
@@ -3260,6 +3271,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"
|
||||
@@ -9490,6 +9507,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"
|
||||
@@ -9503,6 +9526,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"
|
||||
@@ -10180,6 +10212,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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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\"`",
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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) {}
|
||||
| ^^^^^^^^^^
|
||||
@@ -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() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: unsupported reason must not be empty
|
||||
--> tests/ui/empty_reason.rs:5:36
|
||||
|
|
||||
5 | #[sqlite_backend(unsupported = " ")]
|
||||
| ^^^^
|
||||
@@ -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() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: expected `implemented` or `unsupported = "reason"`
|
||||
--> tests/ui/malformed_marker.rs:5:22
|
||||
|
|
||||
5 | #[sqlite_backend(sometimes)]
|
||||
| ^^^^^^^^^
|
||||
@@ -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() {}
|
||||
@@ -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) {}
|
||||
| ^^^^^^^
|
||||
@@ -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() {}
|
||||
@@ -9,6 +9,7 @@ description = "Postgres event store and data access layer for Buzz"
|
||||
|
||||
[dependencies]
|
||||
buzz-core = { workspace = true }
|
||||
buzz-db-backend-macro = { path = "../buzz-db-backend-macro" }
|
||||
sqlx = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SQLite backend inventory
|
||||
|
||||
Generated from `#[sqlite_backend(...)]` declarations in `crates/buzz-db/src/lib.rs`.
|
||||
|
||||
> ⚠️ **PROVISIONAL — classification in progress.** Most declarations are mechanical placeholders and this table is not yet valid S6 input.
|
||||
|
||||
Regenerate after changing declarations with:
|
||||
|
||||
```sh
|
||||
cargo test -p buzz-db backend_inventory_is_current -- --nocapture
|
||||
```
|
||||
|
||||
| Method | SQLite status | Reason |
|
||||
| --- | --- | --- |
|
||||
| `usage_community_count` | unsupported | usage analytics is PostgreSQL-only |
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user