Skip to content

Releases: mathematic-inc/protovalidate-buffa

protovalidate-buffa-protos-v0.1.2

28 Apr 10:14
2eab7ba

Choose a tag to compare

protovalidate-buffa-protos v0.1.2

This release brings full compatibility with buffa 0.4, fixes a source-breaking E0433 compilation error that hit every consumer of the codegen plugin, and tracks a rename in the upstream CEL library — all while holding the line at 2872 / 2872 (100%) on the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023. If you're on an earlier release, upgrading is strongly recommended: the oneof naming fix is a correctness patch that affects every generated validate() implementation that touches a oneof field.


🐛 Bug Fixes

Fix: generated code now matches buffa's *Oneof enum naming (59b307f)

buffa renamed all oneof enums from {PascalCase(name)} to {PascalCase(name)}Oneof to prevent source-breaking collisions when nested types are added to a message. The plugin was still emitting the bare {PascalCase(name)} path at every variant-match site, which caused an E0433: failed to resolve compilation error for any user on a recent buffa.

Before (broken):

// generated by the old plugin — E0433 on every arm
match &msg.delivery {
    Some(pb::Delivery::Address(v)) => { /* ... */ }
    Some(pb::Delivery::PoBox(v))   => { /* ... */ }
    None => {}
}

After (fixed):

// generated by v0.1.2 — compiles clean
match &msg.delivery {
    Some(pb::DeliveryOneof::Address(v)) => { /* ... */ }
    Some(pb::DeliveryOneof::PoBox(v))   => { /* ... */ }
    None => {}
}

This fix touches every generated impl Validate block that matches on a oneof field — which includes internal plugin infrastructure (field_rules::TypeOneof, *Rules::GreaterThanOneof, etc.) as well as user-facing generated code. The conformance suite continues to pass 2872 / 2872 after the change.


⚡ Improvements

buffa 0.4 compatibility (5b4c5f6)

All four published crates now depend on buffa = "0.4" / buffa-codegen = "0.4". The buffa 0.4 line ships the oneof naming convention described above, along with the broader set of improvements the buffa team landed since 0.3. Pin your workspace to buffa = "0.4" to pick up the full set of fixes.

CEL library updated: cel-interpretercel (7818711)

The upstream CEL crate was renamed from cel-interpreter to cel and bumped to 0.13.0 (up from 0.10.0), pulling in antlr4rust 0.5.2. The runtime re-exports it as cel_core so that the local cel module in protovalidate-buffa doesn't collide — no change needed in downstream error-handling code.

If you match on ValidationError fields, the recommended pattern is unchanged — always match on the typed slots, not on rule_id prefixes:

use protovalidate_buffa::ValidationError;

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema mismatch caught at codegen time — file a bug
        panic!("codegen error: {:?}", e.compile_error);
    }
    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) => {
        // Normal per-field violations
        return Err(e.into_connect_error());
    }
}

📦 Installation

Library (protovalidate-buffa-protos)

[dependencies]
protovalidate-buffa-protos = "0.1.2"

Codegen plugin (protoc-gen-protovalidate-buffa)

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

Then wire it into your buf.gen.yaml:

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

Full changelog: protovalidate-buffa-protos-v0.1.1...protovalidate-buffa-protos-v0.1.2

protoc-gen-protovalidate-buffa-v0.1.2

28 Apr 10:14
2eab7ba

Choose a tag to compare

protoc-gen-protovalidate-buffa v0.1.2

v0.1.2 is the release that actually ships. v0.1.1 carried meaningful upgrades — a full migration to buffa 0.4, a CEL runtime crate rename, and corrected oneof enum paths — but the crates.io publish failed silently: protovalidate-buffa-protos v0.1.0 on the registry was built against buffa 0.3 and was missing the __buffa::{oneof,ext} paths that the updated scanner imports. v0.1.2 fixes the dependency chain end-to-end, and the full 2872 / 2872 (100%) protovalidate-conformance suite passes across proto2, proto3, and editions 2023. If you were blocked on v0.1.1 — or prudently waiting to see it land — this is your release.


🐛 Bug Fixes

protovalidate-buffa-protos now published against buffa 0.4

The codegen plugin's descriptor scanner (scan.rs) uses __buffa::ext::{FIELD, MESSAGE, ONEOF} and the __buffa::oneof::* rule variant types introduced in buffa 0.4. protovalidate-buffa-protos v0.1.0 — the only version on crates.io before this release — was compiled against buffa 0.3, which predates those paths. Attempting to install v0.1.1 via cargo install would fail to resolve the dependency graph.

