Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 84 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/protoc-gen-protovalidate-buffa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions crates/protovalidate-buffa-conformance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
Expand Down
26 changes: 14 additions & 12 deletions crates/protovalidate-buffa-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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());
Expand All @@ -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<pb::CreateFooRequestView<'static>>`).
/// Non-handler methods that lack such a parameter return `None`.
fn find_owned_view_arg(sig: &syn::Signature) -> Option<syn::Ident> {
/// 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<syn::Ident> {
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());
Expand All @@ -62,11 +64,11 @@ fn find_owned_view_arg(sig: &syn::Signature) -> Option<syn::Ident> {
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
}
4 changes: 2 additions & 2 deletions crates/protovalidate-buffa-protos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 3 additions & 2 deletions crates/protovalidate-buffa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
11 changes: 11 additions & 0 deletions crates/protovalidate-buffa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ),* $(,)? ) => {{
Expand Down
57 changes: 57 additions & 0 deletions crates/protovalidate-buffa/tests/connect_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> ServiceRequest<'_, T> {
fn to_owned_message(&self) -> FakeOwned
where
T: AsRef<FakeView>,
{
self.0.as_ref().to_owned_message()
}
}

impl AsRef<FakeView> for FakeView {
fn as_ref(&self) -> &FakeView {
self
}
}

trait FakeService07 {
fn handle(&self, request: ServiceRequest<'_, FakeView>) -> Result<(), ::connectrpc::ConnectError>;
}

struct Impl07 {
called: Cell<bool>,
}

#[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");
}
Loading