Skip to content

Releases: mathematic-inc/protovalidate-buffa

protoc-gen-protovalidate-buffa-v0.5.1

Choose a tag to compare

@jrandolf jrandolf released this 14 Jul 22:50
67f2794

protoc-gen-protovalidate-buffa v0.5.1

This patch release squashes a codegen bug that caused generated validators to fail to compile when an optional string field (or an editions 2023 field_presence = EXPLICIT field) carried a string.pattern rule — exactly the kind of silent failure that's hardest to track down. If you have any such fields in your schema, upgrade immediately. Everyone else gets a free correctness improvement and a regression test that'll keep it fixed forever.

Conformance remains at 2872 / 2872 (100%) across proto2, proto3, and editions 2023.


🐛 Bug Fixes

Fix borrow in optional string pattern check (#21)

What broke. An explicit-presence string field — written as optional string in proto3, or using field_presence = EXPLICIT in editions — is lowered to an owned let v: String binding before the shared string-rule emitter runs. The string.pattern check was then passing that v directly to Regex::is_match, which expects &str. An owned String does not coerce in argument position, so the generated code wouldn't compile:

error[E0308]: mismatched types
  --> gen/protovalidate/my_message.rs:42:34
   |
   |         PATTERN.is_match(v)
   |                          ^ expected `&str`, found `String`

Why conformance didn't catch it. The upstream protovalidate-conformance suite tests string.pattern only on plain (implicit-presence) fields, where the binding is &String and auto-deref coerces correctly. The optional path never exercised this branch.

The fix. The emitter now passes v.as_str(), which works whether v is an owned String (optional path) or a &String (oneof path). A dedicated render-based regression test covering both paths was added to make sure this can't regress silently again.

Who is affected. Any schema with a field declared optional string (proto3) or field_presence = EXPLICIT (editions) that also has a string.pattern rule. If your schema has such a field, the generated validator would not have compiled at all — so you would already know. After upgrading and re-running buf generate, it will compile and validate correctly.

Proto example:

syntax = "proto3";
import "buf/validate/validate.proto";

message SlugRequest {
  // optional + pattern: previously generated non-compiling Rust
  optional string slug = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9-]+$"
    min_len: 1
    max_len: 64
  }];
}

Generated code now compiles:

// protovalidate-buffa emits this — v0.5.1 correctly borrows the owned binding
if let Some(v) = &self.slug {
    let v: String = v.clone(); // explicit-presence path
    if !SLUG_PATTERN.is_match(v.as_str()) { // <-- .as_str() is the fix
        violations.push(/* ... */);
    }
}

Error handling (unchanged from v0.5.0 — matching on typed fields, not stringly-typed prefixes):

use protovalidate_buffa::{Validate, ValidationError};

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(ValidationError { violations, compile_error: None, runtime_error: None }) => {
        // Field constraint violations — surface these to the caller
        for v in &violations {
            eprintln!("violation on {}: {}", v.field_path, v.message);
        }
    }
    Err(ValidationError { compile_error: Some(e), .. }) => {
        // Schema mismatch caught at codegen time — this is a bug in the .proto
        panic!("codegen schema error: {e}");
    }
    Err(ValidationError { runtime_error: Some(e), .. }) => {
        // Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`)
        eprintln!("runtime validation error: {e}");
    }
}

Installation

Runtime library (protovalidate-buffa)

[dependencies]
protovalidate-buffa = "0.5.1"

Codegen plugin (protoc-gen-protovalidate-buffa)

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protoc-gen-protovalidate-buffa-v0.5.1 \
  protoc-gen-protovalidate-buffa

Then re-run buf generate to pick up the fixed output. No changes to your buf.gen.yaml or .proto files are required.

v0.5.1

Choose a tag to compare

@jrandolf jrandolf released this 28 Jun 20:47
83d3088

protovalidate-buffa v0.5.1

#[connect_impl] now speaks fluent Connect 0.7 — and still hits 2872 / 2872 (100%) on the upstream conformance suite.

This patch release rounds out the Connect integration story. The #[connect_impl] macro previously understood only buffa's OwnedView<T> handler signatures; it now recognises connectrpc::ServiceRequest<'_, T> as well, so teams that have adopted the Connect 0.7 owned-message wrapper get automatic validation out of the box — no per-handler boilerplate, no forgetting a req.validate()?.


