aws_smithy_schema/schema/protocol.rs
1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Client protocol traits for protocol-agnostic request serialization and response deserialization.
7//!
8//! [`ClientProtocolInner`] is the trait implementors write. It carries associated
9//! `Request` / `Response` types and allows transport-agnostic protocols (the SEP calls
10//! this out as a requirement).
11//!
12//! [`ClientProtocol`] is the object-safe view that callers use through `dyn`. It's
13//! parameterized over concrete request/response types (defaulted to HTTP) so
14//! [`SharedClientProtocol`] can be stored in a [`ConfigBag`] and swapped at runtime.
15//!
16//! A blanket impl (`impl<P: ClientProtocolInner> ClientProtocol<P::Request, P::Response> for P`)
17//! means implementors only write `ClientProtocolInner`; the object-safe view comes for
18//! free. This mirrors the [`Codec`](crate::codec::Codec) / [`DynCodec`](crate::codec::DynCodec)
19//! pair in the codec module — the same "static-dispatch inner trait + object-safe sibling"
20//! pattern.
21//!
22//! # Implementing a custom protocol
23//!
24//! Third parties can create custom protocols and use them with any client without
25//! modifying a code generator.
26//!
27//! ```ignore
28//! use aws_smithy_schema::protocol::{apply_http_endpoint, ClientProtocolInner};
29//! use aws_smithy_schema::{Schema, ShapeId};
30//! use aws_smithy_schema::serde::SerializableStruct;
31//!
32//! #[derive(Debug)]
33//! struct MyProtocol {
34//! codec: MyJsonCodec,
35//! }
36//!
37//! impl ClientProtocolInner for MyProtocol {
38//! type Request = aws_smithy_runtime_api::http::Request;
39//! type Response = aws_smithy_runtime_api::http::Response;
40//!
41//! fn protocol_id(&self) -> &ShapeId { &MY_PROTOCOL_ID }
42//!
43//! fn serialize_request(
44//! &self,
45//! input: &dyn SerializableStruct,
46//! input_schema: &Schema,
47//! endpoint: &str,
48//! cfg: &ConfigBag,
49//! ) -> Result<Self::Request, SerdeError> {
50//! todo!()
51//! }
52//!
53//! fn deserialize_response<'a>(
54//! &self,
55//! response: &'a Self::Response,
56//! output_schema: &Schema,
57//! cfg: &ConfigBag,
58//! ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
59//! todo!()
60//! }
61//!
62//! fn update_endpoint(
63//! &self,
64//! request: &mut Self::Request,
65//! endpoint: &aws_smithy_types::endpoint::Endpoint,
66//! cfg: &ConfigBag,
67//! ) -> Result<(), SerdeError> {
68//! apply_http_endpoint(request, endpoint, cfg)
69//! }
70//! }
71//! ```
72
73use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
74use crate::{Schema, ShapeId};
75use aws_smithy_types::config_bag::ConfigBag;
76use aws_smithy_types::endpoint::Endpoint;
77
78/// Statically-dispatched client protocol trait — the one implementors write.
79///
80/// `Request` and `Response` are associated types so a protocol can target any transport
81/// (HTTP, MQTT, Unix-socket, in-memory, …). For the common HTTP case, set both to
82/// `aws_smithy_runtime_api::http::Request` / `Response`.
83///
84/// Callers who need to store a protocol behind `dyn` (e.g., in a [`ConfigBag`] for
85/// runtime swapping) should use the object-safe [`ClientProtocol`] trait instead.
86/// Every `ClientProtocolInner` is automatically a
87/// `ClientProtocol<Self::Request, Self::Response>` via a blanket impl, so implementors
88/// never write `ClientProtocol` manually.
89///
90/// See [`apply_http_endpoint`] for the canonical HTTP implementation of
91/// `update_endpoint`.
92///
93/// # Lifecycle
94///
95/// Instances are immutable and thread-safe. They are typically created once and
96/// shared across all requests for a client.
97pub trait ClientProtocolInner: Send + Sync + std::fmt::Debug {
98 /// The protocol's request message type (e.g., `http::Request`).
99 type Request;
100
101 /// The protocol's response message type (e.g., `http::Response`).
102 type Response;
103
104 /// Returns the Smithy shape ID of this protocol.
105 fn protocol_id(&self) -> &ShapeId;
106
107 /// Serializes an operation input into a request message.
108 fn serialize_request(
109 &self,
110 input: &dyn SerializableStruct,
111 input_schema: &Schema,
112 endpoint: &str,
113 cfg: &ConfigBag,
114 ) -> Result<Self::Request, SerdeError>;
115
116 /// Deserializes a response message, returning a boxed [`ShapeDeserializer`] over
117 /// the response body.
118 ///
119 /// The deserializer reads only body members. Callers that also need to read
120 /// transport-bound members (HTTP headers, status code) do that directly in
121 /// generated code before consuming the deserializer.
122 fn deserialize_response<'a>(
123 &self,
124 response: &'a Self::Response,
125 output_schema: &Schema,
126 cfg: &ConfigBag,
127 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
128
129 /// Updates a previously serialized request with a resolved endpoint.
130 ///
131 /// Required by SEP requirement 7. The orchestrator calls this after endpoint
132 /// resolution, which happens *after* `serialize_request`.
133 ///
134 /// HTTP protocols should implement this as:
135 /// ```ignore
136 /// apply_http_endpoint(request, endpoint, cfg)
137 /// ```
138 /// (See [`apply_http_endpoint`].) Non-HTTP protocols implement the transport's
139 /// equivalent.
140 fn update_endpoint(
141 &self,
142 request: &mut Self::Request,
143 endpoint: &Endpoint,
144 cfg: &ConfigBag,
145 ) -> Result<(), SerdeError>;
146
147 /// Returns the codec used for payload (de)serialization, if any.
148 ///
149 /// See [`DynCodec`](crate::codec::DynCodec) for why the codec is exposed
150 /// through the object-safe sibling.
151 fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
152 None
153 }
154}
155
156/// Object-safe view of [`ClientProtocolInner`] parameterized over concrete
157/// request / response types.
158///
159/// This is what callers hold behind `dyn` — for example,
160/// [`SharedClientProtocol`] stores `Arc<dyn ClientProtocol<Req, Res>>` so the
161/// protocol can be swapped at runtime. The generic `Req` / `Res` parameters
162/// default to HTTP so existing call sites remain source-compatible.
163///
164/// Every `ClientProtocolInner` gets `ClientProtocol` for free via a blanket
165/// impl; implementors should write `ClientProtocolInner` only.
166pub trait ClientProtocol<
167 Req = aws_smithy_runtime_api::http::Request,
168 Res = aws_smithy_runtime_api::http::Response,
169>: Send + Sync + std::fmt::Debug
170{
171 /// Returns the Smithy shape ID of this protocol.
172 fn protocol_id(&self) -> &ShapeId;
173
174 /// Serializes an operation input into a request message.
175 fn serialize_request(
176 &self,
177 input: &dyn SerializableStruct,
178 input_schema: &Schema,
179 endpoint: &str,
180 cfg: &ConfigBag,
181 ) -> Result<Req, SerdeError>;
182
183 /// Deserializes a response message, returning a boxed [`ShapeDeserializer`].
184 fn deserialize_response<'a>(
185 &self,
186 response: &'a Res,
187 output_schema: &Schema,
188 cfg: &ConfigBag,
189 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError>;
190
191 /// Updates a previously serialized request with a resolved endpoint.
192 fn update_endpoint(
193 &self,
194 request: &mut Req,
195 endpoint: &Endpoint,
196 cfg: &ConfigBag,
197 ) -> Result<(), SerdeError>;
198
199 /// Returns the codec used for payload (de)serialization, if any.
200 fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec>;
201}
202
203// Blanket impl: any `ClientProtocolInner` is automatically a `ClientProtocol`
204// parameterized over its associated `Request` / `Response` types.
205impl<P> ClientProtocol<P::Request, P::Response> for P
206where
207 P: ClientProtocolInner,
208{
209 fn protocol_id(&self) -> &ShapeId {
210 <Self as ClientProtocolInner>::protocol_id(self)
211 }
212
213 fn serialize_request(
214 &self,
215 input: &dyn SerializableStruct,
216 input_schema: &Schema,
217 endpoint: &str,
218 cfg: &ConfigBag,
219 ) -> Result<P::Request, SerdeError> {
220 <Self as ClientProtocolInner>::serialize_request(self, input, input_schema, endpoint, cfg)
221 }
222
223 fn deserialize_response<'a>(
224 &self,
225 response: &'a P::Response,
226 output_schema: &Schema,
227 cfg: &ConfigBag,
228 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
229 <Self as ClientProtocolInner>::deserialize_response(self, response, output_schema, cfg)
230 }
231
232 fn update_endpoint(
233 &self,
234 request: &mut P::Request,
235 endpoint: &Endpoint,
236 cfg: &ConfigBag,
237 ) -> Result<(), SerdeError> {
238 <Self as ClientProtocolInner>::update_endpoint(self, request, endpoint, cfg)
239 }
240
241 fn payload_codec(&self) -> Option<&dyn crate::codec::DynCodec> {
242 <Self as ClientProtocolInner>::payload_codec(self)
243 }
244}
245
246/// Applies a resolved endpoint to an HTTP request.
247///
248/// This is the canonical HTTP implementation of
249/// [`ClientProtocolInner::update_endpoint`]. HTTP protocols should delegate to it.
250///
251/// Handles endpoint prefixes (for `EndpointPrefix`-enabled operations) and
252/// endpoint-supplied headers.
253pub fn apply_http_endpoint(
254 request: &mut aws_smithy_runtime_api::http::Request,
255 endpoint: &Endpoint,
256 cfg: &ConfigBag,
257) -> Result<(), SerdeError> {
258 use std::borrow::Cow;
259
260 let endpoint_prefix = cfg.load::<aws_smithy_runtime_api::client::endpoint::EndpointPrefix>();
261 let endpoint_url = match endpoint_prefix {
262 None => Cow::Borrowed(endpoint.url()),
263 Some(prefix) => {
264 let parsed: http::Uri = endpoint
265 .url()
266 .parse()
267 .map_err(|e| SerdeError::custom(format!("invalid endpoint URI: {e}")))?;
268 let scheme = parsed.scheme_str().unwrap_or_default();
269 let prefix = prefix.as_str();
270 let authority = parsed.authority().map(|a| a.as_str()).unwrap_or_default();
271 let path_and_query = parsed
272 .path_and_query()
273 .map(|pq| pq.as_str())
274 .unwrap_or_default();
275 Cow::Owned(format!("{scheme}://{prefix}{authority}{path_and_query}"))
276 }
277 };
278
279 request.uri_mut().set_endpoint(&endpoint_url).map_err(|e| {
280 SerdeError::custom(format!("failed to apply endpoint `{endpoint_url}`: {e}"))
281 })?;
282
283 for (header_name, header_values) in endpoint.headers() {
284 request.headers_mut().remove(header_name);
285 for value in header_values {
286 request
287 .headers_mut()
288 .append(header_name.to_owned(), value.to_owned());
289 }
290 }
291
292 Ok(())
293}
294
295/// A shared, type-erased client protocol stored in a [`ConfigBag`].
296///
297/// Wraps `Arc<dyn ClientProtocol<Req, Res>>` so a protocol can be stored and
298/// retrieved from the config bag for runtime protocol selection.
299///
300/// Defaults to HTTP transport types. Custom transports would use
301/// `SharedClientProtocol<MyReq, MyRes>` and would need their own `Storable`
302/// adaptation (not provided here — today only HTTP has a `Storable` impl,
303/// reflecting the fact that the orchestrator is HTTP-concrete).
304#[derive(Debug)]
305pub struct SharedClientProtocol<
306 Req = aws_smithy_runtime_api::http::Request,
307 Res = aws_smithy_runtime_api::http::Response,
308> {
309 inner: std::sync::Arc<dyn ClientProtocol<Req, Res>>,
310}
311
312// Manual `Clone` — `Arc` is cheaply cloneable regardless of whether the inner
313// `Req` / `Res` types are themselves `Clone`, so this impl avoids a spurious
314// `Req: Clone, Res: Clone` bound that `#[derive(Clone)]` would introduce.
315impl<Req, Res> Clone for SharedClientProtocol<Req, Res> {
316 fn clone(&self) -> Self {
317 Self {
318 inner: self.inner.clone(),
319 }
320 }
321}
322
323impl<Req, Res> SharedClientProtocol<Req, Res>
324where
325 Req: 'static,
326 Res: 'static,
327{
328 /// Creates a new shared protocol from any [`ClientProtocol<Req, Res>`] impl.
329 ///
330 /// In practice callers pass a concrete type that implements
331 /// [`ClientProtocolInner`] — the blanket `impl<P: ClientProtocolInner>
332 /// ClientProtocol<P::Request, P::Response> for P` makes every
333 /// `ClientProtocolInner` automatically usable here.
334 pub fn new<P>(protocol: P) -> Self
335 where
336 P: ClientProtocol<Req, Res> + 'static,
337 {
338 Self {
339 inner: std::sync::Arc::new(protocol),
340 }
341 }
342}
343
344impl<Req, Res> std::ops::Deref for SharedClientProtocol<Req, Res> {
345 type Target = dyn ClientProtocol<Req, Res>;
346
347 fn deref(&self) -> &Self::Target {
348 &*self.inner
349 }
350}
351
352// Only the HTTP specialization is storable in the config bag, matching the
353// orchestrator's HTTP-concrete wiring today. This is paired with the three
354// `protocol(…)` setters — `aws_types::SdkConfig::Builder::protocol`,
355// `aws_config::ConfigLoader::protocol`, and the generated
356// `ConfigBuilder::protocol` — all of which accept `impl ClientProtocol +
357// 'static` (resolving via defaults to the HTTP specialization) and store
358// the resulting `SharedClientProtocol<http::Request, http::Response>` here.
359//
360// Non-HTTP transports would add their own Storable newtype alongside their
361// transport integration (with its own dedicated setter) rather than
362// generalizing this impl — see §10.2 of the implementation overview.
363impl aws_smithy_types::config_bag::Storable
364 for SharedClientProtocol<
365 aws_smithy_runtime_api::http::Request,
366 aws_smithy_runtime_api::http::Response,
367 >
368{
369 type Storer = aws_smithy_types::config_bag::StoreReplace<Self>;
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
376 use crate::{Schema, ShapeId};
377 use aws_smithy_runtime_api::http::{Request, Response};
378 use aws_smithy_types::body::SdkBody;
379 use aws_smithy_types::config_bag::{ConfigBag, Layer};
380 use aws_smithy_types::endpoint::Endpoint;
381
382 /// Minimal protocol impl that uses the HTTP apply_http_endpoint helper.
383 #[derive(Debug)]
384 struct StubProtocol;
385
386 static STUB_ID: ShapeId = ShapeId::from_static("test#StubProtocol", "test", "StubProtocol");
387
388 impl ClientProtocolInner for StubProtocol {
389 type Request = Request;
390 type Response = Response;
391
392 fn protocol_id(&self) -> &ShapeId {
393 &STUB_ID
394 }
395 fn serialize_request(
396 &self,
397 _input: &dyn SerializableStruct,
398 _input_schema: &Schema,
399 _endpoint: &str,
400 _cfg: &ConfigBag,
401 ) -> Result<Request, SerdeError> {
402 unimplemented!()
403 }
404 fn deserialize_response<'a>(
405 &self,
406 _response: &'a Response,
407 _output_schema: &Schema,
408 _cfg: &ConfigBag,
409 ) -> Result<Box<dyn ShapeDeserializer + 'a>, SerdeError> {
410 unimplemented!()
411 }
412 fn update_endpoint(
413 &self,
414 request: &mut Request,
415 endpoint: &Endpoint,
416 cfg: &ConfigBag,
417 ) -> Result<(), SerdeError> {
418 apply_http_endpoint(request, endpoint, cfg)
419 }
420 }
421
422 fn request_with_uri(uri: &str) -> Request {
423 let mut req = Request::new(SdkBody::empty());
424 req.set_uri(uri).unwrap();
425 req
426 }
427
428 #[test]
429 fn basic_endpoint() {
430 let proto = StubProtocol;
431 let mut req = request_with_uri("/original/path");
432 let endpoint = Endpoint::builder()
433 .url("https://service.us-east-1.amazonaws.com")
434 .build();
435 let cfg = ConfigBag::base();
436
437 ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
438 assert_eq!(
439 req.uri(),
440 "https://service.us-east-1.amazonaws.com/original/path"
441 );
442 }
443
444 #[test]
445 fn endpoint_with_prefix() {
446 let proto = StubProtocol;
447 let mut req = request_with_uri("/path");
448 let endpoint = Endpoint::builder()
449 .url("https://service.us-east-1.amazonaws.com")
450 .build();
451 let mut cfg = ConfigBag::base();
452 let mut layer = Layer::new("test");
453 layer.store_put(
454 aws_smithy_runtime_api::client::endpoint::EndpointPrefix::new("myprefix.").unwrap(),
455 );
456 cfg.push_shared_layer(layer.freeze());
457
458 ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
459 assert_eq!(
460 req.uri(),
461 "https://myprefix.service.us-east-1.amazonaws.com/path"
462 );
463 }
464
465 #[test]
466 fn endpoint_with_headers() {
467 let proto = StubProtocol;
468 let mut req = request_with_uri("/path");
469 let endpoint = Endpoint::builder()
470 .url("https://example.com")
471 .header("x-custom", "value1")
472 .header("x-custom", "value2")
473 .build();
474 let cfg = ConfigBag::base();
475
476 ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
477 assert_eq!(req.uri(), "https://example.com/path");
478 let values: Vec<&str> = req.headers().get_all("x-custom").collect();
479 assert_eq!(values, vec!["value1", "value2"]);
480 }
481
482 #[test]
483 fn endpoint_with_path() {
484 let proto = StubProtocol;
485 let mut req = request_with_uri("/operation");
486 let endpoint = Endpoint::builder().url("https://example.com/base").build();
487 let cfg = ConfigBag::base();
488
489 ClientProtocolInner::update_endpoint(&proto, &mut req, &endpoint, &cfg).unwrap();
490 assert_eq!(req.uri(), "https://example.com/base/operation");
491 }
492}