protovalidate-buffa-protos is now published at v0.1.2 (buffa 0.4), and protoc-gen-protovalidate-buffa declares >= 0.1.2, so cargo install resolves cleanly out of the box.


⚡ Improvements

(Changes originally targeted at v0.1.1 — included here since v0.1.1 was never successfully installable.)

Upgraded to buffa 0.4

All workspace crates (buffa, buffa-types, buffa-build, buffa-codegen) are now on 0.4. The buffa 0.4 codegen layout reorganises generated internals:

buffa 0.3 path buffa 0.4 path
Extension constants at crate root __buffa::ext::{FIELD, MESSAGE, ONEOF}
Oneof modules at crate root __buffa::oneof::<rule_type>
FooOneof enum variant names Foo (suffix dropped)

The plugin's scanner and emitter are fully updated to match. Generated Validate impls for your own proto messages are unaffected — the reorganisation is internal to the plugin's own use of buffa APIs.

Switched from cel-interpreter to cel

The upstream CEL crate was renamed from cel-interpreter to cel. The runtime re-exports it as cel_core to avoid a name collision with the internal cel module:

// Downstream crates that need to interop with the CEL runtime use:
use protovalidate_buffa::cel_core;

No API changes — just the renamed upstream crate flowing through.

Fixed generated oneof enum paths

An earlier buffa release renamed oneof enums to {PascalCaseName}Oneof to prevent source-breaking collisions when nested types are added. The plugin was still emitting bare {PascalCaseName} paths, producing E0433 compile errors in consumers building against the renamed buffa. The emitter now appends the Oneof suffix at every variant-match site in generated code.


Conformance

2872 / 2872 (100%) of the upstream protovalidate-conformance suite passes, covering proto2, proto3, and editions 2023 — including predefined rules on buf.validate.*Rules.


Error-handling reminder

When inspecting a ValidationError, match on the typed fields rather than sniffing rule_id strings:

use protovalidate_buffa::ValidationError;

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(ValidationError { compile_error: Some(msg), .. }) => {
        // Schema mismatch caught at codegen time — should not happen
        // in production; indicates a plugin/proto version skew.
        eprintln!("BUG: compile error: {msg}");
    }
    Err(ValidationError { runtime_error: Some(msg), .. }) => {
        // Rule precondition failed at runtime (e.g. bytes.pattern on
        // non-UTF-8 input, CEL type mismatch).
        eprintln!("runtime error: {msg}");
    }
    Err(ValidationError { violations, .. }) => {
        // Normal case: one or more field rule failures.
        for v in &violations {
            eprintln!("{}: {}", v.field, v.message);
        }
    }
}

Installation

Runtime library — Cargo.toml

[dependencies]
protovalidate-buffa = "0.1.0"

Enable the Connect error adapter if you're using connect-rs:

protovalidate-buffa = { version = "0.1.0", features = ["connect"] }

Codegen plugin

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

Then wire it into your buf.gen.yaml:

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

And annotate your proto messages with the full upstream rule vocabulary:

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

message CreateUserRequest {
  string email = 1 [(buf.validate.field).string = {
    min_len: 5, max_len: 254, email: true
  }];
  int32 age = 2 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];
}
// With #[connect_impl], validation runs automatically on every handler:
#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        req: OwnedView<pb::CreateUserRequestView<'static>>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // req.validate()? is inserted here automatically.
        // Reach this line only if all rules pass.
    }
}

protoc-gen-protovalidate-buffa-v0.1.1

28 Apr 10:04
8c10854

Choose a tag to compare

protoc-gen-protovalidate-buffa v0.1.1

protovalidate-buffa v0.1.1 is a compatibility release that keeps the crate fully aligned with the latest buffa and CEL ecosystems — no rule left behind. Every one of the 2872 / 2872 (100%) upstream protovalidate-conformance cases still passes across proto2, proto3, and editions 2023, even as the generated code paths were overhauled top-to-bottom to match buffa 0.4's new module layout. If you were blocked on upgrading to buffa 0.4, this release unblocks you.


🐛 Bug Fixes

Oneof enum naming fixed for buffa 0.3+

