diff --git a/Cargo.lock b/Cargo.lock index 8d0effd..bfc4501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,26 +117,39 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "bytes", + "cfg_aliases", +] + [[package]] name = "buffa" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941c714734a660caa93a210c531dec553cc0ef417890409914d24032c94ab840" +checksum = "ec95898e2ef31d6266042f21c398fa5ff8334d14d6b2ac5fb38b55448d94f595" dependencies = [ "base64", "bytes", + "compact_str", + "ecow", "hashbrown 0.15.5", "once_cell", "serde", "serde_json", + "smol_str", "thiserror 2.0.18", ] [[package]] name = "buffa-build" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96988700c7be5e4dd1dd22ee5daecd13090b24ec62003ee7fd4a9d7fef50a7c" +checksum = "c26e1e2ec3ddd680a25e60687e0dca72fc2326f01d2ff26acbd8c266a3664693" dependencies = [ "buffa", "buffa-codegen", @@ -145,9 +158,9 @@ dependencies = [ [[package]] name = "buffa-codegen" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693ea91be01336fb0084b2e7bb071be7393224c72f737c846471e4013e58c327" +checksum = "855364959e603cad456c1656bcf57f431ae6e5b4d4e4f166f7d594df882b6218" dependencies = [ "buffa", "buffa-descriptor", @@ -160,9 +173,9 @@ dependencies = [ [[package]] name = "buffa-descriptor" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8df83adfcb525621f5a96c88e610f90aff1112ec57522d9b48a943df327394" +checksum = "f74c0bf128c53f22b0fc3c1e8e66969cd21375879061293fd4e5298e839099a9" dependencies = [ "buffa", "serde", @@ -190,6 +203,15 @@ dependencies = [ "serde", ] +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.62" @@ -222,6 +244,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -243,6 +271,21 @@ dependencies = [ "phf", ] +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -264,9 +307,9 @@ checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "connectrpc" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3382e121377f6c160bbbe56f5809637ccbabff7edbf56bb8e96cc64e65998045" +checksum = "003fac88ace432fd8e6adbe13da8b20e90613b89e70c72186c505ad1bea307b2" dependencies = [ "async-compression", "async-trait", @@ -306,6 +349,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ecow" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1010,6 +1062,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1095,6 +1153,22 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.117" diff --git a/crates/protoc-gen-protovalidate-buffa/Cargo.toml b/crates/protoc-gen-protovalidate-buffa/Cargo.toml index 859ea45..a069943 100644 --- a/crates/protoc-gen-protovalidate-buffa/Cargo.toml +++ b/crates/protoc-gen-protovalidate-buffa/Cargo.toml @@ -22,8 +22,8 @@ name = "protoc-gen-protovalidate-buffa" path = "src/main.rs" [dependencies] -buffa = "0.6" -buffa-codegen = "0.6" +buffa = "0.7" +buffa-codegen = "0.7" protovalidate-buffa-protos = { path = "../protovalidate-buffa-protos", version = "0.4.0" } proc-macro2 = "1" quote = "1" diff --git a/crates/protovalidate-buffa-conformance/Cargo.toml b/crates/protovalidate-buffa-conformance/Cargo.toml index 8471dd2..f2641f5 100644 --- a/crates/protovalidate-buffa-conformance/Cargo.toml +++ b/crates/protovalidate-buffa-conformance/Cargo.toml @@ -20,7 +20,7 @@ name = "protovalidate-buffa-conformance" path = "src/main.rs" [dependencies] -buffa = "0.6" +buffa = "0.7" protovalidate-buffa = { path = "../protovalidate-buffa", version = "0.4.0", default-features = false, features = ["tz"] } protovalidate-buffa-protos = { path = "../protovalidate-buffa-protos", version = "0.4.0" } anyhow = "1" @@ -33,10 +33,10 @@ regex = "1" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } [build-dependencies] -buffa-build = "0.6" +buffa-build = "0.7" protoc-gen-protovalidate-buffa = { path = "../protoc-gen-protovalidate-buffa", version = "0.4.0" } -buffa = "0.6" -buffa-codegen = "0.6" +buffa = "0.7" +buffa-codegen = "0.7" anyhow = "1" prettyplease = "0.2" syn = { version = "2", features = ["full"] } diff --git a/crates/protovalidate-buffa-macros/src/lib.rs b/crates/protovalidate-buffa-macros/src/lib.rs index 54cc274..bffea1d 100644 --- a/crates/protovalidate-buffa-macros/src/lib.rs +++ b/crates/protovalidate-buffa-macros/src/lib.rs @@ -1,10 +1,11 @@ //! `#[connect_impl]` — inserts `req.validate()?` at the top of every Connect -//! service handler method in an `impl` block whose request parameter is an -//! `OwnedView<_>`. Single-site safety net: add it once to the service impl -//! and every present-and-future handler is validated on entry. +//! service handler method in an `impl` block whose request parameter is a +//! `ServiceRequest<'_, _>` (connectrpc 0.7) or an `OwnedView<_>` (0.6). +//! Single-site safety net: add it once to the service impl and every +//! present-and-future handler is validated on entry. //! //! Non-handler `async fn`s inside the same `impl` block are left alone -//! (they lack an `OwnedView<_>` parameter, so the macro skips them). +//! (they lack such a request parameter, so the macro skips them). use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; @@ -26,7 +27,7 @@ pub fn connect_impl(attr: TokenStream, input: TokenStream) -> TokenStream { for impl_item in &mut item.items { if let ImplItem::Fn(f) = impl_item - && let Some(arg_ident) = find_owned_view_arg(&f.sig) + && let Some(arg_ident) = find_request_arg(&f.sig) { let pv_ident = proc_macro2::Ident::new("__protovalidate_buffa_req_owned", arg_ident.span()); @@ -47,13 +48,14 @@ pub fn connect_impl(attr: TokenStream, input: TokenStream) -> TokenStream { TokenStream::from(quote! { #item }) } -/// Returns the ident of the first parameter whose type is a path ending in -/// `OwnedView` (e.g. `OwnedView>`). -/// Non-handler methods that lack such a parameter return `None`. -fn find_owned_view_arg(sig: &syn::Signature) -> Option { +/// Returns the ident of the first parameter that is a Connect request: a +/// `ServiceRequest<'_, _>` (connectrpc 0.7) or an `OwnedView<_>` (0.6). Both +/// expose `to_owned_message()`, which the inserted code calls. Non-handler +/// methods that lack such a parameter return `None`. +fn find_request_arg(sig: &syn::Signature) -> Option { for arg in &sig.inputs { if let FnArg::Typed(PatType { pat, ty, .. }) = arg - && is_owned_view(ty) + && is_request_view(ty) && let syn::Pat::Ident(pat_ident) = pat.as_ref() { return Some(pat_ident.ident.clone()); @@ -62,11 +64,11 @@ fn find_owned_view_arg(sig: &syn::Signature) -> Option { None } -fn is_owned_view(ty: &Type) -> bool { +fn is_request_view(ty: &Type) -> bool { if let Type::Path(TypePath { path, .. }) = ty && let Some(last) = path.segments.last() { - return last.ident == "OwnedView"; + return last.ident == "ServiceRequest" || last.ident == "OwnedView"; } false } diff --git a/crates/protovalidate-buffa-protos/Cargo.toml b/crates/protovalidate-buffa-protos/Cargo.toml index 795fba7..934af03 100644 --- a/crates/protovalidate-buffa-protos/Cargo.toml +++ b/crates/protovalidate-buffa-protos/Cargo.toml @@ -15,7 +15,7 @@ keywords.workspace = true workspace = true [dependencies] -buffa = "0.6" +buffa = "0.7" [build-dependencies] -buffa-build = "0.6" +buffa-build = "0.7" diff --git a/crates/protovalidate-buffa/Cargo.toml b/crates/protovalidate-buffa/Cargo.toml index 438a444..51cbb1c 100644 --- a/crates/protovalidate-buffa/Cargo.toml +++ b/crates/protovalidate-buffa/Cargo.toml @@ -26,7 +26,8 @@ connect = ["dep:connectrpc"] tz = ["dep:chrono-tz"] [dependencies] -buffa = "0.6" +buffa = "0.7" +buffa-types = "0.7" protovalidate-buffa-macros = { path = "../protovalidate-buffa-macros", version = "0.3.0" } regex = "1" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } @@ -35,6 +36,6 @@ uuid = "1" ulid = "1" ipnet = "2" fluent-uri = "0.4" -connectrpc = { version = "0.6", optional = true } +connectrpc = { version = "0.7", optional = true } percent-encoding = "2" http = "1" diff --git a/crates/protovalidate-buffa/src/lib.rs b/crates/protovalidate-buffa/src/lib.rs index d4e3d58..5732716 100644 --- a/crates/protovalidate-buffa/src/lib.rs +++ b/crates/protovalidate-buffa/src/lib.rs @@ -75,6 +75,17 @@ pub trait Validate { fn validate(&self) -> Result<(), ValidationError>; } +/// `google.protobuf.Empty` carries no fields and no rules, so it always +/// validates. Providing it here (the trait's home crate — downstreams can't, +/// by the orphan rule) lets `#[connect_impl]` apply uniformly to services +/// whose handlers take an empty request, which is the common shape for +/// subscribe / list / no-argument RPCs. +impl Validate for ::buffa_types::google::protobuf::Empty { + fn validate(&self) -> Result<(), ValidationError> { + Ok(()) + } +} + #[macro_export] macro_rules! field_path { ( $( $part:expr ),* $(,)? ) => {{ diff --git a/crates/protovalidate-buffa/tests/connect_impl.rs b/crates/protovalidate-buffa/tests/connect_impl.rs index 48edf43..86460cd 100644 --- a/crates/protovalidate-buffa/tests/connect_impl.rs +++ b/crates/protovalidate-buffa/tests/connect_impl.rs @@ -94,3 +94,60 @@ fn injects_validate_and_runs_body_on_success() { svc.handle(OwnedView(FakeView { valid: true })).unwrap(); assert!(svc.called.get(), "body must run when validate passes"); } + +// connectrpc 0.7 hands handlers a `ServiceRequest<'_, _>` rather than an +// `OwnedView<_>`; the macro must recognize it too (both expose +// `to_owned_message()`), else `#[connect_impl]` silently no-ops under 0.7. +struct ServiceRequest<'a, T>(&'a T); + +impl ServiceRequest<'_, T> { + fn to_owned_message(&self) -> FakeOwned + where + T: AsRef, + { + self.0.as_ref().to_owned_message() + } +} + +impl AsRef for FakeView { + fn as_ref(&self) -> &FakeView { + self + } +} + +trait FakeService07 { + fn handle(&self, request: ServiceRequest<'_, FakeView>) -> Result<(), ::connectrpc::ConnectError>; +} + +struct Impl07 { + called: Cell, +} + +#[connect_impl] +impl FakeService07 for Impl07 { + fn handle( + &self, + _request: ServiceRequest<'_, FakeView>, + ) -> Result<(), ::connectrpc::ConnectError> { + self.called.set(true); + Ok(()) + } +} + +#[test] +fn injects_validate_for_service_request_0_7() { + let svc = Impl07 { + called: Cell::new(false), + }; + let bad = FakeView { valid: false }; + let err = svc.handle(ServiceRequest(&bad)).unwrap_err(); + assert_eq!(err.code, ::connectrpc::ErrorCode::InvalidArgument); + assert!(!svc.called.get(), "body must not run when validate fails"); + + let svc = Impl07 { + called: Cell::new(false), + }; + let good = FakeView { valid: true }; + svc.handle(ServiceRequest(&good)).unwrap(); + assert!(svc.called.get(), "body must run when validate passes"); +}