🐛 Bug Fixes

#[connect_impl] validates Connect ServiceRequest handlers

Prior to this release, applying #[connect_impl] to a service impl that used Connect 0.7's ServiceRequest<'_, T> parameter type silently skipped those handlers — the macro only injected validation when it spotted an OwnedView<T>. Any handler written in the newer style received no automatic validation at all.

The macro now detects both shapes:

Handler signature Validated?
request: OwnedView<pb::FooView<'static>> ✅ (was already working)
request: connectrpc::ServiceRequest<'_, pb::Foo> ✅ (fixed in v0.5.1)

Both types expose to_owned_message(), so the injected code is uniform and zero-surprise:

// Apply once at the impl level — every handler is covered, now and in future.
#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        // Connect 0.7 ServiceRequest — now fully validated automatically.
        request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // Validation runs before this line.
        // Invalid requests are rejected as InvalidArgument before reaching here.
        let req = request.into_message();
        // ...
    }
}

Validation failures surface as a structured ConnectError::invalid_argument carrying the full set of rule violations, powered by ValidationError::into_connect_error. The typed error fields let you distinguish failure modes cleanly:

match msg.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema mismatch caught at codegen time; this is a bug in the .proto or plugin config.
        tracing::error!(compile_error = %e.compile_error.as_deref().unwrap_or(""));
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition couldn't be evaluated (e.g. non-UTF-8 bytes under `pattern`).
        tracing::warn!(runtime_error = %e.runtime_error.as_deref().unwrap_or(""));
    }
    Err(e) => {
        // The common case: one or more field-level rule violations.
        for v in &e.violations {
            tracing::debug!(field = %v.field_path, message = %v.message);
        }
    }
}

Conformance

2872 / 2872 (100%) — all cases in the upstream protovalidate-conformance harness pass, covering proto2, proto3, and editions 2023. Nothing regressed.


Installation

Library consumers

[dependencies]
protovalidate-buffa = "0.5.1"

Enable the connect feature (on by default) to get ValidationError::into_connect_error and the #[connect_impl] macro.

Codegen plugin

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag v0.5.1 \
  protoc-gen-protovalidate-buffa

Then wire it into your buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

Dual-licensed Apache-2.0 / MIT.

v0.5.0

Choose a tag to compare

@jrandolf jrandolf released this 28 Jun 01:22
d8d61e6

protovalidate-buffa v0.5.0

protovalidate-buffa v0.5.0 lands three important correctness fixes that make the codegen plugin robust across real-world proto schemas: keyword-named fields now produce valid Rust instead of panicking at generation time, the optional Connect integration no longer drags in streaming and compression crates you never asked for, and the whole workspace has been updated to the buffa 0.7 line. The 100% conformance score — 2872 / 2872 cases across proto2, proto3, and editions 2023 — remains intact throughout.


🐛 Bug Fixes

Keyword-named fields and oneofs no longer break codegen

The plugin was building field accessors (self.<field>) directly from raw proto names. Any field or oneof whose name is a Rust keyword caused one of three failure modes:

  • format_ident!("type") emits the raw invalid token self.typesyn / prettyplease refuse to parse it.
  • parse_str::<Ident>("type") returns Err and fails the render pipeline.
  • Keywords that can't be raw identifiers (self, super, crate, Self) caused an outright Ident::new panic at codegen time.

The fix routes every proto-name-to-ident conversion through a shared field_ident helper that delegates to buffa_codegen::idents::make_field_ident — the same function buffa itself uses when naming struct fields. Emitted accessors now match the generated message types by construction:

// proto
message Request {
  string type = 1 [(buf.validate.field).string.min_len = 1];
  string self = 2 [(buf.validate.field).string.min_len = 1];
}
// generated (before this fix: compile error / panic)
impl Validate for Request {
    fn validate(&self) -> Result<(), ValidationError> {
        // raw-able keyword → r#type
        if self.r#type.len() < 1 { /* ... */ }
        // non-raw-able keyword → suffixed
        if self.self_.len() < 1 { /* ... */ }
        Ok(())
    }
}

Regression tests covering keyword fields, keyword-named oneofs, and CEL rules on keyword-named fields are now part of the plugin's integration test suite.