The codegen plugin was emitting bare PascalCase(name) variant paths in generated match arms — one enum rename in buffa's generated code was enough to cause E0433 on every oneof match site. Generated code now uses the correct __buffa::oneof::<module>::<Enum>::<Variant> paths, matching what buffa actually produces.

Before (would not compile against buffa ≥ 0.3):

// generated code emitted by v0.1.0 — broken
match &self.kind {
    Some(Kind::Email(ref v)) => { /* ... */ }
    //   ^^^^  E0433: failed to resolve
    None => { /* ... */ }
}

After (v0.1.1):

// generated code emitted by v0.1.1 — correct
match &self.kind {
    Some(__buffa::oneof::create_user_request::Kind::Email(ref v)) => { /* ... */ }
    None => { /* ... */ }
}

⚡ Improvements

Upgraded to buffa 0.4

buffa 0.4 reorganises the private __buffa module tree that generated code relies on:

What changed Before (0.3) After (0.4)
Oneof modules __buffa::oneof (no change) __buffa::oneof
Extension constants scattered __buffa::ext::*
Oneof enum identifiers FooOneof Foo

The codegen plugin now emits paths that target the 0.4 layout exclusively. All workspace crates (protovalidate-buffa, protovalidate-buffa-protos, protovalidate-buffa-conformance) have been updated in lockstep — conformance remains 2872 / 2872 throughout.

Swapped cel-interpreter for the renamed cel crate

The upstream CEL implementation renamed its published crate from cel-interpreter to cel. The runtime (protovalidate-buffa) now depends on cel directly and re-exports it as cel_core to avoid ambiguity with the crate's own cel module:

// Available if you need to drop down to raw CEL evaluation
use protovalidate_buffa::cel_core;

Consumer crates that previously pinned cel-interpreter directly should migrate to celprotovalidate-buffa re-exports it so most users won't need to add it explicitly.


Conformance

2872 / 2872 (100%) — unchanged from v0.1.0. Every rule family in the upstream standard-rules catalogue and predefined rules is covered across proto2, proto3, and editions 2023.

The ValidationError type continues to carry three orthogonal, typed signals — match on these fields, not on rule_id string prefixes:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // schema mismatch caught at codegen time — file a bug
        eprintln!("compile error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // rule precondition failed at runtime (e.g. non-UTF-8 input to `pattern`)
        eprintln!("runtime error: {}", e.runtime_error.unwrap());
    }
    Err(e) => {
        // one or more field violations — the common case
        for v in &e.violations {
            eprintln!("{}: {}", v.field, v.message);
        }
    }
}

Installation

Library — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.1.1"

Codegen plugin — install from source:

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

Then wire it into buf.gen.yaml:

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

That's it — annotate your .proto messages with (buf.validate.*) rules, run buf generate, and call req.validate()? in your handlers (or drop #[protovalidate_buffa::connect_impl] on your service impl to have it inserted automatically).

v0.1.0

21 Apr 12:23
bf2c05c

Choose a tag to compare

🎉 protovalidate-buffa v0.1.0 — Initial Release

We're thrilled to ship the first release of protovalidate-buffa: a static-codegen implementation of Buf's protovalidate purpose-built for the buffa Rust protobuf runtime. From day one it passes 2872 / 2872 (100%) of the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023 — every rule family, every edge case, zero compromises. If you're writing Rust gRPC / Connect services on buffa and want battle-tested field validation with zero boilerplate, this release is for you.


✨ New Features

Validate trait and structured error type

The protovalidate-buffa runtime crate exposes a single trait your generated code implements:

pub trait Validate {
    fn validate(&self) -> Result<(), ValidationError>;
}

ValidationError carries three orthogonal, typed signals — no stringing-parsing required:

pub struct ValidationError {
    pub violations: Vec<Violation>,     // per-field rule failures (the common case)
    pub compile_error: Option<String>,  // schema mismatch caught at codegen time
    pub runtime_error: Option<String>,  // rule precondition failure at runtime
}

Match on the typed fields directly:

