Releases: mathematic-inc/protovalidate-buffa
protovalidate-buffa-protos-v0.1.2
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-interpreter → cel (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-buffaThen wire it into your buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allFull changelog: protovalidate-buffa-protos-v0.1.1...protovalidate-buffa-protos-v0.1.2
protoc-gen-protovalidate-buffa-v0.1.2
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-buffaThen wire it into your buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allAnd 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
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 cel — protovalidate-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-buffaThen wire it into buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allThat'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
🎉 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: allThen 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 stringrules: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, andin/not_insetsbytesrules: byte-level analogues of the string rules above, plusip,ipv4,ipv6repeatedandmaprules with per-item and per-key/value sub-rulesmessagerules:required,oneof(at-most-one / exactly-one semantics)oneofrules:required- CEL rules: message-level and field-level
celexpressions viacel-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-buffaprotovalidate-buffa-protos-v0.1.0
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-buffaWire into buf.gen.yaml
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allThen 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
🎉 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: allAnnotate 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,
optionalfields) - 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-buffaprotoc-gen-protovalidate-buffa-v0.1.0
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-buffa — buf 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: allprotovalidate-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 generatetime, 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-buffaDual-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.