This is a breaking change if you were working around the bug by renaming proto fields — you can remove those workarounds now.

Connect feature no longer pulls in streaming and compression crates

The optional connect feature only ever needed ConnectError and ErrorCode from connectrpc. By default, connectrpc previously enabled its streaming and compression feature flags, which added async-compression and related transitive dependencies to your build even when you just wanted req.validate()?InvalidArgument. Those are now gone. Enabling the connect feature is leaner than it used to be.


⚡ Improvements

buffa 0.7

The workspace — runtime, macros, protos, plugin, and conformance harness — is now on the buffa 0.7 line. connectrpc has been moved to its matching 0.7 release to keep the transitive dependency graph consistent.


Conformance

2872 / 2872 (100%) against the upstream protovalidate-conformance suite, covering proto2, proto3, and editions 2023. Every rule family in the standard-rules catalogue and predefined-rules support is covered.


Error handling reminder

Match on the typed ValidationError fields, not on rule_id prefixes:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema-level mismatch caught at codegen time — this is a bug
        // in your proto annotations or plugin version. Log and panic in dev.
        panic!("protovalidate compile error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition failed at runtime (e.g. bytes.pattern on non-UTF-8).
        return Err(e.into_connect_error());
    }
    Err(e) => {
        // The common case: one or more field violations.
        return Err(e.into_connect_error()); // → InvalidArgument
    }
}

Installation

Library consumers — add to Cargo.toml:

protovalidate-buffa = "0.5.0"

Codegen plugin — install from source:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag v0.5.0 \
  protoc-gen-protovalidate-buffa

Then wire it into your buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

protovalidate-buffa-protos-v0.5.0

Choose a tag to compare

@jrandolf jrandolf released this 28 Jun 01:22
d8d61e6

protovalidate-buffa-protos v0.5.0

This release ships three targeted fixes that make the codegen plugin more
robust in real-world proto schemas: proto fields and oneofs named with Rust
keywords now generate valid code instead of panicking, the connect feature
pulls in substantially fewer transitive dependencies, and the whole workspace
moves to the buffa 0.7 line. The 100% conformance record stands —
2872 / 2872 cases across proto2, proto3, and editions 2023 — and now the
plugin can handle whatever names your protos throw at it.


🐛 Bug Fixes

Rust keyword field and oneof names no longer break codegen

The plugin previously built field accessors (self.<field>) directly from raw
proto names. Any field or oneof named with a Rust keyword — type, match,
use, or even self — caused format_ident! to emit invalid syntax,
parse_str::<Ident> to return an error, or Ident::new to panic outright.

All name-to-ident conversions in the emitter now route through a shared
field_ident helper that delegates to buffa_codegen::idents::make_field_ident
— the exact same function buffa uses when naming struct fields. This guarantees
that generated accessors always match the buffa-generated struct layout, even
on keyword names:

// These used to generate broken Rust. Now they work.
message FilterRequest {
  string type   = 1 [(buf.validate.field).string.min_len = 1];
  string match  = 2 [(buf.validate.field).string.min_len = 1];
}

Generated accessors now emit self.r#type and self.r#match (or the
_-suffix form for the rare non-raw-able keywords), matching the buffa struct
fields exactly. A regression test suite for the full emit::render pipeline
on keyword names is included.

connect feature no longer pulls streaming and compression dependencies

The optional Connect integration only needs ConnectError and ErrorCode.
connectrpc's default features were previously enabled, which dragged in
streaming and compression transitive dependencies unconditionally whenever the
connect feature was active.

connectrpc is now declared with default-features = false, so enabling
connect brings in only what the validation→Connect-error mapping actually
uses. If you match on ValidationError in a Connect handler, the ergonomics
are unchanged:

use protovalidate_buffa::{ValidationError, Validate};

fn handle(req: &MyRequest) -> Result<(), ConnectError> {
    req.validate().map_err(|e| {
        // Match on the typed fields, not rule_id string prefixes.
        if e.compile_error.is_some() {
            // Schema-level mismatch caught at codegen — should not reach prod.
            ConnectError::new(Code::Internal, "validation schema error")
        } else if e.runtime_error.is_some() {
            // Rule precondition failed at runtime (e.g. non-UTF-8 under `pattern`).
            ConnectError::new(Code::Internal, "validation runtime error")
        } else {
            // The common path: field rule failures.
            e.into_connect_error()
        }
    })
}