match req.validate() {
    Ok(()) => { /* all good */ }
    Err(e) if e.compile_error.is_some() => {
        // rule/field type mismatch, malformed oneof spec, or CEL referencing
        // a non-existent field — fix the .proto annotation
        eprintln!("codegen schema error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // e.g. bytes.pattern on non-UTF-8 input, CEL type mismatch
        eprintln!("runtime precondition failed: {}", e.runtime_error.unwrap());
    }
    Err(e) => {
        // normal validation failures — inspect e.violations
        for v in &e.violations {
            eprintln!("field `{}` failed rule `{}`: {}", v.field, v.rule_id, v.message);
        }
    }
}

protoc-gen-protovalidate-buffa codegen plugin

The plugin reads every (buf.validate.field), (buf.validate.message), and (buf.validate.oneof) annotation from your compiled descriptors and emits pure-Rust impl Validate for YourMessage blocks. Wire it into buf.gen.yaml alongside your existing buffa plugin:

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

Then annotate your messages:

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

message CreateUserRequest {
  string email = 1 [(buf.validate.field).string = {
    min_len: 5, max_len: 254, email: true
  }];
  int32 age = 2 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];
  string trace_id = 3 [(buf.validate.field).string.uuid = true];
}

The generated impl is a direct struct-field walk — no runtime descriptor lookup, no dynamic dispatch. LLVM can inline the whole thing.

Schema-aware compile errors

Rule/field type mismatches, malformed message.oneof specs, and CEL expressions referencing non-existent fields are surfaced at codegen time rather than silently blowing up on the first call. When the plugin detects a schema error it emits a stub impl Validate that immediately returns ValidationError { compile_error: Some("...") } with a human-readable description of the problem — so your CI catches it, not production.

Full standard + predefined rule catalog

Every rule family in the upstream standard-rules catalogue is implemented, including:

  • Scalar rules: int32, int64, uint32, uint64, sint32/64, fixed32/64, sfixed32/64, float, double, bool
  • string rules: min_len, max_len, len, pattern, prefix, suffix, contains, not_contains, email, hostname, ip, ipv4, ipv6, uri, uri_ref, uuid, tuuid, ulid, address, ip_with_prefixlen, ipv4_with_prefixlen, ipv6_with_prefixlen, ipv4_prefix, ipv6_prefix, well_known_regex, and in/not_in sets
  • bytes rules: byte-level analogues of the string rules above, plus ip, ipv4, ipv6
  • repeated and map rules with per-item and per-key/value sub-rules
  • message rules: required, oneof (at-most-one / exactly-one semantics)
  • oneof rules: required
  • CEL rules: message-level and field-level cel expressions via cel-interpreter
  • Predefined rules on buf.validate.*Rules

All 2872 conformance cases pass — proto2, proto3, and editions 2023.

#[connect_impl] macro for zero-friction Connect integration

Apply #[protovalidate_buffa::connect_impl] to any Connect service impl block and validation is inserted automatically before every handler body — no per-handler discipline needed:

use protovalidate_buffa::Validate; // Validate in scope so the macro can call it

#[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> {
        // req.validate()? is inserted here by the macro.
        // This body only runs for already-validated requests.
        todo!()
    }
}

Connect error adapter

With the default connect feature enabled, convert any ValidationError into a ConnectError::invalid_argument in one call:

let validated = req.validate().map_err(ValidationError::into_connect_error)?;

📦 Crates in this release

Crate Version Purpose
protovalidate-buffa 0.1.0 Runtime: Validate trait, error types, CEL integration, rule helpers, Connect adapter
protovalidate-buffa-macros 0.1.0 #[connect_impl] proc macro (re-exported from the runtime crate)
protoc-gen-protovalidate-buffa 0.1.0 buf generate plugin — emits impl Validate from (buf.validate.*) annotations
protovalidate-buffa-protos 0.1.0 Compiled Rust types for buf/validate/validate.proto

protovalidate-buffa-conformance is also in the workspace but is private (publish = false) and not part of the release surface.


🔬 Conformance

2872 / 2872 (100%) of the upstream protovalidate-conformance suite passes, covering proto2, proto3, and proto editions 2023 — the same harness that all official protovalidate implementations are measured against.


📥 Installation

Library consumers

Add to your Cargo.toml:

[dependencies]
protovalidate-buffa = "0.1.0"

To opt out of the Connect adapter, disable default features:

protovalidate-buffa = { version = "0.1.0", default-features = false }

Codegen plugin

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

protovalidate-buffa-protos-v0.1.0

21 Apr 12:23
bf2c05c

Choose a tag to compare

protovalidate-buffa-protos v0.1.0 — Initial Release 🎉

