Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Passes the full text-format conformance suite (883/883).

## 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 @@ -115,6 +115,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