Or just use #[connect_impl] and let the macro handle it for every handler
at once:

#[protovalidate_buffa::connect_impl]
impl MyService for MyServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: OwnedView<pb::CreateUserRequestView<'static>>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // req.validate()? is inserted automatically — body only sees
        // already-validated requests.
    }
}

⬆️ Updated Dependencies

  • buffa → 0.7 across the workspace (buffa, buffa-build, buffa-codegen).
  • connectrpc → 0.7 (aligned with the buffa 0.7 transitive requirement,
    default features disabled).

Conformance

2872 / 2872 (100%) — every case in the upstream
protovalidate-conformance
suite passes, covering proto2, proto3, and editions 2023.


Installation

Library consumers — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.5.0"

Codegen plugin — install from source:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protovalidate-buffa-protos-v0.5.0 \
  protoc-gen-protovalidate-buffa

Then wire it into buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

protovalidate-buffa-macros-v0.3.1

Choose a tag to compare

@jrandolf jrandolf released this 28 Jun 20:47
83d3088

protovalidate-buffa-macros v0.3.1

#[connect_impl] just levelled up. This patch release teaches the macro to recognise Connect 0.7's ServiceRequest<'_, T> request parameters alongside the existing OwnedView<T> support — meaning your service impls get automatic, zero-boilerplate proto validation regardless of which request wrapper your handlers use. One attribute, every handler covered, no per-method discipline required. The full upstream conformance suite remains at a perfect 2872 / 2872 (100%) across proto2, proto3, and editions 2023.

🐛 Bug Fixes

#[connect_impl] now validates ServiceRequest handlers

Prior to this release, the macro only injected validate()? into handlers whose request parameter was typed as OwnedView<_>. Handlers using Connect 0.7's connectrpc::ServiceRequest<'_, T> wrapper were silently skipped — no validation, no error, just a gap in your safety net.

Before (validation was not injected):

#[connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
        //       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        //       Previously ignored by #[connect_impl] — no validate()!
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // User code ran even on invalid requests. Oops.
    }
}

After (validation is injected automatically):

#[connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // #[connect_impl] now validates the request here automatically.
        // Body only sees already-validated requests — for both OwnedView and ServiceRequest.
    }
}

The macro now recognises both OwnedView<_> and ServiceRequest<'_, _> as handler request parameters and injects validation for either. Non-handler methods that lack a recognised request parameter continue to be left untouched.