We're thrilled to ship the first release of protovalidate-buffa — a complete, static-codegen implementation of Buf's protovalidate for the buffa Rust protobuf runtime. From day one it achieves 2872 / 2872 (100%) conformance against the upstream protovalidate-conformance harness across proto2, proto3, and editions 2023. No reflection, no runtime descriptor lookup — just fast, inlinable field walks generated directly from your .proto annotations.


✨ What's Included

Four published crates

Crate Version Role
protovalidate-buffa 0.1.0 Runtime: Validate trait, typed error model, CEL integration, Connect adapter
protovalidate-buffa-macros 0.1.0 #[connect_impl] proc macro
protoc-gen-protovalidate-buffa 0.1.0 buf generate codegen plugin
protovalidate-buffa-protos 0.1.0 Compiled Rust types for buf/validate/validate.proto

Full rule coverage

Every rule family in the standard-rules catalogue is implemented, including predefined rules on buf.validate.*Rules. That's the number behind the 2872 / 2872 conformance score.

Static codegen — zero reflection overhead

The protoc-gen-protovalidate-buffa plugin walks every (buf.validate.*) extension at buf generate time and emits a pure-Rust impl Validate for YourMessage block. At runtime, validate() is a direct struct-field walk that LLVM can inline — no dynamic dispatch, no descriptor registry, no HashMap lookups.

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

message CreateUserRequest {
  string email = 1 [(buf.validate.field).string = {
    min_len: 5, max_len: 254, email: true
  }];
  int32 age = 2 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];
}
// Generated by protoc-gen-protovalidate-buffa — nothing to write by hand.
impl Validate for CreateUserRequest {
    fn validate(&self) -> Result<(), ValidationError> { /* ... */ }
}

Schema-aware compile errors surfaced at codegen time

Rule / field type mismatches, malformed message.oneof specs, and CEL expressions that reference non-existent fields are caught by the plugin and baked into the generated validate() as a compile_error — surfaced the first time the method is called rather than silently ignored.

Typed, structured ValidationError

Three orthogonal signals, all pattern-matchable:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Schema mismatch baked in at codegen time — indicates a bug in the
        // .proto annotations or a stale generated file.
        eprintln!("schema 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).
        eprintln!("runtime error: {}", e.runtime_error.unwrap());
    }
    Err(e) => {
        // One or more field rule violations — the common case.
        for v in &e.violations {
            eprintln!("{}: {}", v.field, v.message);
        }
    }
}

CEL expression support

Message-level and field-level CEL rules are fully supported via cel-interpreter. CEL runtime errors (type mismatches, division by zero) are lifted into the typed runtime_error field so callers match on structure, never on rule_id string prefixes.

#[connect_impl] — zero-discipline validation for every RPC

Apply the macro once to a Connect service impl block and req.validate()? is inserted at the top of every handler automatically. Handlers see only already-validated requests; no per-method discipline required.

use protovalidate_buffa::Validate;

#[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> {
        // Validation already ran — body only executes for valid requests.
        todo!()
    }
}

The optional connect feature (enabled by default) also provides ValidationError::into_connect_error(), which maps violations directly to a Connect InvalidArgument error with structured field paths.

Proto2, proto3, and editions 2023

All three syntax flavours are tested end-to-end against the upstream conformance suite. 2872 / 2872 cases pass.


🚀 Getting Started

Add the runtime

# Cargo.toml
[dependencies]
protovalidate-buffa = "0.1.0"

Install the codegen plugin

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

Wire into buf.gen.yaml

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

Then annotate your .proto messages with (buf.validate.*) rules, run buf generate, and include the generated files in your crate. See the upstream quick start for the full rule vocabulary and the project README for the Rust-specific wiring.


Dual-licensed under Apache-2.0 or MIT at your option.

protovalidate-buffa-macros-v0.1.0

21 Apr 12:23
bf2c05c

Choose a tag to compare

🎉 protovalidate-buffa v0.1.0 — Initial Release

We're thrilled to ship the first release of protovalidate-buffa: a complete, static-codegen implementation of Buf's protovalidate for the buffa Rust protobuf runtime. Out of the gate, it passes 2872 / 2872 (100%) of the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023 — every rule family in the standard catalogue, predefined rules included. If you've been waiting for first-class protovalidate support in a buffa-based service, this is it.


✨ What's Included

protovalidate-buffa — Runtime library

The core crate exposes the Validate trait, the structured ValidationError type, and the Violation / FieldPath types that mirror the upstream proto shape.

