feat(buffa-codegen): gate generated json/views/text impls on crate features#117
Conversation
…atures
Add `CodeGenConfig::gate_impls_on_crate_features: bool` (default `false`).
When `true`, generated impls controlled by `generate_json`,
`generate_views`, and `generate_text` are wrapped in
`#[cfg(feature = "json" | "views" | "text")]` (or `#[cfg_attr(...)]` for
derives and field attributes) instead of being emitted unconditionally.
The consuming crate defines matching Cargo features and enables the
corresponding runtime support behind them.
The `generate_*` flags still control *whether* an impl kind is emitted;
the new flag only controls *how*. `generate_arbitrary` is unchanged — it
was always `cfg_attr`-gated on `feature = "arbitrary"`.
Mechanism: `feature_gates.rs` adds a `FeatureGates` struct (resolved once
per config) and four helpers — `cfg_block` for a single item/statement
(with a `debug_assert` against multi-item misuse), `cfg_const_block` for
sibling impls (`#[cfg(...)] const _: () = { ... };`), `cfg_block_any` for
items that exist iff *any* of a set of modes is on (e.g.
`register_types`, gated on `any(json, text)`), and `cfg_attr` for derives
and helper attributes.
Wired through:
- `message.rs`: struct serde derives + `#[serde(default)]`, field
`#[serde(...)]` attrs, oneof `#[serde(flatten)]`, unknown-fields
`#[serde(skip)]`, custom `Deserialize` impl, `ProtoElemJson` impl,
JSON/Text Any consts, view re-exports. The `__FooExtJson` wrapper
struct (and Deref/DerefMut/From) become unconditional — a struct
field's type can't change behind a `cfg`, and encode/decode reach the
inner `UnknownFields` through `DerefMut` either way; only its
`Serialize`/`Deserialize` impls are gated.
- `enumeration.rs`: enum serde impls in a `const _: () = { ... };` block.
- `oneof.rs`: oneof `Serialize` impl.
- `view.rs`: `impl Serialize for FooView`.
- `impl_text.rs`: `impl TextFormat`, including the `MessageSet` early
return.
- `extension.rs`: JSON/Text extension consts.
- `lib.rs`: `register_types` (per-statement gates plus an
`any(json, text)` gate on the fn and its package-root re-export), the
`__buffa::view` module in the stitcher.
`task gen-bootstrap-types` and `task gen-wkt-types` are no-ops — the
default produces byte-identical output.
Tests: helper-fn unit tests in `feature_gates.rs`; integration tests in
`tests/feature_gating.rs` asserting cfg attributes appear in the right
places, that ungated output has zero `feature = ...` cfgs, that disabling
a `generate_*` flag suppresses both the impl and its gate, and a
`syn::parse_file` smoke test on the gated output. Compile coverage with
each feature combination is deferred to the `buffa-descriptor` /
`buffa-types` regen PR (#113), which adds the actual feature definitions.
|
All contributors have signed the CLA ✍️ ✅ |
…trix
The string-match assertions in `tests/feature_gating.rs` and the
`syn::parse_file` smoke test verify the *shape* of gated output but not
that it resolves. A `cfg`-gated item referenced from an ungated context
is valid Rust syntax — only the compiler catches it.
Add `tests/feature_gating_compile.rs`: generate all 24 protoc-buildable
`buffa-test/protos/*.proto` files (editions, oneofs, extensions, maps,
groups, MessageSet, cross-package, prelude shadows, …) with
`gate_impls_on_crate_features = true`, lay them out in a temp crate that
defines `json` / `views` / `text` features, and `cargo check` each of the
8 `{json, views, text}` subsets. Marked `#[ignore]` so the default
workspace test stays fast; CI runs it explicitly via a new
`Feature-gating compile matrix` step.
The test caught a real bug: top-level message view re-exports
(`pub use self::__buffa::view::FooView;`, emitted in `lib.rs` rather than
`message.rs` where the nested-message and view-oneof re-exports live)
were not `cfg`-gated. The string assertions and syntax check missed it
because the re-export *is* valid Rust — the failure is a name-resolution
error against a `view` module that's been cfg'd out. Fixed and pinned
with `gated_view_reexports_are_cfg_blocked`.
Update: compile-matrix coveragePushed a second commit adding The new test generates all 24 protoc-buildable It caught a real bug the existing tests missed: top-level message view re-exports ( Coverage now:
|
Summary
Adds the codegen mechanism that lets
buffa-descriptor(and, if we choose to later,buffa-types) ship every generated impl while keeping the codegen toolchain (buffa-codegen/buffa-build/protoc-gen-buffa) lean. #118 (stacked on this PR) does thebuffa-descriptorregen and dep cleanup. Tracked in #113.What
CodeGenConfig::gate_impls_on_crate_features: bool(defaultfalse). Whentrue, generated impls controlled bygenerate_json,generate_views, andgenerate_textare wrapped in#[cfg(feature = "json" | "views" | "text")](or#[cfg_attr(...)]for derives and field attributes) instead of being emitted unconditionally. The consuming crate defines matching Cargo features:The
generate_*flags still control whether an impl kind is emitted; the new flag only controls how.generate_arbitraryis unchanged — it was alwayscfg_attr-gated. Defaultfalseis a no-op:task gen-bootstrap-types/task gen-wkt-typesproduce byte-identical output.Mechanism
feature_gates.rsadds aFeatureGatesstruct (resolved once per config fromgate_impls_on_crate_features∧generate_*) and fourpub(crate)helpers:cfg_block(tokens, gate)—#[cfg(feature = "x")] <item>for a single item or statement, with adebug_assertagainst multi-item misuse (a#[cfg]outer attribute attaches only to the next item, so trailing siblings would silently leak ungated).cfg_const_block(tokens, gate)—#[cfg(feature = "x")] const _: () = { <impls> };for sibling impls (the anonymous const is itself a single item; trait impls inside register globally).cfg_block_any(tokens, &[gates])—#[cfg(any(feature = "a", feature = "b"))]for items that exist iff any of a set of modes is enabled. Used byregister_types, which registers both JSON and text entries.cfg_attr(attr_body, gate)—#[cfg_attr(feature = "x", <attr>)]or#[<attr>]for derives and helper attributes that must only apply when the feature is on (a#[serde(...)]field attribute on a struct that doesn't#[derive(Serialize)]is a hard compile error).What's gated
#[derive(::serde::Serialize, ::serde::Deserialize)],#[serde(default)]cfg_attr(feature = "json", ...)message.rs#[serde(rename = ...)], oneof#[serde(flatten)], unknown-fields#[serde(skip)]cfg_attrmessage.rsimpl Deserialize,impl ProtoElemJson, oneofimpl Serialize, viewimpl Serializecfg(feature = "json")message.rs,oneof.rs,view.rsimpl Serialize/Deserialize/ProtoElemJsoncfg(feature = "json")on aconst _: () = { ... };enumeration.rs__FooExtJsonwrapper'sSerialize/Deserializecfg(feature = "json"); the struct + Deref/DerefMut/From stay unconditional — encode/decode reach the innerUnknownFieldsthroughDerefMutand a struct field's type can't change behind acfgmessage.rs__*_JSON_ANY/__*_TEXT_ANY), JSON/Text extension constscfgmessage.rs,extension.rsimpl ::buffa::text::TextFormat(including theMessageSetearly return)cfg(feature = "text")impl_text.rs__buffa::viewmodule in the stitcher, natural-pathpub use ... FooView;cfg(feature = "views")lib.rs,message.rsregister_typesfn body statements, the fn declaration, and its package-root re-exportcfg;cfg(any(feature = "json", feature = "text"))on the fn and re-exportlib.rsTests
feature_gates.rshelper unit tests:for_configresolution,cfg_block/cfg_const_block/cfg_attr/cfg_block_anyshapes, the debug_assert against multi-itemcfg_blockmisuse.tests/feature_gating.rsintegration tests against generated output: cfg attributes appear in the right places; ungated output has zerofeature = "json"|"views"|"text"references; disabling agenerate_*flag suppresses both the impl and its gate; the__ExtJsonwrapper struct is unconditional but its serde impls are gated;register_typescarriesany(json, text)gates on both the fn and its re-export;syn::parse_filesmoke test for structural validity.Notes for review
rust-code-reviewerran. Findings addressed:MessageSetearly return inimpl_text.rsbypassed the gate — fixed.register_typesreferencedTypeRegistryunconditionally when its body could be all-cfg'd-out — gated the fn and re-export onany(json, text).# — fixed.squash()helper and normalised them.cfg_blockhad no enforcement against multi-item misuse —debug_assertadded.generate_json = false+ gating on — added.syn::parse_filesmoke test as a structural floor; this PR adds afeature_gating_compilematrix test against thebuffa-test/protos/fixture set, and feat(buffa-descriptor): regen with views/json/text/arbitrary behind crate features #118 (thebuffa-descriptorregen) is verified end-to-end againstbufbuild/registry/bufbuild/bufwith each feature combo.Follow-ups (separate PRs)
buffa-descriptorwithgate_impls_on_crate_features = true, definejson/views/text/arbitraryfeatures in itsCargo.toml, and dep on it frombuffa-codegenwithdefault-features = false. Closes Feature-gated codegen impls + buffa-descriptor regen for descriptor message types as fields #113.buffa-buildbuilder method +protoc-gen-buffaplugin opt to expose the knob for downstream codegen integrations.buffa-typesshould also be regen'd with the gate. Less urgent — WKT views/text are the only unconditional impls that would benefit; JSON is hand-written andarbitraryis alreadycfg_attr-gated.buffa-descriptorwith each feature combination (thefeature_gating_compiletest in this PR covers the codegen-output shape; feat(buffa-descriptor): regen with views/json/text/arbitrary behind crate features #118'scargo checkmatrix coversbuffa-descriptoritself; a CI step would pin both).