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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ fuzz/corpus/

# Local tooling extracted from the tools image (task install-protoc).
/.local/

# Local editor settings
/.claude/settings.local.json
26 changes: 21 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Deprecated

- **`set_any_registry`, `set_extension_registry`** — use
`buffa::json_registry::set_json_registry` instead, which installs both halves
`buffa::type_registry::set_type_registry` instead, which installs all maps
in one call. The deprecated functions still work.
- **`AnyTypeEntry` → `JsonAnyEntry`, `ExtensionRegistryEntry` → `JsonExtEntry`.**
Type aliases for one release cycle. The text-format fields have moved to
separate `TextAnyEntry` / `TextExtEntry` structs in `type_registry`.

### Added

Expand All @@ -35,16 +38,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
proto2 `[default = ...]` on extension declarations, and MessageSet wire
format behind `CodeGenConfig::allow_message_set`. See the
[Extensions section of the user guide](docs/guide.md#extensions-custom-options).
- **`JsonRegistry`** — unified JSON registry covering both `Any` type entries
and extension entries. Codegen emits `register_json(&mut JsonRegistry)` per
file; call once per generated file, then `set_json_registry(reg)`.
- **`TypeRegistry`** — unified registry covering `Any` type entries and
extension entries for both JSON and text formats. Codegen emits
`register_types(&mut TypeRegistry)` per file; call once per generated file,
then `set_type_registry(reg)`. JSON entries (`JsonAnyEntry`, `JsonExtEntry`)
and text entries (`TextAnyEntry`, `TextExtEntry`) live in feature-split
maps so `json` and `text` are independently enableable.
- **`JsonParseOptions::strict_extension_keys`** — error on unregistered `"[...]"`
JSON keys (default: silently drop, matching pre-0.3 behavior for all unknown
keys).
- **Editions `features.message_encoding = DELIMITED`** — fully supported in
codegen, previously parsed but ignored. Message fields with this feature use
the group wire format (StartGroup/EndGroup) instead of length-prefixed.
- **Conformance:** `TestAllTypesEdition2023` enabled; 5539 → 5549 passing (std).
- **Text format (`textproto`)** — the `buffa::text` module provides
`TextFormat` trait, `TextEncoder`, `TextDecoder`, and `encode_to_string` /
`decode_from_str` conveniences. Enable with `features = ["text"]`
(zero-dependency, `no_std`-compatible) and `Config::generate_text(true)`.
Covers `Any` expansion (`[type.googleapis.com/...] { ... }`), extension
brackets (`[pkg.ext] { ... }`), and group/DELIMITED naming. `Any` expansion
and extension brackets consult the text maps in `TypeRegistry` — the `json`
and `text` features are independently enableable. Passes the full
text-format conformance suite (883/883).
- **Conformance:** `TestAllTypesEdition2023` enabled; binary+JSON 5539 → 5549
passing (std). Text format suite 0 → 883 passing (was entirely skipped).

## [0.2.0] - 2026-03-16

Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ task conformance # now uses the locally-built image
(std, no_std, via-view), each producing two suites:

1. Binary + JSON suite — expects thousands of successes (~5500 std, ~5500 no_std, ~2800 via-view — view mode skips JSON)
2. Text format suite — always `0 successes, 883 skipped` (text format is not supported)
2. Text format suite — 883 successes for std and no_std (the full suite); via-view shows `0 successes, 883 skipped` (views have no `TextFormat` — textproto goes through the owned type via `to_owned_message()`)

So a healthy run shows **6 `CONFORMANCE SUITE PASSED` lines**. The `883 skipped` in the text format suites is expected and correct.
So a healthy run shows **6 `CONFORMANCE SUITE PASSED` lines**.

The Dockerfile builds **two binaries**: one with default features (std) and one with `--no-default-features` (no_std). The via-view run reuses the std binary with `BUFFA_VIA_VIEW=1` set, routing binary input through `decode_view → to_owned_message → encode` to verify owned/view decoder parity.

**Expected failures** are listed in `conformance/known_failures.txt` (std), `conformance/known_failures_nostd.txt` (no_std), and `conformance/known_failures_view.txt` (via-view). When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it.
**Expected failures** are listed in `conformance/known_failures.txt` (std binary+JSON), `conformance/known_failures_nostd.txt` (no_std binary+JSON), `conformance/known_failures_view.txt` (via-view), and `conformance/known_failures_text.txt` (text format — shared between std and no_std; currently empty). The text list is passed via `--text_format_failure_list` since the runner validates each suite's list independently. When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it.

**Capturing output**: To save per-run logs for analysis, mount a directory and set `CONFORMANCE_OUT`:

Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A pure-Rust Protocol Buffers implementation with first-class [protobuf editions]

## Why buffa?

The Rust ecosystem lacks an actively maintained, pure-Rust library that supports [protobuf editions](https://protobuf.dev/editions/overview/). Buffa fills that gap with a ground-up design that treats editions as the core abstraction. It passes all current binary and JSON protobuf serialization conformance tests.
The Rust ecosystem lacks an actively maintained, pure-Rust library that supports [protobuf editions](https://protobuf.dev/editions/overview/). Buffa fills that gap with a ground-up design that treats editions as the core abstraction. It passes the full protobuf conformance suite — binary, JSON, and text — with zero expected failures.

## Features

Expand All @@ -24,19 +24,18 @@ The Rust ecosystem lacks an actively maintained, pure-Rust library that supports

## Wire formats

buffa supports **binary** and **JSON** protobuf encodings:
buffa supports **binary**, **JSON**, and **text** protobuf encodings:

- **Binary wire format** -- full support for all scalar types, nested messages, repeated/packed fields, maps, oneofs, groups, and unknown fields.

- **Proto3 JSON** -- canonical protobuf JSON mapping via optional `serde` integration. Includes well-known type serialization (Timestamp as RFC 3339, Duration as `"1.5s"`, int64/uint64 as quoted strings, bytes as base64, etc.).

**Text format (`textproto`) is not supported** and is not planned.
- **Text format (`textproto`)** -- the human-readable debug format. Covers `Any` expansion (`[type.googleapis.com/...] { ... }`), extension bracket syntax (`[pkg.ext] { ... }`), and group/DELIMITED fields. `no_std`-compatible.

## Unsupported features

These are intentionally out of scope:

- **Text format (`textproto`)** — not planned. Binary and JSON are the wire formats that matter for RPC and storage.
- **Runtime reflection** (`DynamicMessage`, descriptor-driven introspection) — not planned for 0.1. Buffa is a codegen-first library; if you need schema-agnostic processing, consider preserving unknown fields or using `Any`.
- **Proto2 optional-field getter methods** — `[default = X]` on `optional` fields does not generate `fn field_name(&self) -> T` unwrap-to-default accessors. Custom defaults are applied only to `required` fields via `impl Default`. Optional fields are `Option<T>`; use pattern matching or `.unwrap_or(X)`.
- **Scoped `JsonParseOptions` in `no_std`** — serde's `Deserialize` trait has no context parameter, so runtime options must be passed through ambient state. In `std` builds, [`with_json_parse_options`] provides per-closure, per-thread scoping via a thread-local. In `no_std` builds, [`set_global_json_parse_options`] provides process-wide set-once configuration via a global atomic. The two APIs are mutually exclusive. The `no_std` global supports singular-enum accept-with-default but not repeated/map container filtering (which requires scoped strict-mode override).
Expand Down
45 changes: 45 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,51 @@ tasks:
- rm -f src/gen/context.v1.context.rs src/gen/log.v1.log.rs src/gen/mod.rs
- PATH="{{.ROOT_DIR}}/target/release:$PATH" buf generate

# The examples are independent cargo projects (own Cargo.toml, own target/),
# not workspace members — they declare path deps on the workspace crates to
# mirror a downstream consumer's setup.

build-examples:
desc: Build all example binaries.
cmds:
- cargo build --manifest-path examples/addressbook/Cargo.toml
- cargo build --manifest-path examples/envelope/Cargo.toml
- cargo build --manifest-path examples/logging/Cargo.toml

example-envelope:
desc: >-
Run the extensions demo — binary + JSON roundtrip of custom options,
[default = ...] values, extendee-mismatch panic. Self-contained, no args.
dir: examples/envelope
cmds:
- cargo run

# `dir: {{.USER_WORKING_DIR}}` so file-path arguments resolve relative to
# where `task` was invoked, not the Taskfile's directory. Task defaults to
# running commands from the Taskfile location, which would make
# `task example-addressbook -- dump book.pb` look for ./book.pb in the
# repo root instead of the user's cwd.

example-addressbook:
desc: >-
Run the addressbook CLI. Pass subcommand + args after `--`:
`task example-addressbook -- add book.pb` /
`task example-addressbook -- list book.pb` /
`task example-addressbook -- dump book.pb` (textproto).
dir: '{{.USER_WORKING_DIR}}'
cmds:
- cargo run --manifest-path {{.ROOT_DIR}}/examples/addressbook/Cargo.toml -- {{.CLI_ARGS}}

example-logging:
desc: >-
Run the structured-logging CLI. Pass subcommand + file after `--`:
`task example-logging -- write log.pb` /
`task example-logging -- read log.pb` /
`task example-logging -- filter log.pb WARN`.
dir: '{{.USER_WORKING_DIR}}'
cmds:
- cargo run --manifest-path {{.ROOT_DIR}}/examples/logging/Cargo.toml -- {{.CLI_ARGS}}

build-plugin:
desc: Build the protoc plugins in release mode.
cmds:
Expand Down
11 changes: 11 additions & 0 deletions buffa-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ impl Config {
self
}

/// Enable or disable `impl buffa::text::TextFormat` on generated message
/// structs (default: false).
///
/// When enabled, the downstream crate must enable the `buffa/text`
/// feature for the runtime textproto encoder/decoder.
#[must_use]
pub fn generate_text(mut self, enabled: bool) -> Self {
self.codegen_config.generate_text = enabled;
self
}

/// Enable or disable `#[derive(arbitrary::Arbitrary)]` on generated
/// types (default: false).
///
Expand Down
13 changes: 13 additions & 0 deletions buffa-codegen/src/bin/gen_wkt_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,24 @@ fn main() {
// hand-written in the *_ext.rs modules (Timestamp → RFC3339,
// Duration → "3.000001s", Any → type-URL dispatch, etc.).
// None of the WKTs use derive-serde.
//
// generate_text = true Textproto has no special WKT treatment
// (unlike JSON), so the generated field-by-field impls are
// correct. `buffa/text` is zero-dep — enabled unconditionally
// in buffa-types so no feature-gate wrapping is needed.
//
// emit_register_fn = false All seven WKT files are `include!`d into
// one namespace — seven `register_types` fns would collide. WKTs
// register via the hand-written `register_wkt_types` in
// `any_ext.rs` anyway. Per-message `__*_TEXT_ANY` consts are
// still emitted (harmless `#[doc(hidden)] pub`).
let mut config = buffa_codegen::CodeGenConfig::default();
config.generate_views = true;
config.preserve_unknown_fields = true;
config.generate_arbitrary = true;
config.generate_json = false;
config.generate_text = true;
config.emit_register_fn = false;

let files_to_generate: Vec<String> = WKT_PROTOS.iter().map(|s| s.to_string()).collect();

Expand Down
Loading
Loading