ValidationError carries three orthogonal, typed signals — no stringly-typed rule_id prefix sniffing required:

pub struct ValidationError {
    /// Per-field rule failures (the common case).
    pub violations: Vec<Violation>,
    /// Schema mismatch detected at codegen time (type mismatch, bad oneof spec,
    /// CEL referencing a non-existent field).
    pub compile_error: Option<String>,
    /// Rule precondition failed at runtime (e.g. bytes.pattern on non-UTF-8 input).
    pub runtime_error: Option<String>,
}

Match on the fields you care about:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // Bug in the .proto annotations or codegen — shouldn't reach production.
        tracing::error!(compile_error = %e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // Rule precondition failed (e.g. non-UTF-8 bytes under a pattern rule).
        return Err(ConnectError::internal(e.runtime_error.unwrap()));
    }
    Err(e) => {
        // One or more field violations — return them to the caller.
        return Err(e.into_connect_error());
    }
}

Enable the connect feature to get ValidationError::into_connect_error(), which maps violations to a Connect InvalidArgument error automatically.

CEL expression support is provided via cel-interpreter. Rule helpers (UUID, ULID, IP, URI, hostname, and friends) are in the rules module.


protovalidate-buffa-macros#[connect_impl]

The #[connect_impl] attribute macro inserts req.validate()? at the top of every handler in a Connect service impl block — no per-handler discipline required, no missed RPCs.

use protovalidate_buffa::connect_impl;

#[connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: OwnedView<pb::CreateUserRequestView<'static>>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // req.validate()? is injected automatically — the body only sees
        // requests that have already passed all proto-annotated rules.
        let user = self.db.insert_user(&request).await?;
        Ok((pb::CreateUserResponse { user: user.into() }, ctx))
    }
}

connect_impl is re-exported from protovalidate-buffa so you only need one dependency in Cargo.toml.


protoc-gen-protovalidate-buffa — Codegen plugin

The buf generate plugin reads (buf.validate.*) extensions from your .proto descriptors and emits pure-Rust impl Validate for YourMessage blocks. Every scalar, message, repeated, map, oneof, and wrapper field type is handled; predefined rules on buf.validate.*Rules messages are walked end-to-end.

Add it to your buf.gen.yaml:

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

Annotate your protos (full rule vocabulary at buf.build/docs/protovalidate):

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

message CreateUserRequest {
  string email = 1 [(buf.validate.field).string = {
    min_len: 5,
    max_len: 254,
    email: true
  }];
  int32 age = 2 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];
}

Schema-level mismatches (rule type / field type mismatch, bad message.oneof spec, CEL referencing a non-existent field) are caught at codegen time and surface as compile_error violations the first time validate() is called — never silently at runtime.


protovalidate-buffa-protos — Compiled buf/validate/validate.proto types

Vendored Rust types for the buf/validate/validate.proto descriptor, consumed internally by the plugin. Available as a separate crate if you need to work directly with the validation proto types.


🏆 Conformance

2872 / 2872 (100%) — every test case in the upstream protovalidate-conformance suite passes, covering:

  • proto2 (required fields, groups, extensions)
  • proto3 (default semantics, optional fields)
  • editions 2023

📦 Installation

Library consumers — add to Cargo.toml:

[dependencies]
protovalidate-buffa = "0.1.0"

To enable the Connect error adapter:

protovalidate-buffa = { version = "0.1.0", features = ["connect"] }

Codegen plugin — install from source:

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

protoc-gen-protovalidate-buffa-v0.1.0

21 Apr 12:23
bf2c05c

Choose a tag to compare

protoc-gen-protovalidate-buffa v0.1.0 🎉

We're thrilled to announce the first public release of protovalidate-buffa — a full static-codegen implementation of Buf's protovalidate for the buffa Rust protobuf runtime. This inaugural release ships with 2872 / 2872 (100%) conformance against the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023 — every standard rule, CEL expression, predefined rule, and message.oneof constraint, validated end-to-end with no asterisks. If you've been waiting for first-class protovalidate support in the buffa ecosystem, the wait is over.


✨ What's Included

protovalidate-buffa — runtime library

The heart of the system. Exposes the Validate trait that generated code implements, and a structured ValidationError type with three orthogonal diagnostic slots:

pub struct ValidationError {
    pub violations: Vec<Violation>,       // per-field rule failures (the common path)
    pub compile_error: Option<String>,    // schema mismatch caught at codegen time
    pub runtime_error: Option<String>,    // rule precondition failure at runtime
}

