Skip to content

Commit ab47c43

Browse files
committed
feat(call): Add call request and response objects for internal generic API
This change introduces data objects for the internal generic API. We introduce separate objects for Unary and Streaming (e.g., `UnaryRequest` vs `StreamingRequest`, `UnaryResponseWriter` vs `StreamingResponseWriter`) as unifying reference and owned APIs proved difficult. The input side (`UnaryRequest`, `StreamingRequest`) is straightforward, providing access to metadata and the message/stream. The output side (`StreamingResponseWriter`) is slightly nuanced, offering a "staged builder" like API: 1. `send_initial_metadata(metadata)` transitions to a `BodyWriter`. 2. The `BodyWriter` allows writing the actual stream and sending trailers. A unit test (`test_interceptor_composition`) is added to demonstrate how a writer can be intercepted, which is more nuanced than intercepting the read input stream. UnaryMethod ByteStream Handler Unary stuff Refactor message traits call changes Change hander call options(might probably be deleted) since call options shouldn't contain metadata.
1 parent 17e457a commit ab47c43

12 files changed

Lines changed: 1006 additions & 0 deletions

grpc/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ allowed_external_types = [
1717
default = ["dns", "_runtime-tokio", "protobuf"]
1818
protobuf = ["dep:protobuf"]
1919
prost = ["dep:prost"]
20+
unsafe-downcast = []
2021
dns = ["dep:hickory-resolver", "_runtime-tokio"]
2122
# The following feature is used to ensure all modules use the runtime
2223
# abstraction instead of using tokio directly.
@@ -60,6 +61,7 @@ tower = { version = "0.5.2", features = [
6061
tower-service = "0.3.3"
6162
url = "2.5.0"
6263
trait-variant = "0.1"
64+
send-future = "0.1"
6365

6466
[dev-dependencies]
6567
async-stream = "0.3.6"

grpc/src/call.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
pub mod handler_call_options;
2+
pub mod lazy;
3+
pub mod message_wrapper;
4+
pub mod metadata;
5+
pub mod metadata_writer;
6+
pub mod streaming_request;
7+
pub mod streaming_response_writer;
8+
pub mod streaming_response_writer_ext;
9+
pub use handler_call_options::HandlerCallOptions;
10+
pub use lazy::Lazy;
11+
pub use message_wrapper::{Incoming, Outgoing};
12+
pub use metadata::Metadata;
13+
pub use streaming_request::StreamingRequest;
14+
pub use streaming_response_writer::{StreamingResponseBodyWriter, StreamingResponseWriter};
15+
16+
#[cfg(test)]
17+
pub(crate) mod test_util;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// Options for method handlers.
2+
#[derive(Debug, Default, Clone)]
3+
pub struct HandlerCallOptions {
4+
pub compression_encoding: Option<String>,
5+
}

grpc/src/call/lazy.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// ... (imports remain)
2+
use crate::message::AsMut;
3+
use crate::Status;
4+
use send_future::SendFuture;
5+
6+
#[trait_variant::make(Send)]
7+
pub trait Lazy<Req>: Send
8+
where
9+
Req: AsMut,
10+
{
11+
async fn resolve(self, target: <Req as AsMut>::Mut<'_>) -> Result<(), Status>;
12+
}
13+
14+
#[trait_variant::make(Send)]
15+
pub trait Mapper<Req>: Send
16+
where
17+
Req: AsMut,
18+
{
19+
async fn map(self, target: <Req as AsMut>::Mut<'_>) -> Result<(), Status>;
20+
}
21+
22+
// --- Zero-Cost Map Combinator ---
23+
24+
pub struct MapLazy<L, M> {
25+
pub inner: L,
26+
pub mapper: M,
27+
}
28+
29+
impl<Req, L, M> Lazy<Req> for MapLazy<L, M>
30+
where
31+
Req: AsMut,
32+
L: Lazy<Req>,
33+
M: Mapper<Req>,
34+
{
35+
async fn resolve(self, mut target: <Req as AsMut>::Mut<'_>) -> Result<(), Status> {
36+
let inner_target = <Req as AsMut>::reborrow_view(&mut target);
37+
self.inner.resolve(inner_target).send().await?;
38+
self.mapper.map(target).send().await
39+
}
40+
}
41+
42+
// --- Sync Map Combinator (Sugar) ---
43+
44+
pub struct SyncMapLazy<L, F> {
45+
pub inner: L,
46+
pub f: F,
47+
}
48+
49+
impl<Req, L, F> Lazy<Req> for SyncMapLazy<L, F>
50+
where
51+
Req: AsMut,
52+
L: Lazy<Req>,
53+
F: FnOnce(<Req as AsMut>::Mut<'_>) -> Result<(), Status> + Send,
54+
{
55+
async fn resolve(self, mut target: <Req as AsMut>::Mut<'_>) -> Result<(), Status> {
56+
let inner_target = <Req as AsMut>::reborrow_view(&mut target);
57+
self.inner.resolve(inner_target).send().await?;
58+
(self.f)(target)
59+
}
60+
}
61+
62+
pub trait LazyExt<Req>: Lazy<Req> + Sized
63+
where
64+
Req: AsMut,
65+
{
66+
fn then<M>(self, mapper: M) -> MapLazy<Self, M> {
67+
MapLazy {
68+
inner: self,
69+
mapper,
70+
}
71+
}
72+
73+
// Sugar for synchronous closures
74+
fn map<F>(self, f: F) -> SyncMapLazy<Self, F>
75+
where
76+
F: FnOnce(<Req as AsMut>::Mut<'_>) -> Result<(), Status> + Send,
77+
{
78+
SyncMapLazy { inner: self, f }
79+
}
80+
}
81+
82+
impl<Req, L: Lazy<Req>> LazyExt<Req> for L where Req: AsMut {}
83+
84+
// --- Test Usage ---
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::*;
89+
use protobuf_well_known_types::Timestamp;
90+
91+
// A struct for our lazy logic (Zero Cost)
92+
struct SetSeconds {
93+
value: i64,
94+
}
95+
96+
impl Lazy<Timestamp> for SetSeconds {
97+
async fn resolve(self, mut target: <Timestamp as AsMut>::Mut<'_>) -> Result<(), Status> {
98+
target.set_seconds(self.value);
99+
Ok(())
100+
}
101+
}
102+
103+
// A struct for our mapping logic (Zero Cost)
104+
struct AddFive;
105+
impl Mapper<Timestamp> for AddFive {
106+
async fn map(self, mut target: <Timestamp as AsMut>::Mut<'_>) -> Result<(), Status> {
107+
let current = target.seconds();
108+
target.set_seconds(current + 5);
109+
Ok(())
110+
}
111+
}
112+
113+
#[tokio::test]
114+
async fn test_zero_allocation_chain() {
115+
// 1. Create the base lazy (Struct, not closure)
116+
let lazy = SetSeconds { value: 10 };
117+
118+
// 2. Map it with a struct mapper
119+
let chained = lazy.then(AddFive);
120+
121+
let mut msg = Timestamp::new();
122+
123+
chained.resolve(msg.as_mut()).await.unwrap();
124+
125+
assert_eq!(msg.seconds(), 15);
126+
}
127+
128+
#[tokio::test]
129+
async fn test_sync_closure_sugar() {
130+
// 1. Create the base lazy
131+
let lazy = SetSeconds { value: 10 };
132+
133+
// 2. Map it with a sync closure (Sugar)
134+
let chained = lazy.map(|mut target| {
135+
let current = target.seconds();
136+
target.set_seconds(current + 20);
137+
Ok(())
138+
});
139+
140+
let mut msg = Timestamp::new();
141+
// ZeroBox allocation! Sync closures are easy.
142+
chained.resolve(msg.as_mut()).await.unwrap();
143+
144+
assert_eq!(msg.seconds(), 30);
145+
}
146+
}

grpc/src/call/message_wrapper.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// A raw message containing bytes and optional read options.
2+
pub struct Incoming<B> {
3+
pub message_bytes: B,
4+
pub options: Option<MessageReadOptions>,
5+
}
6+
7+
#[derive(Debug, Clone, Copy)]
8+
pub struct MessageReadOptions {
9+
pub compressed: bool,
10+
}
11+
12+
/// A wrapped message with optional write options.
13+
pub struct Outgoing<T> {
14+
pub message: T,
15+
pub options: Option<MessageWriteOptions>,
16+
}
17+
18+
impl<T> Outgoing<T> {
19+
pub fn new(message: T) -> Self {
20+
Self {
21+
message,
22+
options: None,
23+
}
24+
}
25+
26+
pub fn with_options(message: T, options: MessageWriteOptions) -> Self {
27+
Self {
28+
message,
29+
options: Some(options),
30+
}
31+
}
32+
}
33+
34+
impl<T> From<T> for Outgoing<T> {
35+
fn from(message: T) -> Self {
36+
Self::new(message)
37+
}
38+
}
39+
40+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41+
pub enum CompressionEncoding {
42+
#[default]
43+
Inherit,
44+
Enabled,
45+
Disabled,
46+
}
47+
48+
#[derive(Debug, Default, Clone, Copy)]
49+
pub struct MessageWriteOptions {
50+
pub compression: CompressionEncoding,
51+
}

grpc/src/call/metadata.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use http::HeaderMap;
2+
3+
#[derive(Debug, Default, Clone, PartialEq)]
4+
pub struct Metadata {
5+
pub(crate) inner: HeaderMap,
6+
}
7+
8+
impl Metadata {
9+
pub fn new(inner: HeaderMap) -> Self {
10+
Self { inner }
11+
}
12+
13+
/// Returns the method name.
14+
pub fn method_name(&self) -> Option<&str> {
15+
self.inner.get("path").and_then(|v| v.to_str().ok())
16+
}
17+
18+
/// Returns the `grpc-encoding` header value.
19+
pub fn encoding(&self) -> Option<&str> {
20+
self.inner
21+
.get("grpc-encoding")
22+
.and_then(|v| v.to_str().ok())
23+
}
24+
25+
/// Returns an iterator over the `grpc-accept-encoding` values.
26+
/// Handles multiple headers and comma-separated values.
27+
pub fn accept_encodings(&self) -> impl Iterator<Item = &str> + '_ {
28+
self.inner
29+
.get_all("grpc-accept-encoding")
30+
.iter()
31+
.filter_map(|v| v.to_str().ok())
32+
.flat_map(|s| s.split(','))
33+
.map(|s| s.trim())
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use super::*;
40+
use http::HeaderValue;
41+
42+
#[test]
43+
fn test_method_name() {
44+
let mut map = HeaderMap::new();
45+
map.insert("path", HeaderValue::from_static("/Service/Method"));
46+
let metadata = Metadata::new(map);
47+
48+
assert_eq!(metadata.method_name(), Some("/Service/Method"));
49+
50+
let empty = Metadata::default();
51+
assert_eq!(empty.method_name(), None);
52+
}
53+
54+
#[test]
55+
fn test_encoding() {
56+
let mut map = HeaderMap::new();
57+
map.insert("grpc-encoding", HeaderValue::from_static("gzip"));
58+
let metadata = Metadata::new(map);
59+
60+
assert_eq!(metadata.encoding(), Some("gzip"));
61+
62+
let empty = Metadata::default();
63+
assert_eq!(empty.encoding(), None);
64+
}
65+
66+
#[test]
67+
fn test_accept_encodings() {
68+
let mut map = HeaderMap::new();
69+
map.insert(
70+
"grpc-accept-encoding",
71+
HeaderValue::from_static("gzip,identity"),
72+
);
73+
let metadata = Metadata::new(map);
74+
75+
let encodings: Vec<_> = metadata.accept_encodings().collect();
76+
assert_eq!(encodings, vec!["gzip", "identity"]);
77+
78+
// Test multiple headers
79+
let mut map = HeaderMap::new();
80+
map.append("grpc-accept-encoding", HeaderValue::from_static("gzip"));
81+
map.append(
82+
"grpc-accept-encoding",
83+
HeaderValue::from_static("deflate, br"),
84+
);
85+
let metadata = Metadata::new(map);
86+
87+
let encodings: Vec<_> = metadata.accept_encodings().collect();
88+
assert_eq!(encodings, vec!["gzip", "deflate", "br"]);
89+
}
90+
}

grpc/src/call/metadata_writer.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use std::marker::Send;
2+
3+
use crate::call::Metadata;
4+
use crate::Status;
5+
6+
/// A trait for writing initial metadata.
7+
#[trait_variant::make(Send)]
8+
pub trait InitialMetadataWriter: Send {
9+
/// Sends initial metadata.
10+
async fn send_initial_metadata(self, metadata: Metadata) -> Result<(), Status>;
11+
}
12+
13+
/// A trait for writing trailing metadata.
14+
#[trait_variant::make(Send)]
15+
pub trait TrailingMetadataWriter: Send {
16+
/// Sends trailing metadata.
17+
async fn send_trailing_metadata(self, metadata: Metadata) -> Result<(), Status>;
18+
}

0 commit comments

Comments
 (0)