If you match on validation errors, remember to use the typed fields — not stringly-typed rule ID prefixes:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema mismatch caught at codegen time — should not reach production
        panic!("codegen schema error: {:?}", e.compile_error);
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`)
        return Err(e.into_connect_error());
    }
    Err(e) => {
        // One or more field violations
        return Err(e.into_connect_error());
    }
}

Getting Started

Library consumers

Add to your Cargo.toml:

[dependencies]
protovalidate-buffa = "0.5.1"   # re-exports #[connect_impl] and the full runtime

The #[connect_impl] macro is gated behind the connect feature (enabled by default). If you want the macros crate directly:

[dependencies]
protovalidate-buffa-macros = "0.3.1"

Codegen plugin

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protovalidate-buffa-macros-v0.3.1 \
  protoc-gen-protovalidate-buffa

Then wire it into buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

Happy validating! 🛡️

protoc-gen-protovalidate-buffa-v0.5.0

Choose a tag to compare

@jrandolf jrandolf released this 28 Jun 01:22
d8d61e6

protoc-gen-protovalidate-buffa v0.5.0

This release hardens the codegen plugin against real-world proto schemas, cleans up
the optional Connect dependency, and upgrades the entire workspace to buffa 0.7 — all
while keeping the 2872 / 2872 (100%) upstream conformance score untouched across
proto2, proto3, and editions 2023. If you have proto fields or oneofs named after Rust
keywords (type, self, super, Self, crate, …), this release fixes a class of
panics and invalid-code-generation bugs that would have silently broken your validator
output. Upgrade is strongly recommended.


🐛 Bug Fixes

Rust keyword field and oneof names no longer break codegen

The plugin previously constructed field accessors by passing raw proto field names
directly to format_ident! / Ident::new / parse_str::<Ident>. Any field or
oneof whose proto name happened to be a Rust keyword crashed or produced unparseable
output:

  • format_ident!("type") — emitted self.type which syn rejects.
  • parse_str::<Ident>("self") — returned Err, halting generation.
  • Ident::new_raw("self")panicked because self cannot be a raw identifier.

All accessor construction is now routed through a shared field_ident helper that
delegates to buffa_codegen::idents::make_field_ident — the same function buffa uses
when generating the message structs. This guarantees that emitted accessors always match
the generated types by construction:

Proto field name Generated accessor
type self.r#type
self self.self_
crate self.crate_
// Previously crashed the plugin. Now works.
message Request {
  string type = 1 [(buf.validate.field).string.min_len = 1];
}

Regression tests covering required fields, CEL rules, and required oneofs with
keyword names are included to pin this behaviour permanently.

Connect feature no longer pulls in streaming/compression transitive deps

Enabling the connect feature only requires ConnectError and ErrorCode from the
connectrpc crate. Default features (which include streaming and compression support)
were previously activated unconditionally, bloating the dependency graph for users who
only needed the InvalidArgument mapping. default-features = false is now set on
the connectrpc dependency, so you get exactly the error types and nothing more.


⚡ Improvements

buffa 0.7

The workspace now tracks the buffa 0.7 line across all crates (buffa, buffa-build,
buffa-codegen, connectrpc). If you are already on buffa 0.7 in your own project,
all transitive dependencies now resolve cleanly to a single version with no duplication.


Error handling reference

Match on the typed ValidationError fields — never on rule_id string prefixes:

use protovalidate_buffa::{Validate, ValidationError};

match req.validate() {
    Ok(()) => { /* all rules passed */ }
    Err(ValidationError { compile_error: Some(msg), .. }) => {
        // Schema mismatch caught at codegen time (rule/field type mismatch,
        // unknown CEL field reference, etc.).
        eprintln!("schema error: {msg}");
    }
    Err(ValidationError { runtime_error: Some(msg), .. }) => {
        // Rule precondition failed at runtime (e.g. bytes.pattern on non-UTF-8 input).
        eprintln!("runtime error: {msg}");
    }
    Err(ValidationError { violations, .. }) => {
        // One or more field rules failed — the common case.
        for v in violations {
            eprintln!("[{}] {}", v.rule_id, v.message);
        }
    }
}

Installation

Library — add to Cargo.toml:

protovalidate-buffa = "0.5.0"

Codegen plugin — install from source:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protoc-gen-protovalidate-buffa-v0.5.0 \
  protoc-gen-protovalidate-buffa

Then wire it into buf.gen.yaml as before:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

v0.4.0

Choose a tag to compare

@jrandolf jrandolf released this 25 May 02:22
ee86068

protovalidate-buffa v0.4.0

protovalidate-buffa v0.4.0 lands full compatibility with buffa 0.6 and connectrpc 0.6 — the latest releases of both upstream frameworks — and keeps the 100% conformance record firmly intact. If you've been waiting to upgrade your buffa runtime while staying on validated, type-safe protovalidate rules, this release is your green light.

⚠️ Breaking Changes

This release tracks breaking changes in two upstream crates. Bumping protovalidate-buffa to 0.4.0 implies upgrading your own dependencies to match:

Dependency Old New
buffa 0.5 0.6
connectrpc 0.4 0.6 (if using the connect feature)

Regenerate your validation code after updating (buf generate) — the plugin is also at 0.4.0 and must match the runtime.

Migrating:

# Cargo.toml
[dependencies]
protovalidate-buffa = "0.4.0"
buffa = "0.6"
connectrpc = "0.6"   # only if you use the `connect` feature

⚡ Improvements

buffa 0.6 support — no more git patches

Previous releases required a [patch.crates-io] block in your workspace Cargo.toml to point at an unreleased buffa revision. That's gone. buffa 0.6 is on crates.io, so the whole stack resolves cleanly from the registry with no patching needed.

connectrpc 0.6 support

The optional connect feature now targets connectrpc 0.6. ValidationError::into_connect_error continues to map field violations to a structured InvalidArgument Connect error — the API is unchanged, only the version floor moves.

// Error handling is the same — match on typed fields, not string prefixes.
match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema mismatch caught at codegen time — should not reach prod.
        tracing::error!(err = %e.compile_error.as_deref().unwrap_or(""), "compile error");
        return Err(e.into_connect_error());
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`).
        return Err(e.into_connect_error());
    }
    Err(e) => {
        // Field violations — the common path.
        return Err(e.into_connect_error());
    }
}