Match on typed fields — not on rule_id string prefixes — so your error-handling stays robust across rule-set changes:

match req.validate() {
    Ok(()) => { /* proceed */ }
    Err(e) if e.compile_error.is_some() => {
        // A rule/field type mismatch slipped through — report the codegen bug.
        eprintln!("schema error: {}", e.compile_error.unwrap());
    }
    Err(e) if e.runtime_error.is_some() => {
        // e.g. bytes.pattern applied to non-UTF-8 input
        eprintln!("runtime error: {}", e.runtime_error.unwrap());
    }
    Err(e) => {
        // Normal validation failures: iterate structured violations.
        for v in &e.violations {
            eprintln!("field `{}` failed `{}`: {}", v.field, v.rule_id, v.message);
        }
    }
}

Violation and FieldPath mirror the upstream proto shape. The connect feature (on by default) provides ValidationError::into_connect_error, which maps failures to a well-formed InvalidArgument Connect error ready to return from any RPC handler.

CEL rules are backed by cel-interpreter. Rule helpers for UUID/ULID/IP/URI/hostname validation are thin wrappers over battle-tested crates (uuid, ulid, ipnet, fluent-uri) and are re-exported so downstream crates need only depend on protovalidate-buffa.

protovalidate-buffa-macros#[connect_impl]

An attribute macro that weaves req.validate()? into the top of every handler in a Connect service impl, eliminating the risk of forgetting validation on any single endpoint:

use protovalidate_buffa::connect_impl;

#[connect_impl]
impl UserService for UserServiceImpl {
    async fn create_user(
        &self,
        ctx: Context,
        request: OwnedView<pb::CreateUserRequestView<'static>>,
    ) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
        // req.validate()? is automatically inserted here.
        // By the time your business logic runs, the request is already valid.
        todo!()
    }
}

Re-exported from the runtime crate — no extra dependency needed.

protoc-gen-protovalidate-buffabuf generate plugin

The codegen plugin. Point buf generate at it, annotate your .proto files with (buf.validate.*) extensions, and the plugin emits pure-Rust impl Validate blocks — one output .rs file per source .proto file, formatted by prettyplease.

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

message CreateUserRequest {
  string email = 1 [(buf.validate.field).string = {
    min_len: 5, max_len: 254, email: true
  }];
  int32 age = 2 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];

  option (buf.validate.message).cel = {
    id: "create_user.age_email_domain",
    message: "users under 18 must use a parent-supervised domain",
    expression: "this.age >= 18 || this.email.endsWith('@kids.example.com')"
  };
}

The plugin walks the full (buf.validate.*) extension tree — field rules, message-level CEL, message.oneof constraints, and predefined rules on buf.validate.*Rules types. Rule/field type mismatches and CEL expressions that reference non-existent fields are caught at codegen time and surface as compile_error at runtime rather than silently passing.

buf.gen.yaml integration:

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

protovalidate-buffa-protos — compiled validate types

Pre-compiled Rust types for buf/validate/validate.proto, consumed by the plugin. Available as a standalone crate if you need to work with the validation proto schema directly.


🏆 Conformance: 2872 / 2872 (100%)

The full upstream protovalidate-conformance suite passes — 2872 test cases covering proto2, proto3, and editions 2023 with no skips or known gaps. This covers every standard rule family, nested message validation, repeated and map field rules, message.oneof constraints, CEL expressions (including cross-field references and custom AsCelValue implementations), and predefined rules.


Why static codegen instead of reflection?

Existing Rust protovalidate implementations (prost-protovalidate, protocheck, protify) all target prost. buffa has a fundamentally different runtime model — zero-copy views, static types, no dynamic-message reflection — making prost-based validators incompatible. The codegen approach also has intrinsic advantages:

  • No runtime descriptor lookup. Every validate() is a direct struct field walk that LLVM can inline.
  • Schema-aware compile errors. Mismatches are surfaced at buf generate time, not at first call in production.

📦 Installation

Library (runtime + macros)

Add to your Cargo.toml:

[dependencies]
protovalidate-buffa = "0.1.0"

Codegen plugin

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

Dual-licensed under Apache-2.0 or MIT at your option. Bug reports, conformance gaps, and pull requests are all very welcome — see CONTRIBUTING to get started.