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}