Skip to main content

vox_types/
client_middleware.rs

1use crate::server_middleware::BoxMiddlewareFuture;
2use crate::{
3    Extensions, Metadata, MetadataEntry, MetadataFlags, MetadataValue, MethodDescriptor, MethodId,
4    RequestCall, VoxError,
5};
6
7/// Borrowed per-call context exposed to client middleware.
8#[derive(Clone, Copy, Debug)]
9pub struct ClientContext<'a> {
10    method: Option<&'static MethodDescriptor>,
11    method_id: MethodId,
12    extensions: &'a Extensions,
13}
14
15impl<'a> ClientContext<'a> {
16    pub fn new(
17        method: Option<&'static MethodDescriptor>,
18        method_id: MethodId,
19        extensions: &'a Extensions,
20    ) -> Self {
21        Self {
22            method,
23            method_id,
24            extensions,
25        }
26    }
27
28    pub fn method(&self) -> Option<&'static MethodDescriptor> {
29        self.method
30    }
31
32    pub fn method_id(&self) -> MethodId {
33        self.method_id
34    }
35
36    pub fn extensions(&self) -> &'a Extensions {
37        self.extensions
38    }
39}
40
41/// Borrowed request wrapper exposed to client middleware.
42///
43/// This allows middleware to add dynamic metadata while keeping the backing
44/// storage alive until the wrapped caller finishes sending the request.
45pub struct ClientRequest<'call, 'state> {
46    call: &'state mut RequestCall<'call>,
47    owned_metadata: &'state mut OwnedMetadata,
48}
49
50impl<'call, 'state> ClientRequest<'call, 'state> {
51    pub fn new(
52        call: &'state mut RequestCall<'call>,
53        owned_metadata: &'state mut OwnedMetadata,
54    ) -> Self {
55        Self {
56            call,
57            owned_metadata,
58        }
59    }
60
61    pub fn call(&self) -> &RequestCall<'call> {
62        self.call
63    }
64
65    pub fn metadata(&self) -> &[MetadataEntry<'call>] {
66        &self.call.metadata
67    }
68
69    pub fn metadata_mut(&mut self) -> &mut Metadata<'call> {
70        &mut self.call.metadata
71    }
72
73    pub fn push_string_metadata(
74        &mut self,
75        key: &'static str,
76        value: impl Into<String>,
77        flags: MetadataFlags,
78    ) {
79        self.owned_metadata
80            .strings
81            .push(value.into().into_boxed_str());
82        let stored = self.owned_metadata.strings.last().unwrap();
83        // SAFETY: The boxed string is heap-allocated (stable address) and owned by
84        // `owned_metadata`, which lives in the same stack frame as `call` in
85        // MiddlewareCaller::call. It won't be dropped until after `call` is consumed.
86        let value: &'call str = unsafe { &*((&**stored) as *const str) };
87        self.call.metadata.push(MetadataEntry {
88            key: key.into(),
89            value: MetadataValue::String(value.into()),
90            flags,
91        });
92    }
93
94    pub fn push_bytes_metadata(
95        &mut self,
96        key: &'static str,
97        value: impl Into<Vec<u8>>,
98        flags: MetadataFlags,
99    ) {
100        self.owned_metadata
101            .bytes
102            .push(value.into().into_boxed_slice());
103        let stored = self.owned_metadata.bytes.last().unwrap();
104        // SAFETY: same reasoning as push_string_metadata above.
105        let value: &'call [u8] = unsafe { &*((&**stored) as *const [u8]) };
106        self.call.metadata.push(MetadataEntry {
107            key: key.into(),
108            value: MetadataValue::Bytes(value.into()),
109            flags,
110        });
111    }
112
113    pub fn push_u64_metadata(&mut self, key: &'static str, value: u64, flags: MetadataFlags) {
114        self.call.metadata.push(MetadataEntry {
115            key: key.into(),
116            value: MetadataValue::U64(value),
117            flags,
118        });
119    }
120}
121
122#[derive(Default)]
123pub struct OwnedMetadata {
124    strings: Vec<Box<str>>,
125    bytes: Vec<Box<[u8]>>,
126}
127
128#[derive(Clone, Copy)]
129pub enum ClientCallOutcome<'a> {
130    Response,
131    Error(&'a VoxError),
132}
133
134impl ClientCallOutcome<'_> {
135    pub fn is_ok(self) -> bool {
136        matches!(self, Self::Response)
137    }
138}
139
140pub trait ClientMiddleware: Send + Sync + 'static {
141    fn pre<'a, 'call>(
142        &'a self,
143        _context: &'a ClientContext<'a>,
144        _request: &'a mut ClientRequest<'call, 'a>,
145    ) -> BoxMiddlewareFuture<'a> {
146        Box::pin(async {})
147    }
148
149    fn post<'a>(
150        &'a self,
151        _context: &'a ClientContext<'a>,
152        _outcome: ClientCallOutcome<'a>,
153    ) -> BoxMiddlewareFuture<'a> {
154        Box::pin(async {})
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use crate::Payload;
161
162    use super::{ClientRequest, MetadataFlags, MethodId, OwnedMetadata, RequestCall};
163
164    #[test]
165    fn client_request_can_add_owned_metadata() {
166        let mut call = RequestCall {
167            method_id: MethodId(1),
168            metadata: vec![],
169            args: Payload::PostcardBytes(&[]),
170            schemas: Default::default(),
171        };
172        let mut owned = OwnedMetadata::default();
173        let mut request = ClientRequest::new(&mut call, &mut owned);
174        request.push_string_metadata("x-test", "value".to_string(), MetadataFlags::NONE);
175        request.push_bytes_metadata("x-bytes", vec![1, 2, 3], MetadataFlags::NONE);
176        request.push_u64_metadata("x-num", 7, MetadataFlags::NONE);
177
178        assert_eq!(request.metadata().len(), 3);
179        assert!(matches!(
180            &request.metadata()[0].value,
181            crate::MetadataValue::String(s) if s == "value"
182        ));
183        assert!(matches!(
184            &request.metadata()[1].value,
185            crate::MetadataValue::Bytes(bytes) if bytes.as_ref() == [1, 2, 3]
186        ));
187        assert!(matches!(
188            request.metadata()[2].value,
189            crate::MetadataValue::U64(7)
190        ));
191    }
192}