diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f01b80..a89ef4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Breaking changes +- **All generated `*View<'a>` structs gain a `__buffa_cached_size` field** + for the new `ViewEncode` impl. Code that constructs a view literal + without `..Default::default()` will fail to compile; use the trailing + `..Default::default()` per the documented convention. Applies to WKT + view structs in `buffa-types` and to consumer-generated views. - **`google.protobuf.Any.value` is now `::bytes::Bytes` instead of `Vec`.** Makes `Any::clone()` a cheap refcount bump (up to ~170x faster for large payloads) instead of a full memcpy. Call sites constructing an `Any` by hand @@ -15,6 +20,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), directly). Reading `any.value` is unchanged — `Bytes` derefs to `&[u8]`. `buffa-types` now depends on `bytes` unconditionally. +### Added + +- **`ViewEncode<'a>` — serialization from borrowed view types.** Generated + `*View<'a>` types implement `ViewEncode` (whenever views are generated, + i.e. `generate_views(true)`, the default) with the same two-pass + `compute_size`/`write_to` model as `Message`. Views can be constructed + from borrowed `&'a str` / `&'a [u8]` and encoded without intermediate + `String`/`Vec` allocation. Benchmarks: parity on serialize-only; ~6× on + build+encode for a 15-label string-map message. +- **`MapView::new(Vec)` / `From` / `FromIterator`** for constructing + map views directly (for `ViewEncode`). + ## [0.3.0] - 2026-04-01 ### Breaking changes diff --git a/benchmarks/buffa/benches/protobuf.rs b/benchmarks/buffa/benches/protobuf.rs index 617020e..d791ce7 100644 --- a/benchmarks/buffa/benches/protobuf.rs +++ b/benchmarks/buffa/benches/protobuf.rs @@ -1,8 +1,12 @@ -use buffa::{Message, MessageView}; +use buffa::{Message, MessageView, ViewEncode}; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use serde::{de::DeserializeOwned, Serialize}; -use bench_buffa::bench::__buffa::view::*; +use bench_buffa::bench::__buffa::view::{ + analytics_event::PropertyView, AnalyticsEventView, ApiResponseView, LogRecordView, + MediaFrameView, +}; +use bench_buffa::bench::__buffa::{oneof, view::oneof as view_oneof}; use bench_buffa::bench::*; use bench_buffa::benchmarks::BenchmarkDataset; use bench_buffa::proto3::__buffa::view::GoogleMessage1View; @@ -209,6 +213,295 @@ fn bench_media_frame_view(c: &mut Criterion) { group.finish(); } +/// Add `encode_view` to a concrete per-dataset bench group: pre-decode +/// payloads into views, assert wire-compat against owned decode, then bench +/// re-encoding from the views' borrowed fields. The owned `encode` baseline +/// is in [`benchmark_decode`] — same group name, so throughputs sit side by +/// side. +/// +/// Per-dataset functions are concrete (not generic over `V`) because the +/// views borrow from the locally-decoded `dataset.payload`; a `<'a, V>` fn +/// signature can't tie `'a` to a local. Same shape as `decode_view` above. +macro_rules! bench_view_encode { + ($fn_name:ident, $owned:ty, $view:ty, $group:literal, $dataset:literal) => { + fn $fn_name(c: &mut Criterion) { + let dataset = load_dataset(include_bytes!($dataset)); + let bytes = total_payload_bytes(&dataset); + let views: Vec<$view> = dataset + .payload + .iter() + .map(|p| <$view>::decode_view(p).unwrap()) + .collect(); + for (v, p) in views.iter().zip(&dataset.payload) { + let from_view = <$owned>::decode_from_slice(&v.encode_to_vec()).unwrap(); + let from_wire = <$owned>::decode_from_slice(p).unwrap(); + assert!(from_view == from_wire, "view-encode wire mismatch"); + } + let mut group = c.benchmark_group($group); + group.throughput(Throughput::Bytes(bytes)); + group.bench_function("encode_view", |b| { + b.iter(|| { + for v in &views { + criterion::black_box(v.encode_to_vec()); + } + }); + }); + group.finish(); + } + }; +} + +bench_view_encode!( + bench_api_response_view_encode, + ApiResponse, + ApiResponseView, + "buffa/api_response", + "../../datasets/api_response.pb" +); +bench_view_encode!( + bench_log_record_view_encode, + LogRecord, + LogRecordView, + "buffa/log_record", + "../../datasets/log_record.pb" +); +bench_view_encode!( + bench_analytics_event_view_encode, + AnalyticsEvent, + AnalyticsEventView, + "buffa/analytics_event", + "../../datasets/analytics_event.pb" +); +bench_view_encode!( + bench_google_message1_view_encode, + bench_buffa::proto3::GoogleMessage1, + GoogleMessage1View, + "buffa/google_message1_proto3", + "../../datasets/google_message1_proto3.pb" +); +bench_view_encode!( + bench_media_frame_view_encode, + MediaFrame, + MediaFrameView, + "buffa/media_frame", + "../../datasets/media_frame.pb" +); + +/// Build-then-encode benches: unlike `encode`/`encode_view` (which serialize +/// a pre-built struct), these include the cost of populating the message from +/// borrowed source — the per-field `String`/`Vec`/`HashMap` allocs that the +/// view path avoids. Each uses a synthetic fixture representative of the +/// message's shape; both paths populate identical fields, throughput is the +/// encoded length. +/// +/// `bench_build_encode!(fn_name, group, OwnedTy, owned_expr, view_expr)` — +/// the two exprs share the source bindings declared above the macro call. +/// Asserts decode-equivalence (not byte-equality, since `HashMap` vs +/// `MapView` iteration order may differ on the wire). +macro_rules! bench_build_encode { + ($fn_name:ident, $group:literal, $owned_ty:ty, $owned:expr, $view:expr $(,)?) => { + fn $fn_name(c: &mut Criterion) { + let probe = ($owned).encode_to_vec(); + let view_bytes = ($view).encode_to_vec(); + assert_eq!(probe.len(), view_bytes.len(), "fixture encode-len mismatch"); + assert_eq!( + <$owned_ty>::decode_from_slice(&probe).unwrap(), + <$owned_ty>::decode_from_slice(&view_bytes).unwrap(), + "owned/view fixtures must decode-equal" + ); + let mut group = c.benchmark_group($group); + group.throughput(Throughput::Bytes(probe.len() as u64)); + group.bench_function("build_encode", |b| { + b.iter(|| criterion::black_box(($owned).encode_to_vec())); + }); + group.bench_function("build_encode_view", |b| { + b.iter(|| criterion::black_box(($view).encode_to_vec())); + }); + group.finish(); + } + }; +} + +const TAGS: [&str; 5] = ["payments", "us-west-2a", "canary", "v3.14.2", "k8s"]; + +bench_build_encode!( + bench_api_response_build_encode, + "buffa/api_response", + ApiResponse, + ApiResponse { + request_id: 9_001_234_567_890, + status_code: 200, + message: "transaction accepted".into(), + latency_ms: 17.42, + cached: true, + trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".into()), + retry_after_ms: None, + tags: TAGS.iter().map(|s| (*s).into()).collect(), + ..Default::default() + }, + ApiResponseView { + request_id: 9_001_234_567_890, + status_code: 200, + message: "transaction accepted", + latency_ms: 17.42, + cached: true, + trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736"), + retry_after_ms: None, + tags: TAGS.iter().copied().collect(), + ..Default::default() + }, +); + +const LABELS: [(&str, &str); 15] = [ + ("k8s.io/label-key-00", "label-value-0000"), + ("k8s.io/label-key-01", "label-value-0001"), + ("k8s.io/label-key-02", "label-value-0002"), + ("k8s.io/label-key-03", "label-value-0003"), + ("k8s.io/label-key-04", "label-value-0004"), + ("k8s.io/label-key-05", "label-value-0005"), + ("k8s.io/label-key-06", "label-value-0006"), + ("k8s.io/label-key-07", "label-value-0007"), + ("k8s.io/label-key-08", "label-value-0008"), + ("k8s.io/label-key-09", "label-value-0009"), + ("k8s.io/label-key-10", "label-value-0010"), + ("k8s.io/label-key-11", "label-value-0011"), + ("k8s.io/label-key-12", "label-value-0012"), + ("k8s.io/label-key-13", "label-value-0013"), + ("k8s.io/label-key-14", "label-value-0014"), +]; +const LOG_SVC: &str = "inventory-service-2a"; +const LOG_MSG: &str = "GET /api/v1/items?tenant=acme-corp&warehouse=us-west-2a&page=1400 200 17ms"; + +bench_build_encode!( + bench_log_record_build_encode, + "buffa/log_record", + LogRecord, + LogRecord { + service_name: LOG_SVC.into(), + message: LOG_MSG.into(), + labels: LABELS.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(), + ..Default::default() + }, + LogRecordView { + service_name: LOG_SVC, + message: LOG_MSG, + labels: LABELS.iter().copied().collect(), + ..Default::default() + }, +); + +const PROPS: [(&str, &str); 8] = [ + ("page", "/checkout/confirm"), + ("referrer", "https://example.com/cart"), + ("session", "f0e1d2c3b4a59687"), + ("variant", "control"), + ("locale", "en-US"), + ("device", "desktop"), + ("browser", "firefox-125"), + ("ab_bucket", "treatment-7"), +]; + +// `sections` (recursive Nested) omitted: building nested views means a +// `Box` per child — that conflates the alloc-avoidance signal +// with the documented `MessageFieldView` boxing follow-up. +bench_build_encode!( + bench_analytics_event_build_encode, + "buffa/analytics_event", + AnalyticsEvent, + AnalyticsEvent { + event_id: "evt_01HW3K9QXAMPLE".into(), + timestamp: 1_700_000_000_000, + user_id: "usr_8f7e6d5c4b3a2910".into(), + properties: PROPS + .iter() + .map(|(k, v)| analytics_event::Property { + key: (*k).into(), + value: Some(oneof::analytics_event::property::Value::StringValue( + (*v).into(), + )), + ..Default::default() + }) + .collect(), + ..Default::default() + }, + AnalyticsEventView { + event_id: "evt_01HW3K9QXAMPLE", + timestamp: 1_700_000_000_000, + user_id: "usr_8f7e6d5c4b3a2910", + properties: PROPS + .iter() + .map(|(k, v)| PropertyView { + key: k, + value: Some(view_oneof::analytics_event::property::Value::StringValue(v)), + ..Default::default() + }) + .collect(), + ..Default::default() + }, +); + +bench_build_encode!( + bench_google_message1_build_encode, + "buffa/google_message1_proto3", + bench_buffa::proto3::GoogleMessage1, + bench_buffa::proto3::GoogleMessage1 { + field1: "the quick brown fox".into(), + field9: "jumps over the lazy dog".into(), + field2: 42, + field3: 17, + field6: 9001, + field22: 1_234_567_890_123, + field12: true, + field14: true, + field100: 100, + field101: 101, + ..Default::default() + }, + GoogleMessage1View { + field1: "the quick brown fox", + field9: "jumps over the lazy dog", + field2: 42, + field3: 17, + field6: 9001, + field22: 1_234_567_890_123, + field12: true, + field14: true, + field100: 100, + field101: 101, + ..Default::default() + }, +); + +static MF_BODY: [u8; 4096] = [0xAB; 4096]; +static MF_CHUNKS: [[u8; 1024]; 4] = [[0xC0; 1024], [0xC1; 1024], [0xC2; 1024], [0xC3; 1024]]; +static MF_ATT_A: [u8; 512] = [0xA0; 512]; +static MF_ATT_B: [u8; 768] = [0xB0; 768]; +const MF_ATTACH: [(&str, &[u8]); 2] = [("thumbnail", &MF_ATT_A), ("metadata", &MF_ATT_B)]; + +bench_build_encode!( + bench_media_frame_build_encode, + "buffa/media_frame", + MediaFrame, + MediaFrame { + frame_id: "frame-001a2b3c".into(), + timestamp_nanos: 1_700_000_000_000_000_000, + content_type: "video/h264".into(), + body: MF_BODY.to_vec(), + chunks: MF_CHUNKS.iter().map(|c| c.to_vec()).collect(), + attachments: MF_ATTACH.iter().map(|(k, v)| ((*k).into(), v.to_vec())).collect(), + ..Default::default() + }, + MediaFrameView { + frame_id: "frame-001a2b3c", + timestamp_nanos: 1_700_000_000_000_000_000, + content_type: "video/h264", + body: &MF_BODY, + chunks: MF_CHUNKS.iter().map(|c| &c[..]).collect(), + attachments: MF_ATTACH.iter().copied().collect(), + ..Default::default() + }, +); + fn bench_api_response(c: &mut Criterion) { benchmark_decode::( c, @@ -305,6 +598,16 @@ criterion_group!( bench_analytics_event_view, bench_google_message1_view, bench_media_frame_view, + bench_api_response_view_encode, + bench_log_record_view_encode, + bench_analytics_event_view_encode, + bench_google_message1_view_encode, + bench_media_frame_view_encode, + bench_api_response_build_encode, + bench_log_record_build_encode, + bench_analytics_event_build_encode, + bench_google_message1_build_encode, + bench_media_frame_build_encode, ); criterion_group!( diff --git a/benchmarks/charts/generate.py b/benchmarks/charts/generate.py index 6b9da6c..5dbcdc3 100644 --- a/benchmarks/charts/generate.py +++ b/benchmarks/charts/generate.py @@ -188,11 +188,16 @@ def build_tables( ]), ("binary-encode", [ ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "encode")), + ("buffa (view)", lambda ms, md: _get(buffa, "buffa", ms, "encode_view")), ("prost", lambda ms, md: _get(prost, "prost", ms, "encode")), ("prost (bytes)", lambda ms, md: _get(prost_bytes, "prost-bytes", ms, "encode")), ("protobuf-v4", lambda ms, md: _get(google, "google", ms, "encode")), ("Go", lambda ms, md: _get_go(go, "BinaryEncode", md)), ]), + ("build-encode", [ + ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "build_encode")), + ("buffa (view)", lambda ms, md: _get(buffa, "buffa", ms, "build_encode_view")), + ]), ("json-encode", [ ("buffa", lambda ms, md: _get(buffa, "buffa", ms, "json_encode")), ("prost", lambda ms, md: _get(prost, "prost", ms, "json_encode")), @@ -215,6 +220,11 @@ def build_tables( return tables +def messages_with_data(table: dict[str, dict[str, float | None]]) -> list[str]: + """Subset of MESSAGES that have at least one non-None value in this table.""" + return [m for m in MESSAGES if any(table[s].get(m) for s in table)] + + # ── SVG chart generation ─────────────────────────────────────────────── @dataclass @@ -366,6 +376,7 @@ def generate_readme_tables(tables: dict[str, dict[str, dict[str, float | None]]] chart_meta = { "binary-decode": ("Binary decode", "buffa"), "binary-encode": ("Binary encode", "buffa"), + "build-encode": ("Build + binary encode", "buffa"), "json-encode": ("JSON encode", "buffa"), "json-decode": ("JSON decode", "buffa"), } @@ -377,7 +388,7 @@ def generate_readme_tables(tables: dict[str, dict[str, dict[str, float | None]]] sep = "|---------|" + "|".join("------:" for _ in series_names) + "|" rows: list[str] = [] - for msg in MESSAGES: + for msg in messages_with_data(table): baseline = table[baseline_name].get(msg) cells = [_pct(table[s].get(msg), baseline) for s in series_names] rows.append(f"| {msg} | " + " | ".join(cells) + " |") @@ -429,6 +440,7 @@ def load_criterion(name: str) -> dict[str, float]: chart_titles = { "binary-decode": "Binary Decode Throughput", "binary-encode": "Binary Encode Throughput", + "build-encode": "Build + Binary Encode Throughput (from borrowed source data)", "json-encode": "JSON Encode Throughput", "json-decode": "JSON Decode Throughput", } diff --git a/benchmarks/charts/tables.md b/benchmarks/charts/tables.md index 38c110c..bc3ed34 100644 --- a/benchmarks/charts/tables.md +++ b/benchmarks/charts/tables.md @@ -18,6 +18,15 @@ | GoogleMessage1 | 2,594 | 1,808 (−30%) | — | 869 (−67%) | 360 (−86%) | | MediaFrame | 45,990 | 38,514 (−16%) | — | 10,463 (−77%) | 1,647 (−96%) | +### Build + binary encode + +| Message | buffa | buffa (view) | +|---------|------:|------:| +| ApiResponse | 790 | 1,799 (+128%) | +| LogRecord | 531 | 3,080 (+480%) | +| AnalyticsEvent | 408 | 1,178 (+189%) | +| GoogleMessage1 | 935 | 1,251 (+34%) | + ### JSON encode | Message | buffa | prost | Go | diff --git a/buffa-codegen/src/impl_message.rs b/buffa-codegen/src/impl_message.rs index 66d8031..0ff2c12 100644 --- a/buffa-codegen/src/impl_message.rs +++ b/buffa-codegen/src/impl_message.rs @@ -214,43 +214,30 @@ pub(crate) fn closed_enum_unknown_route( } } -/// Generate `unsafe impl DefaultInstance` and `impl Message` for a message. -/// -/// `preserve_unknown_fields`: when `true`, the generated merge collects -/// unknown fields into `self.__buffa_unknown_fields` and both `compute_size` and -/// `write_to` include them. -#[allow(clippy::too_many_arguments)] -pub fn generate_message_impl( - ctx: &CodeGenContext, - msg: &DescriptorProto, - preserve_unknown_fields: bool, - rust_name: &str, - current_package: &str, - proto_fqn: &str, - features: &ResolvedFeatures, - oneof_idents: &std::collections::HashMap, - oneof_prefix: &TokenStream, - nesting: usize, -) -> Result { - let name_ident = format_ident!("{}", rust_name); +/// Partition a message's fields by encode-dispatch shape. Shared between +/// [`generate_message_impl`] (owned) and [`build_view_encode_methods`] (view) +/// so a new field category only needs adding here. +struct ClassifiedFields<'a> { + scalar: Vec<&'a FieldDescriptorProto>, + repeated: Vec<&'a FieldDescriptorProto>, + map: Vec<&'a FieldDescriptorProto>, + oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&'a FieldDescriptorProto>)>, +} - let scalar_fields: Vec<_> = msg +fn classify_fields<'a>( + msg: &'a DescriptorProto, + oneof_idents: &std::collections::HashMap, +) -> ClassifiedFields<'a> { + let scalar = msg .field .iter() .filter(|f| { - // Real oneof members are excluded (handled via the oneof enum). - if is_real_oneof_member(f) { - return false; - } - if f.label.unwrap_or_default() == Label::LABEL_REPEATED { - return false; - } - is_supported_field_type(f.r#type.unwrap_or_default()) + !is_real_oneof_member(f) + && f.label.unwrap_or_default() != Label::LABEL_REPEATED + && is_supported_field_type(f.r#type.unwrap_or_default()) }) .collect(); - - // Repeated fields (excluding map entries, which are handled separately). - let repeated_fields: Vec<_> = msg + let repeated = msg .field .iter() .filter(|f| { @@ -259,9 +246,15 @@ pub fn generate_message_impl( && is_supported_field_type(f.r#type.unwrap_or_default()) }) .collect(); - - // Non-synthetic oneof groups: each entry is (oneof_name, enum_ident, fields[]). - let oneof_groups: Vec<(String, proc_macro2::Ident, Vec<&FieldDescriptorProto>)> = msg + let map = msg + .field + .iter() + .filter(|f| { + f.label.unwrap_or_default() == Label::LABEL_REPEATED + && crate::message::is_map_field(msg, f) + }) + .collect(); + let oneof_groups = msg .oneof_decl .iter() .enumerate() @@ -282,6 +275,40 @@ pub fn generate_message_impl( )) }) .collect(); + ClassifiedFields { + scalar, + repeated, + map, + oneof_groups, + } +} + +/// Generate `unsafe impl DefaultInstance` and `impl Message` for a message. +/// +/// `preserve_unknown_fields`: when `true`, the generated merge collects +/// unknown fields into `self.__buffa_unknown_fields` and both `compute_size` and +/// `write_to` include them. +#[allow(clippy::too_many_arguments)] +pub fn generate_message_impl( + ctx: &CodeGenContext, + msg: &DescriptorProto, + preserve_unknown_fields: bool, + rust_name: &str, + current_package: &str, + proto_fqn: &str, + features: &ResolvedFeatures, + oneof_idents: &std::collections::HashMap, + oneof_prefix: &TokenStream, + nesting: usize, +) -> Result { + let name_ident = format_ident!("{}", rust_name); + + let ClassifiedFields { + scalar: scalar_fields, + repeated: repeated_fields, + map: map_fields, + oneof_groups, + } = classify_fields(msg, oneof_idents); let compute_stmts = scalar_fields .iter() @@ -336,16 +363,6 @@ pub fn generate_message_impl( oneof_merge_arms.extend(mas); } - // Map fields. - let map_fields: Vec<_> = msg - .field - .iter() - .filter(|f| { - f.label.unwrap_or_default() == Label::LABEL_REPEATED - && crate::message::is_map_field(msg, f) - }) - .collect(); - let mut map_compute_stmts: Vec = Vec::new(); let mut map_write_stmts: Vec = Vec::new(); let mut map_merge_arms: Vec = Vec::new(); @@ -604,6 +621,177 @@ pub fn generate_message_impl( }) } +/// Build the `compute_size` / `write_to` / `cached_size` method tokens for a +/// **view** type. Reuses the same per-field stmt builders as +/// [`generate_message_impl`] — they emit `&self.field`-relative code that is +/// duck-type-compatible with view field types (`&'a str`, `RepeatedView`, +/// `MapView`, `MessageFieldView`). +pub(crate) fn build_view_encode_methods( + ctx: &CodeGenContext, + msg: &DescriptorProto, + preserve_unknown_fields: bool, + features: &ResolvedFeatures, + oneof_idents: &std::collections::HashMap, + view_oneof_prefix: &TokenStream, +) -> Result { + let ClassifiedFields { + scalar: scalar_fields, + repeated: repeated_fields, + map: map_fields, + oneof_groups, + } = classify_fields(msg, oneof_idents); + + let compute_stmts = scalar_fields + .iter() + .copied() + .map(|f| scalar_compute_size_stmt(ctx, f, features)) + .collect::, _>>()?; + let repeated_compute_stmts = repeated_fields + .iter() + .copied() + .map(|f| repeated_compute_size_stmt(ctx, f, features)) + .collect::, _>>()?; + let write_stmts = scalar_fields + .iter() + .copied() + .map(|f| scalar_write_to_stmt(ctx, f, features)) + .collect::, _>>()?; + let repeated_write_stmts = repeated_fields + .iter() + .copied() + .map(|f| repeated_write_to_stmt(ctx, f, features)) + .collect::, _>>()?; + + // The view-side oneof enum (in the parallel `__buffa::view::oneof::` tree) + // has the same variant *names* as the owned `__buffa::oneof::::Kind` + // but borrowed payload types (`&'a str` / `Box>` vs `String` / + // `Box`). The arm builders only emit the enum path + variant name and + // call duck-typed primitives (`string_encoded_len(x)`, `x.compute_size()`), + // so they work unchanged once pointed at the view enum via + // `view_oneof_prefix` (no `View` suffix — the tree disambiguates). + let mut oneof_compute_stmts: Vec = Vec::new(); + let mut oneof_write_stmts: Vec = Vec::new(); + for (oneof_name, enum_ident, fields) in &oneof_groups { + let field_ident = make_field_ident(oneof_name); + let qualified: TokenStream = quote! { #view_oneof_prefix #enum_ident }; + let mut size_arms: Vec = Vec::new(); + let mut write_arms: Vec = Vec::new(); + for field in fields { + let field_number = validated_field_number(field)?; + let ty = effective_type(ctx, field, features); + let variant = crate::oneof::oneof_variant_ident( + field + .name + .as_deref() + .ok_or(CodeGenError::MissingField("field.name"))?, + ); + let tag_len = tag_encoded_len(field_number, wire_type_byte(ty)); + let wire_type = wire_type_token(ty); + size_arms.push(oneof_size_arm(&qualified, &variant, tag_len, ty)); + write_arms.push(oneof_write_arm( + &qualified, + &variant, + field_number, + ty, + &wire_type, + )); + } + oneof_compute_stmts.push(quote! { + if let ::core::option::Option::Some(ref v) = self.#field_ident { + match v { #(#size_arms)* } + } + }); + oneof_write_stmts.push(quote! { + if let ::core::option::Option::Some(ref v) = self.#field_ident { + match v { #(#write_arms)* } + } + }); + } + + // map_{compute_size,write_to}_stmt emit `for (k, v) in &self.field { ... }`. + // For owned `&HashMap` that yields `(&K, &V)` directly. For + // `&MapView<'_,K,V>` it yields `&(K,V)`, but match-ergonomics binds the + // pattern `(k, v)` to `(&K, &V)` either way — so the same generated body + // works on both without modification. + let mut map_compute_stmts: Vec = Vec::new(); + let mut map_write_stmts: Vec = Vec::new(); + for f in &map_fields { + map_compute_stmts.push(map_compute_size_stmt(ctx, msg, f, features)?); + map_write_stmts.push(map_write_to_stmt(ctx, msg, f, features)?); + } + + let unknown_fields_size_stmt = if preserve_unknown_fields { + quote! { size += self.__buffa_unknown_fields.encoded_len() as u32; } + } else { + quote! {} + }; + // MessageSet (option message_set_wire_format = true) needs no special + // handling here: `UnknownFieldsView` stores raw verbatim wire spans, so the + // Item-group framing is already in the bytes and `write_to` is a passthrough. + // The owned path (see `generate_message_impl`) re-wraps because owned + // `UnknownFields` stores parsed `(number, data)` pairs. + let unknown_fields_write_stmt = if preserve_unknown_fields { + quote! { self.__buffa_unknown_fields.write_to(buf); } + } else { + quote! {} + }; + + let has_compute = !scalar_fields.is_empty() + || !repeated_fields.is_empty() + || !oneof_compute_stmts.is_empty() + || !map_compute_stmts.is_empty() + || preserve_unknown_fields; + let size_decl = if has_compute { + quote! { let mut size = 0u32; } + } else { + quote! { let size = 0u32; } + }; + let has_write = !write_stmts.is_empty() + || !repeated_write_stmts.is_empty() + || !oneof_write_stmts.is_empty() + || !map_write_stmts.is_empty() + || preserve_unknown_fields; + let buf_param = if has_write { + quote! { buf: &mut impl ::buffa::bytes::BufMut } + } else { + quote! { _buf: &mut impl ::buffa::bytes::BufMut } + }; + + Ok(quote! { + // needless_borrow: stmt builders emit `&self.field` so they work on + // owned `String`/`Vec`; on view fields (`&'a str`/`&'a [u8]`) + // the borrow is redundant but harmless. + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + #size_decl + #(#compute_stmts)* + #(#repeated_compute_stmts)* + #(#oneof_compute_stmts)* + #(#map_compute_stmts)* + #unknown_fields_size_stmt + self.__buffa_cached_size.set(size); + size + } + + #[allow(clippy::needless_borrow)] + fn write_to(&self, #buf_param) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + #(#write_stmts)* + #(#repeated_write_stmts)* + #(#oneof_write_stmts)* + #(#map_write_stmts)* + #unknown_fields_write_stmt + } + + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } + }) +} + /// Generate a clear statement for a scalar (non-repeated, non-oneof) field. /// /// Returns a `TokenStream` that clears the field to its default value while diff --git a/buffa-codegen/src/view.rs b/buffa-codegen/src/view.rs index f28da60..014a8ee 100644 --- a/buffa-codegen/src/view.rs +++ b/buffa-codegen/src/view.rs @@ -149,6 +149,23 @@ pub(crate) fn generate_view_with_nesting( } else { quote! {} }; + let view_encode_methods = crate::impl_message::build_view_encode_methods( + ctx, + msg, + ctx.config.preserve_unknown_fields, + features, + &oneof_idents, + &view_oneof_prefix, + )?; + let cached_size_field = quote! { + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, + }; + let view_encode_impl = quote! { + impl<'a> ::buffa::ViewEncode<'a> for #view_ident<'a> { + #view_encode_methods + } + }; // When preserving unknowns we capture `before_tag` so we can compute the // raw byte span after `skip_field` advances the cursor. @@ -205,6 +222,7 @@ pub(crate) fn generate_view_with_nesting( #(#direct_fields)* #(#oneof_struct_fields)* #unknown_fields_field + #cached_size_field #phantom_field } @@ -298,6 +316,8 @@ pub(crate) fn generate_view_with_nesting( } } + #view_encode_impl + // SAFETY: The static default instance is lazily initialized via OnceBox // and never mutated after publication. unsafe impl ::buffa::DefaultViewInstance for #view_ident<'static> { diff --git a/buffa-test/build.rs b/buffa-test/build.rs index 9863dbd..1cc1a50 100644 --- a/buffa-test/build.rs +++ b/buffa-test/build.rs @@ -123,7 +123,7 @@ fn main() { .files(&["protos/edge_cases.proto"]) .includes(&["protos/"]) .generate_json(true) - .generate_views(false) + .generate_views(true) .compile() .expect("buffa_build failed for edge_cases.proto"); diff --git a/buffa-test/protos/basic.proto b/buffa-test/protos/basic.proto index 92bb543..d9b7ed3 100644 --- a/buffa-test/protos/basic.proto +++ b/buffa-test/protos/basic.proto @@ -40,10 +40,12 @@ message Person { optional int32 maybe_age = 11; optional string maybe_nickname = 12; - // Oneof field + // Oneof field — string and message variants. The message variant + // exercises ViewEncode dispatch through Box>. oneof contact { string email = 13; string phone = 14; + Address home_address = 15; } } diff --git a/buffa-test/src/tests/bytes_type.rs b/buffa-test/src/tests/bytes_type.rs index 2f7831c..ca2cd45 100644 --- a/buffa-test/src/tests/bytes_type.rs +++ b/buffa-test/src/tests/bytes_type.rs @@ -269,3 +269,39 @@ fn test_bytes_type_map_value_stays_vec() { let owned: BytesContexts = view.to_owned_message(); assert_eq!(owned.by_key.get("k").map(Vec::as_slice), Some(&b"v"[..])); } + +#[test] +fn test_bytes_type_view_encode_roundtrip() { + // ViewEncode × use_bytes_type: view-side bytes fields are `&[u8]` while + // owned-side are `bytes::Bytes`. The encode-stmt builders duck-type + // through `encode_bytes(&self.#ident, buf)` (AsRef<[u8]>) so this should + // be wire-identical to the owned encode across all bytes-field shapes. + use buffa::{MessageView, ViewEncode}; + let msg = BytesContexts { + many: vec![ + bytes::Bytes::from_static(b"a"), + bytes::Bytes::from_static(b"bc"), + ], + maybe: Some(bytes::Bytes::from_static(&[0x00, 0xFF])), + choice: Some(ChoiceOneof::Raw(bytes::Bytes::from_static(b"o"))), + by_key: [("k".to_string(), b"v".to_vec())].into_iter().collect(), + ..Default::default() + }; + let wire = msg.encode_to_vec(); + let view = BytesContextsView::decode_view(&wire).expect("decode_view"); + let view_wire = view.encode_to_vec(); + // Decode-then-compare rather than byte-equality (map iteration order is + // hash-seed dependent on the owned side). + let back = BytesContexts::decode_from_slice(&view_wire).expect("decode"); + assert_eq!(back, msg); + // Singular bytes field via Person.avatar (no map → byte-exact). + use crate::basic_bytes::__buffa::view::PersonView; + let p = Person { + id: 1, + avatar: bytes::Bytes::from_static(&[0xCA, 0xFE, 0xBE, 0xEF]), + ..Default::default() + }; + let p_wire = p.encode_to_vec(); + let p_view = PersonView::decode_view(&p_wire).expect("decode_view"); + assert_eq!(p_view.encode_to_vec(), p_wire); +} diff --git a/buffa-test/src/tests/view.rs b/buffa-test/src/tests/view.rs index c23d87a..8703845 100644 --- a/buffa-test/src/tests/view.rs +++ b/buffa-test/src/tests/view.rs @@ -2,9 +2,10 @@ //! MapView iteration, oneof views, unknown-field preservation, recursion limit. use crate::basic::__buffa::oneof; +use crate::basic::__buffa::view::oneof as view_oneof; use crate::basic::__buffa::view::*; use crate::basic::*; -use buffa::{Message, MessageView}; +use buffa::{Message, MessageView, ViewEncode}; #[test] fn test_view_decodes_scalar_fields() { @@ -269,6 +270,33 @@ fn test_compute_size_matches_encode_len() { assert_eq!(size, bytes.len()); } +#[test] +fn test_compute_size_matches_encode_len_view() { + // Covers nested-message, repeated-message, and oneof-message cached-size + // dispatch through MessageFieldView/Box. + let addr = AddressView { + street: "1 Main St", + city: "Springfield", + zip_code: 12345, + ..Default::default() + }; + let view = PersonView { + id: 99, + name: "Bob", + tags: ["a", "b"].iter().copied().collect(), + address: addr.clone().into(), + addresses: [addr.clone()].into_iter().collect(), + contact: Some(view_oneof::person::Contact::HomeAddress(Box::new(addr))), + ..Default::default() + }; + let size = view.compute_size() as usize; + let bytes = view.encode_to_vec(); + assert_eq!(size, bytes.len()); + // Round-trips through owned. + let owned = Person::decode(&mut bytes.as_slice()).expect("decode owned"); + assert_eq!(owned.encode_to_vec().len(), size); +} + #[test] fn test_view_map_with_open_enum_value() { // map — proto3 open enum as map value. @@ -339,3 +367,219 @@ fn test_view_no_unknown_fields_all_scalar_compiles() { let owned = view.to_owned_message(); assert_eq!(owned.f_int32, 42); } + +// ── view encode ────────────────────────────────────────────────────────── + +#[test] +fn test_view_encode_roundtrip_address() { + let owned = Address { + street: "1 Infinite Loop".into(), + city: "Cupertino".into(), + zip_code: 95014, + ..Default::default() + }; + let owned_bytes = owned.encode_to_vec(); + let view = AddressView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); +} + +#[test] +fn test_view_encode_construct_from_borrows() { + let view = AddressView { + street: "borrowed st", + city: "ref town", + zip_code: 12345, + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Address::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.street, "borrowed st"); + assert_eq!(decoded.city, "ref town"); + assert_eq!(decoded.zip_code, 12345); +} + +#[test] +fn test_view_encode_roundtrip_inventory_maps() { + let mut owned = Inventory::default(); + owned.stock.insert("widgets".into(), 7); + owned.stock.insert("gadgets".into(), 3); + owned.locations.insert( + "hq".into(), + Address { + street: "main".into(), + city: "metro".into(), + zip_code: 1, + ..Default::default() + }, + ); + owned.statuses.insert("hq".into(), Status::ACTIVE.into()); + let owned_bytes = owned.encode_to_vec(); + let view = InventoryView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + // Map ordering: HashMap iter order is unspecified, but a decoded + // MapView preserves wire order, so re-encoding the view of the + // owned encoding produces the SAME bytes. + assert_eq!(owned_bytes, view_bytes); +} + +#[test] +fn test_view_encode_construct_map_from_iter() { + let pairs = [("a", 1i32), ("b", 2)]; + let view = InventoryView { + stock: pairs.iter().copied().collect(), + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Inventory::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.stock.get("a"), Some(&1)); + assert_eq!(decoded.stock.get("b"), Some(&2)); +} + +#[test] +fn test_view_encode_oneof_roundtrip() { + // String variant. + let mut owned = Person::default(); + owned.contact = Some(oneof::person::Contact::Email("a@b.c".into())); + let owned_bytes = owned.encode_to_vec(); + let view = PersonView::decode_view(&owned_bytes).unwrap(); + assert_eq!(owned_bytes, view.encode_to_vec()); + + // Nested-message variant — exercises ViewEncode dispatch through Box. + let mut owned = Person::default(); + owned.contact = Some(oneof::person::Contact::HomeAddress(Box::new(Address { + street: "1 main".into(), + city: "metro".into(), + zip_code: 9, + ..Default::default() + }))); + let owned_bytes = owned.encode_to_vec(); + let view = PersonView::decode_view(&owned_bytes).unwrap(); + assert_eq!(owned_bytes, view.encode_to_vec()); +} + +#[test] +fn test_view_encode_repeated_nested_and_oneof_from_borrows() { + use buffa::{MessageFieldView, RepeatedView}; + let addr = AddressView { + street: "x", + city: "y", + zip_code: 9, + ..Default::default() + }; + let view = PersonView { + tags: RepeatedView::new(vec!["t1", "t2"]), + lucky_numbers: RepeatedView::new(vec![7, 11]), + address: MessageFieldView::set(addr.clone()), + addresses: RepeatedView::new(vec![addr.clone()]), + contact: Some(view_oneof::person::Contact::HomeAddress(Box::new(addr))), + ..Default::default() + }; + let bytes = view.encode_to_vec(); + let decoded = Person::decode_from_slice(&bytes).unwrap(); + assert_eq!(decoded.tags, ["t1", "t2"]); + assert_eq!(decoded.lucky_numbers, [7, 11]); + assert_eq!(decoded.address.street, "x"); + assert_eq!(decoded.addresses[0].zip_code, 9); + let Some(oneof::person::Contact::HomeAddress(a)) = &decoded.contact else { + panic!("expected HomeAddress variant") + }; + assert_eq!(a.city, "y"); +} + +#[test] +fn test_view_encode_compute_size_matches_len() { + let owned = Address { + street: "1 Infinite Loop".into(), + city: "Cupertino".into(), + zip_code: 95014, + ..Default::default() + }; + let bytes = owned.encode_to_vec(); + let view = AddressView::decode_view(&bytes).unwrap(); + assert_eq!(view.compute_size() as usize, view.encode_to_vec().len()); +} + +#[test] +fn test_view_encode_proto2_groups_roundtrip() { + use crate::proto2::__buffa::view::WithGroupsView; + use crate::proto2::{with_groups, WithGroups}; + let owned = WithGroups { + mygroup: with_groups::MyGroup { + a: Some(7), + b: Some("inner".into()), + ..Default::default() + } + .into(), + item: vec![ + with_groups::Item { + id: Some(1), + name: Some("first".into()), + ..Default::default() + }, + with_groups::Item { + id: Some(2), + name: Some("second".into()), + ..Default::default() + }, + ], + label: Some("alongside".into()), + ..Default::default() + }; + let owned_bytes = owned.encode_to_vec(); + let view = WithGroupsView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); + assert_eq!(view.compute_size() as usize, view_bytes.len()); +} + +#[test] +fn test_view_encode_proto2_oneof_group_closed_enum_roundtrip() { + use crate::proto2::__buffa::oneof::view_coverage as vc_oneof; + use crate::proto2::__buffa::view::ViewCoverageView; + use crate::proto2::{view_coverage, Priority, ViewCoverage}; + let mut owned = ViewCoverage { + level: Priority::HIGH, + choice: Some(vc_oneof::Choice::Payload(Box::new( + view_coverage::Payload { + x: Some(42), + y: Some("oneof-group".into()), + ..Default::default() + }, + ))), + ..Default::default() + }; + owned.by_id.insert(10, "ten".into()); + owned.priorities.insert("k".into(), Priority::LOW); + let owned_bytes = owned.encode_to_vec(); + let view = ViewCoverageView::decode_view(&owned_bytes).unwrap(); + let view_bytes = view.encode_to_vec(); + assert_eq!(owned_bytes, view_bytes); + // Re-decode through owned to confirm semantic equality, not just byte + // identity (which here happens to hold because each map has 1 entry). + let redecoded = ViewCoverage::decode_from_slice(&view_bytes).unwrap(); + assert_eq!(redecoded, owned); +} + +#[test] +fn test_view_encode_closed_enum_unknown_value_preserved() { + // Unknown closed-enum value on the wire: view decode cannot represent + // 99 as a `Priority` so the typed `level` field stays at default and the + // raw bytes go to UnknownFieldsView. ViewEncode must re-emit those + // unknown bytes so a downstream owned decoder sees the same result as + // decoding the original wire directly. + use crate::proto2::__buffa::view::ViewCoverageView; + use crate::proto2::{Priority, ViewCoverage}; + // field 1 (level) varint = tag 0x08, value 99 (not a Priority variant). + let wire: &[u8] = &[0x08, 99]; + let owned_direct = ViewCoverage::decode_from_slice(wire).unwrap(); + let view = ViewCoverageView::decode_view(wire).unwrap(); + // The typed field is NOT 99 — it's the proto2 first-variant default. + assert_eq!(view.level, Priority::LOW); + let view_bytes = view.encode_to_vec(); + let owned_via_view = ViewCoverage::decode_from_slice(&view_bytes).unwrap(); + assert_eq!( + owned_via_view, owned_direct, + "view-encode must preserve unknown closed-enum semantics" + ); +} diff --git a/buffa-types/src/generated/google.protobuf.any.__view.rs b/buffa-types/src/generated/google.protobuf.any.__view.rs index 799446b..873b6c3 100644 --- a/buffa-types/src/generated/google.protobuf.any.__view.rs +++ b/buffa-types/src/generated/google.protobuf.any.__view.rs @@ -136,6 +136,8 @@ pub struct AnyView<'a> { /// Field 2: `value` pub value: &'a [u8], pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> AnyView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -233,6 +235,48 @@ impl<'a> ::buffa::MessageView<'a> for AnyView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for AnyView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.type_url.is_empty() { + size += 1u32 + ::buffa::types::string_encoded_len(&self.type_url) as u32; + } + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::bytes_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.type_url.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(&self.type_url, buf); + } + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_bytes(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for AnyView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.duration.__view.rs b/buffa-types/src/generated/google.protobuf.duration.__view.rs index af0b2e4..45bd0c4 100644 --- a/buffa-types/src/generated/google.protobuf.duration.__view.rs +++ b/buffa-types/src/generated/google.protobuf.duration.__view.rs @@ -83,6 +83,8 @@ pub struct DurationView<'a> { /// Field 2: `nanos` pub nanos: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> DurationView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -180,6 +182,42 @@ impl<'a> ::buffa::MessageView<'a> for DurationView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for DurationView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.seconds != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.seconds) as u32; + } + if self.nanos != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.nanos) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.seconds != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.seconds, buf); + } + if self.nanos != 0i32 { + ::buffa::encoding::Tag::new(2u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.nanos, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for DurationView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.empty.__view.rs b/buffa-types/src/generated/google.protobuf.empty.__view.rs index 433f85b..e66a66b 100644 --- a/buffa-types/src/generated/google.protobuf.empty.__view.rs +++ b/buffa-types/src/generated/google.protobuf.empty.__view.rs @@ -13,6 +13,8 @@ #[derive(Clone, Debug, Default)] pub struct EmptyView<'a> { pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> EmptyView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -88,6 +90,26 @@ impl<'a> ::buffa::MessageView<'a> for EmptyView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for EmptyView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for EmptyView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.field_mask.__view.rs b/buffa-types/src/generated/google.protobuf.field_mask.__view.rs index 61c1de1..df18170 100644 --- a/buffa-types/src/generated/google.protobuf.field_mask.__view.rs +++ b/buffa-types/src/generated/google.protobuf.field_mask.__view.rs @@ -231,6 +231,8 @@ pub struct FieldMaskView<'a> { /// Field 1: `paths` pub paths: ::buffa::RepeatedView<'a, &'a str>, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> FieldMaskView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -317,6 +319,37 @@ impl<'a> ::buffa::MessageView<'a> for FieldMaskView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for FieldMaskView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for v in &self.paths { + size += 1u32 + ::buffa::types::string_encoded_len(v) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for v in &self.paths { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(v, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for FieldMaskView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.struct.__view.rs b/buffa-types/src/generated/google.protobuf.struct.__view.rs index 0ac2db9..8c386d5 100644 --- a/buffa-types/src/generated/google.protobuf.struct.__view.rs +++ b/buffa-types/src/generated/google.protobuf.struct.__view.rs @@ -20,6 +20,8 @@ pub struct StructView<'a> { super::super::__buffa::view::ValueView<'a>, >, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> StructView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -157,6 +159,64 @@ impl<'a> ::buffa::MessageView<'a> for StructView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for StructView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for (k, v) in &self.fields { + let entry_size: u32 = 1u32 + ::buffa::types::string_encoded_len(k) as u32 + + 1u32 + + { + let inner = v.compute_size(); + ::buffa::encoding::varint_len(inner as u64) as u32 + inner + }; + size + += 1u32 + ::buffa::encoding::varint_len(entry_size as u64) as u32 + + entry_size; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for (k, v) in &self.fields { + let entry_size: u32 = 1u32 + ::buffa::types::string_encoded_len(k) as u32 + + 1u32 + + { + let inner = v.compute_size(); + ::buffa::encoding::varint_len(inner as u64) as u32 + inner + }; + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(entry_size as u64, buf); + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(k, buf); + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(v.cached_size() as u64, buf); + v.write_to(buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for StructView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -178,6 +238,8 @@ pub struct ValueView<'a> { super::super::__buffa::view::oneof::value::Kind<'a>, >, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -404,6 +466,108 @@ impl<'a> ::buffa::MessageView<'a> for ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if let ::core::option::Option::Some(ref v) = self.kind { + match v { + super::super::__buffa::view::oneof::value::Kind::NullValue(x) => { + size += 1u32 + ::buffa::types::int32_encoded_len(x.to_i32()) as u32; + } + super::super::__buffa::view::oneof::value::Kind::NumberValue(_x) => { + size += 1u32 + ::buffa::types::FIXED64_ENCODED_LEN as u32; + } + super::super::__buffa::view::oneof::value::Kind::StringValue(x) => { + size += 1u32 + ::buffa::types::string_encoded_len(x) as u32; + } + super::super::__buffa::view::oneof::value::Kind::BoolValue(_x) => { + size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + } + super::super::__buffa::view::oneof::value::Kind::StructValue(x) => { + let inner = x.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner as u64) as u32 + + inner; + } + super::super::__buffa::view::oneof::value::Kind::ListValue(x) => { + let inner = x.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner as u64) as u32 + + inner; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.kind { + match v { + super::super::__buffa::view::oneof::value::Kind::NullValue(x) => { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::Varint, + ) + .encode(buf); + ::buffa::types::encode_int32(x.to_i32(), buf); + } + super::super::__buffa::view::oneof::value::Kind::NumberValue(x) => { + ::buffa::encoding::Tag::new( + 2u32, + ::buffa::encoding::WireType::Fixed64, + ) + .encode(buf); + ::buffa::types::encode_double(*x, buf); + } + super::super::__buffa::view::oneof::value::Kind::StringValue(x) => { + ::buffa::encoding::Tag::new( + 3u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(x, buf); + } + super::super::__buffa::view::oneof::value::Kind::BoolValue(x) => { + ::buffa::encoding::Tag::new( + 4u32, + ::buffa::encoding::WireType::Varint, + ) + .encode(buf); + ::buffa::types::encode_bool(*x, buf); + } + super::super::__buffa::view::oneof::value::Kind::StructValue(x) => { + ::buffa::encoding::Tag::new( + 5u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(x.cached_size() as u64, buf); + x.write_to(buf); + } + super::super::__buffa::view::oneof::value::Kind::ListValue(x) => { + ::buffa::encoding::Tag::new( + 6u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(x.cached_size() as u64, buf); + x.write_to(buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -423,6 +587,8 @@ pub struct ListValueView<'a> { /// Field 1: `values` pub values: ::buffa::RepeatedView<'a, super::super::__buffa::view::ValueView<'a>>, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> ListValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -519,6 +685,41 @@ impl<'a> ::buffa::MessageView<'a> for ListValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for ListValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + for v in &self.values { + let inner_size = v.compute_size(); + size + += 1u32 + ::buffa::encoding::varint_len(inner_size as u64) as u32 + + inner_size; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + for v in &self.values { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::encoding::encode_varint(v.cached_size() as u64, buf); + v.write_to(buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for ListValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.timestamp.__view.rs b/buffa-types/src/generated/google.protobuf.timestamp.__view.rs index 5422c4b..1d2bf12 100644 --- a/buffa-types/src/generated/google.protobuf.timestamp.__view.rs +++ b/buffa-types/src/generated/google.protobuf.timestamp.__view.rs @@ -118,6 +118,8 @@ pub struct TimestampView<'a> { /// Field 2: `nanos` pub nanos: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> TimestampView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -215,6 +217,42 @@ impl<'a> ::buffa::MessageView<'a> for TimestampView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for TimestampView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.seconds != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.seconds) as u32; + } + if self.nanos != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.nanos) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.seconds != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.seconds, buf); + } + if self.nanos != 0i32 { + ::buffa::encoding::Tag::new(2u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.nanos, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for TimestampView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa-types/src/generated/google.protobuf.wrappers.__view.rs b/buffa-types/src/generated/google.protobuf.wrappers.__view.rs index 5b1f0d8..3465b58 100644 --- a/buffa-types/src/generated/google.protobuf.wrappers.__view.rs +++ b/buffa-types/src/generated/google.protobuf.wrappers.__view.rs @@ -11,6 +11,8 @@ pub struct DoubleValueView<'a> { /// Field 1: `value` pub value: f64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> DoubleValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -97,6 +99,34 @@ impl<'a> ::buffa::MessageView<'a> for DoubleValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for DoubleValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value.to_bits() != 0u64 { + size += 1u32 + ::buffa::types::FIXED64_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value.to_bits() != 0u64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Fixed64) + .encode(buf); + ::buffa::types::encode_double(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for DoubleValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -116,6 +146,8 @@ pub struct FloatValueView<'a> { /// Field 1: `value` pub value: f32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> FloatValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -202,6 +234,34 @@ impl<'a> ::buffa::MessageView<'a> for FloatValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for FloatValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value.to_bits() != 0u32 { + size += 1u32 + ::buffa::types::FIXED32_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value.to_bits() != 0u32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Fixed32) + .encode(buf); + ::buffa::types::encode_float(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for FloatValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -221,6 +281,8 @@ pub struct Int64ValueView<'a> { /// Field 1: `value` pub value: i64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> Int64ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -307,6 +369,34 @@ impl<'a> ::buffa::MessageView<'a> for Int64ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for Int64ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0i64 { + size += 1u32 + ::buffa::types::int64_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0i64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int64(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for Int64ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -326,6 +416,8 @@ pub struct UInt64ValueView<'a> { /// Field 1: `value` pub value: u64, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> UInt64ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -412,6 +504,34 @@ impl<'a> ::buffa::MessageView<'a> for UInt64ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for UInt64ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0u64 { + size += 1u32 + ::buffa::types::uint64_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0u64 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_uint64(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for UInt64ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -431,6 +551,8 @@ pub struct Int32ValueView<'a> { /// Field 1: `value` pub value: i32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> Int32ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -517,6 +639,34 @@ impl<'a> ::buffa::MessageView<'a> for Int32ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for Int32ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0i32 { + size += 1u32 + ::buffa::types::int32_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0i32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_int32(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for Int32ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -536,6 +686,8 @@ pub struct UInt32ValueView<'a> { /// Field 1: `value` pub value: u32, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> UInt32ValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -622,6 +774,34 @@ impl<'a> ::buffa::MessageView<'a> for UInt32ValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for UInt32ValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value != 0u32 { + size += 1u32 + ::buffa::types::uint32_encoded_len(self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value != 0u32 { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_uint32(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for UInt32ValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -641,6 +821,8 @@ pub struct BoolValueView<'a> { /// Field 1: `value` pub value: bool, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> BoolValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -727,6 +909,34 @@ impl<'a> ::buffa::MessageView<'a> for BoolValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for BoolValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if self.value { + size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.value { + ::buffa::encoding::Tag::new(1u32, ::buffa::encoding::WireType::Varint) + .encode(buf); + ::buffa::types::encode_bool(self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for BoolValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -746,6 +956,8 @@ pub struct StringValueView<'a> { /// Field 1: `value` pub value: &'a str, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> StringValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -832,6 +1044,37 @@ impl<'a> ::buffa::MessageView<'a> for StringValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for StringValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::string_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_string(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for StringValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); @@ -851,6 +1094,8 @@ pub struct BytesValueView<'a> { /// Field 1: `value` pub value: &'a [u8], pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, + #[doc(hidden)] + pub __buffa_cached_size: ::buffa::__private::CachedSize, } impl<'a> BytesValueView<'a> { /// Decode from `buf`, enforcing a recursion depth limit for nested messages. @@ -937,6 +1182,37 @@ impl<'a> ::buffa::MessageView<'a> for BytesValueView<'a> { } } } +impl<'a> ::buffa::ViewEncode<'a> for BytesValueView<'a> { + #[allow(clippy::needless_borrow)] + fn compute_size(&self) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u32; + if !self.value.is_empty() { + size += 1u32 + ::buffa::types::bytes_encoded_len(&self.value) as u32; + } + size += self.__buffa_unknown_fields.encoded_len() as u32; + self.__buffa_cached_size.set(size); + size + } + #[allow(clippy::needless_borrow)] + fn write_to(&self, buf: &mut impl ::buffa::bytes::BufMut) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.value.is_empty() { + ::buffa::encoding::Tag::new( + 1u32, + ::buffa::encoding::WireType::LengthDelimited, + ) + .encode(buf); + ::buffa::types::encode_bytes(&self.value, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn cached_size(&self) -> u32 { + self.__buffa_cached_size.get() + } +} unsafe impl ::buffa::DefaultViewInstance for BytesValueView<'static> { fn default_view_instance() -> &'static Self { static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); diff --git a/buffa/src/lib.rs b/buffa/src/lib.rs index 1173c54..800af10 100644 --- a/buffa/src/lib.rs +++ b/buffa/src/lib.rs @@ -207,7 +207,7 @@ pub use unknown_fields::{UnknownField, UnknownFieldData, UnknownFields}; pub use text::TextFormat; pub use view::{ DefaultViewInstance, HasDefaultViewInstance, MapView, MessageFieldView, MessageView, OwnedView, - RepeatedView, UnknownFieldsView, + RepeatedView, UnknownFieldsView, ViewEncode, }; /// Private re-exports used exclusively by generated code. diff --git a/buffa/src/message.rs b/buffa/src/message.rs index 0a08304..e8fb5be 100644 --- a/buffa/src/message.rs +++ b/buffa/src/message.rs @@ -83,6 +83,8 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// is `u32`, so messages whose encoded size exceeds `u32::MAX` (4 GiB) /// will produce a wrapped (undefined) size and a truncated encoding. /// Stay well within the 2 GiB spec limit. + #[must_use = "compute_size has the side-effect of populating cached sizes; \ + if you only need that, call encode() instead"] fn compute_size(&self) -> u32; /// Write this message's encoded bytes to a buffer. @@ -93,7 +95,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// Convenience: compute size, then write. This is the primary encoding API. fn encode(&self, buf: &mut impl BufMut) { - self.compute_size(); + let _ = self.compute_size(); self.write_to(buf); } @@ -105,6 +107,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { } /// Encode this message to a new `Vec`. + #[must_use] fn encode_to_vec(&self) -> alloc::vec::Vec { let size = self.compute_size() as usize; let mut buf = alloc::vec::Vec::with_capacity(size); @@ -120,6 +123,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// This is equivalent to `Bytes::from(self.encode_to_vec())` — both /// are zero-copy with respect to the encoded bytes — but saves readers /// from having to know that `From> for Bytes` is zero-copy. + #[must_use] fn encode_to_bytes(&self) -> bytes::Bytes { let size = self.compute_size() as usize; let mut buf = bytes::BytesMut::with_capacity(size); @@ -377,6 +381,7 @@ pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync { /// The cached encoded size from the last `compute_size()` call. /// /// Returns 0 if `compute_size()` has never been called. + #[must_use] fn cached_size(&self) -> u32; /// Clear all fields to their default values. diff --git a/buffa/src/view.rs b/buffa/src/view.rs index de795b9..00d7613 100644 --- a/buffa/src/view.rs +++ b/buffa/src/view.rs @@ -76,7 +76,7 @@ use crate::error::DecodeError; use crate::message::Message as _; -use bytes::Bytes; +use bytes::{BufMut, Bytes}; /// Trait for zero-copy borrowed message views. /// @@ -112,6 +112,91 @@ pub trait MessageView<'a>: Sized { fn to_owned_message(&self) -> Self::Owned; } +/// Serialize a [`MessageView`] directly from its borrowed fields. +/// +/// Symmetric with [`Message`](crate::Message)'s two-pass +/// `compute_size` / `write_to` model, but the `&'a str` / `&'a [u8]` / +/// [`MapView`] / [`RepeatedView`] fields are written by borrow — no +/// owned-struct intermediary, no per-field `String`/`Vec` allocations. +/// +/// Generated `*View<'a>` types implement this trait whenever views are +/// generated (`generate_views(true)`, the default). Each view struct +/// carries a `__buffa_cached_size` field for the cached-size pass — same +/// `AtomicU32`-backed [`CachedSize`](crate::__private::CachedSize) as +/// owned messages, so views remain `Send + Sync`. +/// +/// ## When to use +/// +/// Reach for `ViewEncode` when the source data is already in memory and +/// you would otherwise allocate an owned message just to encode-then-drop +/// it — e.g. an RPC handler serializing from app state. If you already +/// hold the owned message, use [`Message::encode`](crate::Message::encode) +/// instead; the wire output is identical. +/// +/// ```rust,ignore +/// let view = PersonView { +/// name: "borrowed", +/// tags: ["a", "b"].iter().copied().collect(), +/// ..Default::default() +/// }; +/// let bytes = view.encode_to_vec(); +/// ``` +#[diagnostic::on_unimplemented( + message = "`{Self}` does not implement `ViewEncode` — view types were not generated for this message", + note = "ViewEncode is implemented on every generated `*View<'a>` type; enable `generate_views(true)` (on by default) in your buffa-build / buffa-codegen config" +)] +pub trait ViewEncode<'a>: MessageView<'a> { + /// Compute and cache the encoded byte size of this view. + /// + /// Recursively computes sizes for sub-message views and stores them in + /// each view's `CachedSize` field. Must be called before + /// [`write_to`](Self::write_to). + #[must_use = "compute_size has the side-effect of populating cached sizes; \ + if you only need that, call encode() instead"] + fn compute_size(&self) -> u32; + + /// Write this view's encoded bytes to a buffer. + /// + /// Assumes [`compute_size`](Self::compute_size) has already been called. + /// Uses cached sizes for length-delimited sub-message headers. + fn write_to(&self, buf: &mut impl BufMut); + + /// Return the size cached by the most recent [`compute_size`](Self::compute_size). + #[must_use] + fn cached_size(&self) -> u32; + + /// Convenience: compute size, then write. Primary view-encode entry point. + fn encode(&self, buf: &mut impl BufMut) { + let _ = self.compute_size(); + self.write_to(buf); + } + + /// Encode this view as a length-delimited byte sequence. + fn encode_length_delimited(&self, buf: &mut impl BufMut) { + let len = self.compute_size(); + crate::encoding::encode_varint(len as u64, buf); + self.write_to(buf); + } + + /// Encode this view to a new `Vec`. + #[must_use] + fn encode_to_vec(&self) -> alloc::vec::Vec { + let size = self.compute_size() as usize; + let mut buf = alloc::vec::Vec::with_capacity(size); + self.write_to(&mut buf); + buf + } + + /// Encode this view to a new [`bytes::Bytes`]. + #[must_use] + fn encode_to_bytes(&self) -> Bytes { + let size = self.compute_size() as usize; + let mut buf = bytes::BytesMut::with_capacity(size); + self.write_to(&mut buf); + buf.freeze() + } +} + /// Provides access to a lazily-initialized default view instance. /// /// View types implement this trait so that [`MessageFieldView`] can @@ -182,6 +267,13 @@ impl MessageFieldView { } } + /// Alias for [`set`](Self::set), mirroring owned + /// [`MessageField::some`](crate::MessageField::some). + #[inline] + pub fn some(v: V) -> Self { + Self::set(v) + } + /// Returns `true` if the field has a value. #[inline] pub fn is_set(&self) -> bool { @@ -210,6 +302,32 @@ impl MessageFieldView { } } +impl<'a, V: ViewEncode<'a>> MessageFieldView { + /// Forward to the inner view's [`compute_size`](ViewEncode::compute_size), + /// or `0` if unset. Generated `compute_size` calls this for nested-message + /// fields, mirroring [`MessageField`](crate::MessageField) on the owned side. + #[inline] + pub fn compute_size(&self) -> u32 { + self.inner.as_deref().map_or(0, V::compute_size) + } + + /// Forward to the inner view's [`cached_size`](ViewEncode::cached_size), + /// or `0` if unset. + #[inline] + pub fn cached_size(&self) -> u32 { + self.inner.as_deref().map_or(0, V::cached_size) + } + + /// Forward to the inner view's [`write_to`](ViewEncode::write_to); + /// no-op if unset. + #[inline] + pub fn write_to(&self, buf: &mut impl BufMut) { + if let Some(v) = self.inner.as_deref() { + v.write_to(buf); + } + } +} + impl Default for MessageFieldView { #[inline] fn default() -> Self { @@ -217,6 +335,13 @@ impl Default for MessageFieldView { } } +impl From for MessageFieldView { + #[inline] + fn from(v: V) -> Self { + Self::set(v) + } +} + /// Marker trait linking a lifetime-parameterized view type `V` (e.g., /// `FooView<'a>`) to its `'static` instantiation that implements /// [`DefaultViewInstance`]. Generated code implements this for every @@ -363,6 +488,18 @@ impl<'b, 'a, T> IntoIterator for &'b RepeatedView<'a, T> { } } +impl<'a, T> From> for RepeatedView<'a, T> { + fn from(elements: alloc::vec::Vec) -> Self { + Self::new(elements) + } +} + +impl<'a, T> FromIterator for RepeatedView<'a, T> { + fn from_iter>(iter: I) -> Self { + Self::new(iter.into_iter().collect()) + } +} + /// A borrowed view of a map field. /// /// Protobuf `map` fields are encoded as repeated sub-messages, each @@ -397,6 +534,17 @@ pub struct MapView<'a, K, V> { } impl<'a, K, V> MapView<'a, K, V> { + /// Construct from a `Vec` of entries, for [`ViewEncode`] use. + /// + /// Duplicate keys are kept and all encoded — valid protobuf wire data + /// (decoders apply last-write-wins). Mirrors [`RepeatedView::new`]. + pub fn new(entries: alloc::vec::Vec<(K, V)>) -> Self { + Self { + entries, + _marker: core::marker::PhantomData, + } + } + /// Returns the number of entries (including duplicates). pub fn len(&self) -> usize { self.entries.len() @@ -456,6 +604,19 @@ impl<'a, K, V> MapView<'a, K, V> { } } +impl<'a, K, V> From> for MapView<'a, K, V> { + fn from(entries: alloc::vec::Vec<(K, V)>) -> Self { + Self::new(entries) + } +} + +/// Duplicate keys are kept and all encoded; see [`MapView::new`]. +impl<'a, K, V> FromIterator<(K, V)> for MapView<'a, K, V> { + fn from_iter>(iter: I) -> Self { + Self::new(iter.into_iter().collect()) + } +} + impl<'a, K, V> Default for MapView<'a, K, V> { fn default() -> Self { Self { @@ -514,6 +675,15 @@ impl<'a> UnknownFieldsView<'a> { self.raw_spans.iter().map(|s| s.len()).sum() } + /// Write all unknown-field bytes verbatim. Each span is a complete + /// `(tag, value)` record as it appeared on the wire, so concatenating + /// them produces a valid encoding. + pub fn write_to(&self, buf: &mut impl BufMut) { + for span in &self.raw_spans { + buf.put_slice(span); + } + } + /// Convert to an owned [`UnknownFields`](crate::UnknownFields) by parsing all stored raw byte spans. /// /// Each span is a complete (tag + value) record as it appeared on the wire. @@ -1228,6 +1398,38 @@ mod tests { } } + impl<'a> ViewEncode<'a> for SimpleMessageView<'a> { + fn compute_size(&self) -> u32 { + let mut size = 0u32; + if self.id != 0 { + size += 1 + crate::types::int32_encoded_len(self.id) as u32; + } + if !self.name.is_empty() { + size += 1 + crate::types::string_encoded_len(self.name) as u32; + } + size + } + + fn write_to(&self, buf: &mut impl bytes::BufMut) { + if self.id != 0 { + crate::encoding::Tag::new(1, crate::encoding::WireType::Varint).encode(buf); + crate::types::encode_int32(self.id, buf); + } + if !self.name.is_empty() { + crate::encoding::Tag::new(2, crate::encoding::WireType::LengthDelimited) + .encode(buf); + crate::types::encode_string(self.name, buf); + } + } + + fn cached_size(&self) -> u32 { + // Test-only stub: SimpleMessageView has no nested messages, + // so nothing reads cached_size. Real impls return the value + // stored by compute_size. + 0 + } + } + /// Encode a SimpleMessage to Bytes for testing. fn encode_simple(id: i32, name: &str) -> Bytes { let msg = SimpleMessage {