Skip to main content

toolkit_contract/
grpc_repr.rs

1//! Compile-time proto-representability check.
2//!
3//! [`GrpcRepr`] and [`GrpcReprScalar`] are marker traits used by the
4//! `#[toolkit::grpc_contract]` macro to verify that every method parameter
5//! and return type can be represented in proto3 — without running the
6//! schema-to-proto generator.
7//!
8//! Opt-in for user DTOs: add `#[derive(toolkit::ProtoBridge)]`. The derive
9//! emits both `impl GrpcRepr for YourType {}` and
10//! `impl GrpcReprScalar for YourType {}`.
11//!
12//! Built-in primitive impls are provided here. Composite shapes
13//! (`Vec<T>`, `Option<T>`, `HashMap<String, V>`, `BTreeMap<String, V>`) are
14//! accepted automatically when their element type implements
15//! [`GrpcReprScalar`]. Nested maps and `Vec<Vec<_>>` are intentionally NOT
16//! representable — proto3 has no equivalent.
17//!
18//! This module is feature-gate-free so the macro can emit static assertions
19//! regardless of which features the downstream crate enables.
20
21use std::collections::{BTreeMap, HashMap};
22
23/// Marker for "any type that can appear in a gRPC method signature, either
24/// as a parameter or as the success type of `Result<T, E>` returned from a
25/// method." Composite shapes (`Vec<T>`, `Option<T>`, maps) are
26/// `GrpcRepr` when their inner type is [`GrpcReprScalar`].
27pub trait GrpcRepr {}
28
29/// Marker for "scalar" types — anything that can sit inside a `Vec<>`,
30/// `Option<>`, or as the value type of a `HashMap<String, V>`.
31///
32/// Maps and lists are deliberately NOT scalars: proto3 disallows nesting
33/// `repeated repeated` and `map<K, map<...>>`.
34pub trait GrpcReprScalar: GrpcRepr {}
35
36// --- primitive scalar impls -------------------------------------------------
37
38macro_rules! impl_primitive_repr {
39    ($($t:ty),* $(,)?) => {
40        $(
41            impl GrpcRepr for $t {}
42            impl GrpcReprScalar for $t {}
43        )*
44    };
45}
46
47// Numeric and string primitives that have a 1:1 proto3 mapping.
48// `String` ↔ `string`, `i32` ↔ `int32`, `i64` ↔ `int64`, `u32` ↔ `uint32`,
49// `u64` ↔ `uint64`, `f32` ↔ `float`, `f64` ↔ `double`, `bool` ↔ `bool`.
50//
51// `i8`, `i16`, `u8`, `u16` are intentionally NOT included: proto3 has no
52// narrower-than-32-bit integer types and silently widening would lose
53// validation. Use `i32`/`u32` explicitly.
54//
55// `i128`, `u128`, `f128`, `char`, `isize`, `usize` are NOT included: they
56// have no proto3 representation.
57impl_primitive_repr!(String, i32, i64, u32, u64, f32, f64, bool);
58
59// --- composite impls --------------------------------------------------------
60
61/// `Vec<T>` → `repeated T`. Disallows `Vec<Vec<T>>` because `Vec<T>` does
62/// not implement [`GrpcReprScalar`].
63impl<T: GrpcReprScalar> GrpcRepr for Vec<T> {}
64
65/// `Option<T>` → `optional T` (proto3). Disallows `Option<Vec<T>>` and
66/// `Option<HashMap<_,_>>` for the same reason maps and lists aren't scalar.
67impl<T: GrpcReprScalar> GrpcRepr for Option<T> {}
68
69/// `HashMap<String, V>` → `map<string, V>`. Restricted to string keys —
70/// proto3 also allows integer keys but the common Rust idiom is string
71/// keys, and admitting more would defeat the guard's clarity.
72impl<V: GrpcReprScalar, S: ::std::hash::BuildHasher> GrpcRepr for HashMap<String, V, S> {}
73
74/// `BTreeMap<String, V>` mirrors `HashMap<String, V>` for code that prefers
75/// deterministic iteration order in serialized output.
76impl<V: GrpcReprScalar> GrpcRepr for BTreeMap<String, V> {}
77
78// --- byte-buffer impls ------------------------------------------------------
79//
80// `Vec<u8>` is *not* a `GrpcReprScalar`: `u8` itself is not in the impl list
81// above (proto3 has no 8-bit integer), so `Vec<u8>` would fail anyway. Users
82// who want a `bytes` field should derive `ProtoBridge` on a wrapper struct
83// or annotate their DTO field — both flow through `#[derive(ProtoBridge)]`.
84
85// --- compile-time assert helper --------------------------------------------
86
87/// Static assertion helper used by `#[toolkit::grpc_contract]` to fail
88/// compilation when a method parameter or return type cannot be represented
89/// in proto3.
90///
91/// The macro emits a `const _: () = { ... };` block calling this for every
92/// non-context method parameter and the `Ok` half of every `Result<T, E>`
93/// return type.
94///
95/// Naming is intentionally awkward — this is not a public API; treat it as
96/// an internal helper that happens to live in `pub mod` so generated code
97/// can reach it.
98#[doc(hidden)]
99pub const fn assert_grpc_repr<T: GrpcRepr + ?Sized>() {}
100
101// ---------------------------------------------------------------------------
102// SecurityContext marker
103// ---------------------------------------------------------------------------
104
105/// Marker trait for "this type carries an in-process security context that
106/// must be projected onto gRPC metadata at the client and reconstructed on
107/// the server" — i.e. a parameter the wire payload synthesizer must skip.
108///
109/// `#[toolkit::grpc_contract]` and `#[toolkit::rest_contract]` detect such
110/// parameters by type name (`*SecurityContext`-suffixed) and emit a static
111/// assertion `assert_security_context::<T>()` so accidentally naming a DTO
112/// `SecurityContext` without implementing this marker fails to compile.
113///
114/// Lives in this module (always-on, feature-gate-free) so the macro can
115/// emit unconditional assertions regardless of which features downstream
116/// crates enable. The default impl for `toolkit_security::SecurityContext`
117/// lives in [`crate::grpc`] under the `grpc-client` feature.
118pub trait SecurityContextMarker {}
119
120/// Compile-time helper used by generated code. Calling
121/// `assert_security_context::<T>()` requires `T: SecurityContextMarker` —
122/// so any type the macro classifies as "security context" must explicitly
123/// opt into the marker trait.
124#[doc(hidden)]
125pub const fn assert_security_context<T: SecurityContextMarker + ?Sized>() {}
126
127/// Error returned by the generated `try_from_i32` inherent method on a
128/// `ProtoBridge` enum when the wire value does not correspond to any known
129/// Rust variant. The parallel infallible `From<i32>` impl silently falls
130/// back to `Default::default()` for unknown discriminants — use
131/// `try_from_i32` when callers need to detect the unknown-variant case.
132#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
133#[error("unknown enum discriminant: {0}")]
134pub struct UnknownEnumDiscriminant(pub i32);
135
136/// Error returned by the generated `try_from_proto` inherent method on a
137/// `ProtoBridge` struct when a `#[proto_bridge(via_string)]` field carries a
138/// value that fails to parse via `FromStr`. The parallel infallible
139/// `From<Proto>` impl panics with `.expect(...)` on malformed input — use
140/// `try_from_proto` to convert wire input from peers without exposing a
141/// remote-DoS surface.
142#[derive(Debug, thiserror::Error)]
143#[error("proto bridge: invalid `{field}` value (could not parse from string): {source}")]
144pub struct ViaStringParseError {
145    pub field: &'static str,
146    #[source]
147    pub source: Box<dyn std::error::Error + Send + Sync + 'static>,
148}
149
150/// Fallible proto → Rust conversion for inbound wire data.
151///
152/// `#[derive(ProtoBridge)]` emits both an infallible `From<Proto>` and a
153/// fallible counterpart. The infallible one panics on a malformed
154/// `#[proto_bridge(via_string)]` field, which is fine for data this process
155/// produced and unacceptable for data a peer sent — a malformed UUID from a
156/// remote would take the process down. Generated gRPC clients and hand-written
157/// tonic servers therefore convert inbound messages through this trait.
158///
159/// The derive implements it for structs and enums alike, so codegen can call it
160/// uniformly. A hand-written proto bridge used as an RPC response type must
161/// implement it too; the missing impl is a compile error at the call site
162/// rather than a silent fall-back to the panicking path.
163pub trait TryFromProto<P>: Sized {
164    /// Convert a proto message into its Rust representation.
165    ///
166    /// # Errors
167    /// Returns [`ViaStringParseError`] when a `via_string` field carries a
168    /// value its `FromStr` impl rejects.
169    fn try_from_proto_wire(proto: P) -> Result<Self, ViaStringParseError>;
170}
171
172/// Logging hook called from generated `From<i32>` impls when the wire value
173/// does not correspond to any known Rust variant.
174///
175/// Centralized here (not inlined into the macro expansion) so SDK crates
176/// that derive `ProtoBridge` do not need a direct `tracing` dependency, and
177/// so the observability behavior can evolve in one place without
178/// re-expanding every macro consumer.
179#[doc(hidden)]
180pub fn log_unknown_enum_discriminant(discriminant: i32, rust_type: &'static str) {
181    tracing::warn!(
182        discriminant,
183        rust_type,
184        "proto bridge: unknown enum discriminant; falling back to Default. \
185         Use `try_from_i32` if the caller needs to detect this case."
186    );
187}