2872 / 2872 conformance — no regressions

The full upstream protovalidate-conformance suite still passes at 2872 / 2872 (100%) across proto2, proto3, and editions 2023. The dependency upgrade did not regress a single test case.

📦 Installation

Library consumers — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.4.0"

Codegen plugin — install the buf generate plugin:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag v0.4.0 \
  protoc-gen-protovalidate-buffa

Then wire it into buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

Full changelog: v0.3.0...v0.4.0

protovalidate-buffa-protos-v0.4.0

Choose a tag to compare

@jrandolf jrandolf released this 25 May 02:22
ee86068

protovalidate-buffa v0.4.0 brings full compatibility with buffa 0.6 and connectrpc 0.6, keeping the entire validation stack in lockstep with the latest buffa runtime. All four published crates have been updated together so you can upgrade your service in one shot. The library continues to pass 2872 / 2872 (100%) of the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023 — every rule, every edge case, zero regressions. If you're upgrading your buffa or connectrpc dependency, this is your paired protovalidate release.

💥 Breaking Changes

Upgraded to buffa 0.6 + connectrpc 0.6

The runtime (protovalidate-buffa), protos crate (protovalidate-buffa-protos), and codegen plugin (protoc-gen-protovalidate-buffa) all target buffa 0.6 and connectrpc 0.6. If your service is still on buffa 0.5.x, stay on v0.3.0 until you're ready to upgrade.

Update your workspace together:

# Cargo.toml
[dependencies]
buffa = "0.6"
connectrpc = "0.6"
protovalidate-buffa = "0.4.0"

✅ Conformance: 2872 / 2872

The full upstream protovalidate-conformance suite passes — all 2872 cases across proto2, proto3, and editions 2023. Every standard rule family is covered: strings, bytes, numerics, booleans, enums, repeated fields, maps, google.protobuf.Any, Duration, Timestamp, FieldMask, wrapper types, predefined rules, CEL expressions, and cross-proto cel_expression references.

Worth calling out: every CEL rule is transpiled to native Rust at codegen time — there is no interpreter at runtime. Generated validate() calls are direct struct field walks that LLVM can inline, with zero per-call Value / HashMap allocations for well-formed input.

Error handling

ValidationError carries three orthogonal, typed signals — match on the fields, not on string prefixes:

use protovalidate_buffa::Validate;

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema-level mismatch caught at codegen time
        // (rule/field type error, bad oneof spec, CEL referencing unknown field)
        eprintln!("compile error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition could not be evaluated at runtime
        // (e.g. bytes.pattern on non-UTF-8 input)
        eprintln!("runtime error: {}", e.runtime_error.unwrap());
    }
    Err(e) => {
        // Per-field violations — map to ConnectError::invalid_argument
        return Err(e.into_connect_error());
    }
}

Zero-boilerplate validation with #[connect_impl]

Apply #[protovalidate_buffa::connect_impl] to a Connect service impl block and req.validate()? is inserted at the top of every handler automatically — no per-handler discipline required:

#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: OwnedView<pb::CreateUserRequestView<'static>>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // Every request arriving here is already validated.
    }
}

Installation

Library — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.4.0"

Codegen plugin — install once and wire into buf.gen.yaml:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protovalidate-buffa-protos-v0.4.0 \
  protoc-gen-protovalidate-buffa
# buf.gen.yaml
plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

protoc-gen-protovalidate-buffa-v0.4.0

Choose a tag to compare

@jrandolf jrandolf released this 25 May 02:22
ee86068

protoc-gen-protovalidate-buffa v0.4.0

protovalidate-buffa v0.4.0 is a compatibility release that tracks the latest buffa and connectrpc ecosystem — your generated validation code stays in lock-step with the runtime it was built for. All 2872 / 2872 (100%) upstream protovalidate-conformance cases continue to pass across proto2, proto3, and editions 2023, so you're upgrading with zero regressions and full confidence.


