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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
620 changes: 172 additions & 448 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [

[workspace.package]
edition = "2024"
rust-version = "1.95"
rust-version = "1.88"
license = "Apache-2.0 OR MIT"
authors = ["Mathematic Inc"]
repository = "https://github.com/mathematic-inc/protovalidate-buffa"
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ For what the rules *mean*, the full rule catalogue, CEL semantics, and design do

## Why a different crate?

Existing Rust implementations of protovalidate (`prost-protovalidate`, `protocheck`, `protify`) all target [prost]. buffa has a different runtime model — zero-copy views, static types, no dynamic-message reflection — so prost-based validators are incompatible. This repo fills that gap.
Existing Rust implementations of protovalidate (`prost-protovalidate`, `protocheck`, `protify`) all target [prost]. buffa has a different runtime model — two-tier owned/borrowed types with zero-copy views, and static generated types rather than descriptor-driven dynamic messages — so prost-based validators are incompatible. This repo fills that gap.

Compared to reflection-based implementations, the codegen approach has two characteristics:

Expand All @@ -40,6 +40,14 @@ Compared to reflection-based implementations, the codegen approach has two chara

`protovalidate-buffa-conformance` also lives in this workspace but is private (`publish = false`) — see [its README](crates/protovalidate-buffa-conformance/README.md) for the conformance test-run flow.

## Compatibility

This workspace currently targets **buffa 0.8**, **connectrpc 0.8**, and **Rust 1.88+** (edition 2024).

The emitted validators reference buffa's generated-code shape directly (field placement, map and view types), so the plugin and your `buffa-build` output must agree on the buffa minor version. buffa is pre-1.0 and treats **minor bumps as breaking**, so upgrading buffa generally means upgrading this crate in lockstep.

buffa 0.9 is intentionally not adopted yet: `connectrpc` 0.8 still depends on buffa `^0.8`, so moving this workspace to 0.9 would link two incompatible buffa versions and break the default `connect` feature for downstream handlers. This crate will move to buffa 0.9 once connectrpc does.

## Supported rules

Every rule family in the upstream [standard-rules catalogue](https://buf.build/docs/protovalidate/schemas/standard-rules/) plus [predefined rules](https://buf.build/docs/protovalidate/schemas/custom-rules/#predefined-rules) is implemented — that's what the 2872 / 2872 conformance number above is measuring. See the upstream docs for semantics; this repo doesn't maintain a parallel list.
Expand Down
10 changes: 5 additions & 5 deletions crates/protoc-gen-protovalidate-buffa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ name = "protoc-gen-protovalidate-buffa"
path = "src/main.rs"

[dependencies]
buffa = "0.7"
buffa-codegen = "0.7"
buffa = "0.8"
buffa-codegen = "0.8"
protovalidate-buffa-protos = { path = "../protovalidate-buffa-protos", version = "0.5.0" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
prettyplease = "0.2"
syn = { version = "3", features = ["full"] }
prettyplease = "0.3"
anyhow = "1"
# Used parse-only — the runtime interpreter, regex pattern matching, and
# chrono-based timestamp handling all live on the runtime side or get
# transpiled away at codegen time, so the plugin can opt out of the
# `regex` and `chrono` default features.
cel = { version = "0.13", default-features = false }
cel = { version = "0.14", default-features = false }
12 changes: 6 additions & 6 deletions crates/protovalidate-buffa-conformance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ name = "protovalidate-buffa-conformance"
path = "src/main.rs"

[dependencies]
buffa = "0.7"
buffa = "0.8"
protovalidate-buffa = { path = "../protovalidate-buffa", version = "0.5.1", default-features = false, features = ["tz"] }
protovalidate-buffa-protos = { path = "../protovalidate-buffa-protos", version = "0.5.0" }
anyhow = "1"
Expand All @@ -33,12 +33,12 @@ regex = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }

[build-dependencies]
buffa-build = "0.7"
buffa-build = "0.8"
protoc-gen-protovalidate-buffa = { path = "../protoc-gen-protovalidate-buffa", version = "0.5.1" }
buffa = "0.7"
buffa-codegen = "0.7"
buffa = "0.8"
buffa-codegen = "0.8"
anyhow = "1"
prettyplease = "0.2"
syn = { version = "2", features = ["full"] }
prettyplease = "0.3"
syn = { version = "3", features = ["full"] }
quote = "1"
proc-macro2 = "1"
12 changes: 5 additions & 7 deletions crates/protovalidate-buffa-conformance/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@ fn main() -> anyhow::Result<()> {
io::stdin().read_to_end(&mut input)?;
let request = TestConformanceRequest::decode_from_slice(&input)?;

let mut results: std::collections::HashMap<String, TestResult> =
std::collections::HashMap::new();
// Build into the response's own map field rather than naming the type:
// buffa generates map fields with its `foldhash` hasher, not the
// `std::hash::RandomState` that a bare `HashMap<K, V>` would default to.
let mut response = TestConformanceResponse::default();
for (name, any) in &request.cases {
results.insert(name.clone(), run_case(any));
response.results.insert(name.clone(), run_case(any));
}

let response = TestConformanceResponse {
results,
..Default::default()
};
let mut out = Vec::new();
response.encode(&mut out);
io::stdout().write_all(&out)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/protovalidate-buffa-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
syn = { version = "3", features = ["full"] }
4 changes: 2 additions & 2 deletions crates/protovalidate-buffa-protos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ keywords.workspace = true
workspace = true

[dependencies]
buffa = "0.7"
buffa = "0.8"

[build-dependencies]
buffa-build = "0.7"
buffa-build = "0.8"
6 changes: 3 additions & 3 deletions crates/protovalidate-buffa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ connect = ["dep:connectrpc"]
tz = ["dep:chrono-tz"]

[dependencies]
buffa = "0.7"
buffa = "0.8"
protovalidate-buffa-macros = { path = "../protovalidate-buffa-macros", version = "0.3.1" }
regex = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
chrono-tz = { version = "0.10", optional = true }
uuid = "1"
ulid = "1"
ulid = "3"
ipnet = "2"
fluent-uri = "0.4"
connectrpc = { version = "0.7", default-features = false, optional = true }
connectrpc = { version = "0.8", default-features = false, optional = true }
percent-encoding = "2"
http = "1"
37 changes: 20 additions & 17 deletions crates/protovalidate-buffa/src/cel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,25 @@ pub fn timestamp_from_secs_nanos(
s.fixed_offset()
}

/// Split a duration literal's magnitude from its unit suffix.
///
/// Suffixes are tried longest-first so `ns` / `us` / `ms` win over the
/// bare `s` they end with. `µs` normalizes to `us`.
fn split_unit(rest: &str) -> Option<(&str, &'static str)> {
const SUFFIXES: [(&str, &str); 7] = [
("ns", "ns"),
("us", "us"),
("µs", "us"),
("ms", "ms"),
("s", "s"),
("m", "m"),
("h", "h"),
];
SUFFIXES
.into_iter()
.find_map(|(suffix, unit)| rest.strip_suffix(suffix).map(|stripped| (stripped, unit)))
}

/// Parse a CEL `duration("…")` string into a `chrono::Duration`.
///
/// Accepts the protobuf duration grammar: an optional sign, a decimal
Expand All @@ -159,23 +178,7 @@ pub fn parse_duration(s: &str) -> Option<chrono::Duration> {
_ => (1i64, s),
};
// Find the unit suffix.
let (num_str, unit) = if let Some(stripped) = rest.strip_suffix("ns") {
(stripped, "ns")
} else if let Some(stripped) = rest.strip_suffix("us") {
(stripped, "us")
} else if let Some(stripped) = rest.strip_suffix("µs") {
(stripped, "us")
} else if let Some(stripped) = rest.strip_suffix("ms") {
(stripped, "ms")
} else if let Some(stripped) = rest.strip_suffix('s') {
(stripped, "s")
} else if let Some(stripped) = rest.strip_suffix('m') {
(stripped, "m")
} else if let Some(stripped) = rest.strip_suffix('h') {
(stripped, "h")
} else {
return None;
};
let (num_str, unit) = split_unit(rest)?;
let value: f64 = num_str.parse().ok()?;
let nanos_total: f64 = match unit {
"ns" => value,
Expand Down
Loading