⚡ Improvements

Updated to buffa 0.6 and connectrpc 0.6

The entire workspace now targets the latest buffa and connectrpc releases. If your project already uses buffa 0.6 or connectrpc 0.6, this release removes the version conflict and lets you upgrade cleanly.

Affected crates: protovalidate-buffa, protoc-gen-protovalidate-buffa, protovalidate-buffa-protos.


Breaking Changes

  • buffa 0.5 → 0.6. The runtime and plugin both now require buffa = "0.6". Update your Cargo.toml and re-run buf generate to regenerate message types against the new runtime.
  • connectrpc 0.4 → 0.6. If you use the connect feature (enabled by default), your project must use connectrpc 0.6. The ValidationError::into_connect_error adapter is fully compatible with the new version.

Updating is a one-line bump in your Cargo.toml:

[dependencies]
protovalidate-buffa = "0.4.0"

Error handling is unchanged — keep matching on the typed fields, not on string prefixes:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema-level mismatch caught at codegen time — should never reach prod
        panic!("validation schema error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition failed at runtime (e.g. non-UTF-8 input under `pattern`)
        return Err(e.into_connect_error());
    }
    Err(e) => {
        // Field violations — the common case
        return Err(e.into_connect_error());
    }
}

Conformance

2872 / 2872 (100%) — every case in the upstream protovalidate-conformance suite passes, covering proto2, proto3, and editions 2023. No regressions from the dependency bump.


Installation

Library (protovalidate-buffa)

Add to your Cargo.toml:

[dependencies]
protovalidate-buffa = "0.4.0"

Codegen plugin (protoc-gen-protovalidate-buffa)

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag protoc-gen-protovalidate-buffa-v0.4.0 \
  protoc-gen-protovalidate-buffa

Then wire it into your buf.gen.yaml:

plugins:
  - local: protoc-gen-protovalidate-buffa
    out: gen/protovalidate
    strategy: all

Re-run buf generate after upgrading the plugin — generated code references buffa 0.6 types.

v0.3.0

Choose a tag to compare

@jrandolf jrandolf released this 18 May 21:05
a45aad4

protovalidate-buffa v0.3.0

This release lands the biggest architectural leap since the project's debut: every CEL rule in your .proto is now transpiled to native Rust at codegen time, and the runtime interpreter is gone for good. Your validate() calls are plain struct field walks — no Value trees, no HashMap allocations, nothing to allocate or interpret on the hot path. LLVM can inline the whole thing. And the full 2872 / 2872 (100%) upstream protovalidate-conformance suite still passes, covering proto2, proto3, and editions 2023. Zero regressions, dramatically lower runtime overhead.


✨ New Features

Compile-time CEL transpilation

protoc-gen-protovalidate-buffa now ships a full typed AST visitor (emit/cel_compile.rs) that walks every CEL expression in (buf.validate.field).cel and (buf.validate.message).cel rules and emits a native Rust TokenStream. By the time you run buf generate, your CEL is already Rust.

Before v0.3.0: the runtime hosted a CEL interpreter, built a Context per call, allocated a Value tree to represent the message, evaluated the expression, and interpreted the result — all on every call.

After v0.3.0: a custom rule like this:

message TransferRequest {
  double amount = 1 [(buf.validate.field).cel = {
    id: "positive_amount",
    message: "amount must be positive",
    expression: "this > 0.0"
  }];
}

…causes the plugin to emit code equivalent to:

// generated — direct comparison, zero allocations
if !(self.amount > 0.0_f64) {
    violations.push(Violation {
        field_path: field_path!("amount"),
        constraint_id: "positive_amount".into(),
        message: "amount must be positive".into(),
    });
}

The transpiler handles the full breadth of protovalidate CEL:

  • Literals, idents, selectsthis.field, rule.gte, selector chains
  • Comprehensionsall, exists, map, filter, .map(filter, expr), nested comprehensions
  • String/bytes/list/map opssize(), contains(), startsWith(), endsWith(), indexOf(), substring(), lowerAscii(), upperAscii(), regex match
  • Semantic validatorsis_ip / is_ip_prefix (including dynamic (ver, strict) dispatch), is_hostname, is_email, is_uri, is_uri_ref
  • has() semantics — correct per field kind: scalar, optional, wrapper, message, repeated
  • Timestamp/duration — constructors, timezone-aware accessors (t.getHours("America/New_York"))
  • Const-foldingrule.foo references in predefined CEL rules inline as Rust literals

Codegen-time detection of always-runtime-error CEL

When the transpiler can prove a CEL expression would unconditionally raise a runtime error — for example, accessing a non-existent field via dyn(this).no_such_field — it now surfaces a FallbackKind::RuntimeError marker at codegen time. Rule definition bugs that previously silently passed validation now fail loudly during code generation, where they belong.


⚡ Improvements

Runtime CEL module: 980 lines → 135 lines

The cel interpreter has been removed entirely from protovalidate-buffa's dependencies. The runtime cel.rs now contains only what generated code actually needs:

  • CelScalar widening trait — uniform i64 / u64 / f64 views over scalar fields, including EnumValue<E>
  • duration_from_secs_nanos / timestamp_from_secs_nanos — WKT constructors
  • now_local — current timestamp for time-relative rules

No Value. No Context. No Program. The plugin continues to use the cel crate's parser (parse-only, no evaluator feature) to build the AST, but evaluation never touches your running binary.

86 CEL transpiler unit tests

The conformance suite exercises rules end-to-end but most cases constant-fold to predefined rules — it barely stresses the transpiler in isolation. emit/cel_compile.rs ships 86 targeted unit tests grouped by risk:

Bucket What's covered
Type system Cross-type comparison, int/uint/double casts, negation, arithmetic, modulo
Comprehensions all/exists/map/filter, nesting, predicate captures, short-list literals
has() semantics One test per SchemaFieldKind variant, plus unknown-field and dyn paths
Sub-message resolution MessageRef not in index, deep chains, self-referential schemas
String semantics Unicode size() (chars, not bytes), concat, regex, indexOf, substring
Map indexing string/int/bool keys, size(), .all() over keys
Const-folding Int/UInt/Double/Bool/Str RuleConst inlines as a Rust literal

Three tests document current transpiler gaps — int == uint cross-type equality, [].all(x, …) on empty list literals, and key in mapValue — so they won't sneak through conformance unnoticed.

Dependency cleanup

cargo +nightly udeps caught several transitive deps that were no longer needed after the interpreter was removed:

  • buffa-types dropped from protovalidate-buffa, protovalidate-buffa-protos, and protovalidate-buffa-conformance
  • connectrpc dropped from protovalidate-buffa-macros (the proc-macro emits paths that resolve through the user's own transitive deps)

Lint and doc hygiene

  • #[allow]#[expect] where the lint reliably fires; stale #[allow]s removed
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps passes cleanly
  • #[cfg(feature = "connect")]-gates the connect_impl re-export so cargo test --no-default-features compiles without errors
  • Inline let-chains replace nested if let blocks; const-eligible helpers marked const fn; Default derived where manually implemented before

Error handling

Match on the typed fields — never on rule_id string prefixes:

match request.validate() {
    Ok(()) => { /* proceed with validated input */ }

    Err(e) if e.compile_error.is_some() => {
        // Schema/type mismatch detected at codegen time.
        // Your .proto annotations don't match the field type —
        // fix the .proto, regenerate, and redeploy.
        tracing::error!("validation schema error: {}", e.compile_error.as_deref().unwrap_or(""));
        return Err(ConnectError::internal("invalid generated validation code"));
    }

    Err(e) if e.runtime_error.is_some() => {
        // A rule precondition couldn't be evaluated at runtime.
        // Common cause: bytes.pattern applied to non-UTF-8 input.
        tracing::warn!("validation runtime error: {}", e.runtime_error.as_deref().unwrap_or(""));
        return Err(ConnectError::invalid_argument("request could not be validated"));
    }

    Err(e) => {
        // The normal path: one or more field rules failed.
        return Err(e.into_connect_error()); // requires the `connect` feature
    }
}

Installation

Library consumers — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.3.0"

Codegen plugin — install from the repo at the release tag:

cargo install \
  --git https://github.com/mathematic-inc/protovalidate-buffa \
  --tag v0.3.0 \
  protoc-gen-protovalidate-buffa