Skip to main content

aws_smithy_http_server/schema/protocol/
registry.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Resolution from a service schema to its [`SharedServerProtocol`]s.
7//!
8//! Generated service builders resolve their protocols through [`ProtocolRegistry`]s instead of
9//! naming concrete protocol structs, so a protocol implemented outside this crate joins the
10//! schema-serde path by contributing a registry. A registry is a const collection of
11//! [`ProtocolRegistration`]s, so a crate keeps every protocol it provides — and each protocol's
12//! [`ProtocolOrder`] placement — in one place:
13//!
14//! ```ignore
15//! pub static MY_PROTOCOLS: ProtocolRegistry = ProtocolRegistry::new(&[
16//!     ProtocolRegistration::metadata_routed::<MyProtocol>("example.protocols#myProtocol")
17//!         .with_order(&[ProtocolOrder::Before("aws.protocols#restJson1")]),
18//! ]);
19//! ```
20//!
21//! A protocol registers as the routing kind it implements:
22//! [`ProtocolRegistration::metadata_routed`] for a
23//! [`MetadataRoutedProtocol`],
24//! [`ProtocolRegistration::body_routed`] for a
25//! [`BodyRoutedProtocol`].
26//!
27//! A service declaring several protocols is served by all of them. The claim order is decided
28//! only by [`ProtocolOrder`] constraints, resolved over every registered protocol — including
29//! ones the service does not serve, so `a Before b` and `b Before c` order `a` before `c` on a
30//! service serving only `a` and `c`. Registry and declaration order carry no meaning: two served
31//! protocols that the constraints leave unordered fail the build, as does the same protocol
32//! registered twice.
33
34use aws_smithy_types::Document;
35
36use crate::config::ServiceConfig;
37use crate::schema::routing::RouterBuildError;
38use crate::schema::ServiceSchema;
39
40use super::{BodyRoutedProtocol, MetadataRoutedProtocol, SharedServerProtocol};
41
42/// Places a protocol relative to another protocol, by shape ID, in a multi-protocol service's
43/// claim order. Constraints resolve transitively over every registered protocol, so a constraint
44/// against a protocol the service does not serve still orders the ones it does.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum ProtocolOrder {
48    /// Ask this protocol before the named one.
49    Before(&'static str),
50    /// Ask this protocol after the named one.
51    After(&'static str),
52}
53
54/// Everything a protocol factory sees.
55///
56/// `#[non_exhaustive]` — grows without breaking registries.
57#[non_exhaustive]
58#[derive(Debug)]
59pub struct ProtocolBuildContext<'a> {
60    /// The service schema the protocol is being built for.
61    pub service: &'static ServiceSchema<'static>,
62    /// Typed service-wide configuration shared by every protocol.
63    pub config: &'a ServiceConfig,
64    /// This protocol's own section from `customizationConfig.protocols.<its shape ID>`.
65    pub settings: Option<&'a Document>,
66}
67
68impl<'a> ProtocolBuildContext<'a> {
69    /// Creates a context with default service configuration and no protocol settings.
70    pub fn new(service: &'static ServiceSchema<'static>) -> Self {
71        static DEFAULT_CONFIG: std::sync::LazyLock<ServiceConfig> = std::sync::LazyLock::new(ServiceConfig::default);
72        Self {
73            service,
74            config: &DEFAULT_CONFIG,
75            settings: None,
76        }
77    }
78
79    /// Sets the service-wide configuration.
80    pub fn with_config(mut self, config: &'a ServiceConfig) -> Self {
81        self.config = config;
82        self
83    }
84
85    /// Sets the protocol's own settings section.
86    pub fn with_settings(mut self, settings: Option<&'a Document>) -> Self {
87        self.settings = settings;
88        self
89    }
90}
91
92/// Builds a protocol from its registered configuration into the erased handle carrying its
93/// routing kind. Produced by [`ProtocolRegistration::metadata_routed`] and
94/// [`ProtocolRegistration::body_routed`], never written by hand.
95pub type ProtocolFactory = fn(&ProtocolBuildContext<'_>) -> Result<SharedServerProtocol, RouterBuildError>;
96
97fn build_metadata_routed<P: MetadataRoutedProtocol>(
98    ctx: &ProtocolBuildContext<'_>,
99) -> Result<SharedServerProtocol, RouterBuildError> {
100    Ok(SharedServerProtocol::metadata_routed(P::from_build_context(ctx)?))
101}
102
103fn build_body_routed<P: BodyRoutedProtocol>(
104    ctx: &ProtocolBuildContext<'_>,
105) -> Result<SharedServerProtocol, RouterBuildError> {
106    Ok(SharedServerProtocol::body_routed(P::from_build_context(ctx)?))
107}
108
109/// A single protocol's entry in a [`ProtocolRegistry`].
110///
111/// Names the protocol it provides, wraps the factory that builds it, and carries the protocol's
112/// [`ProtocolOrder`] placement. Const-constructible, so registrations live in static registries.
113#[derive(Clone, Copy)]
114pub struct ProtocolRegistration {
115    protocol_id: &'static str,
116    build: ProtocolFactory,
117    order: &'static [ProtocolOrder],
118}
119
120impl ProtocolRegistration {
121    /// Registers the metadata-routed protocol `P` for the protocol trait `protocol_id`.
122    ///
123    /// `P` is built only for services whose schema declares `protocol_id`, and must answer the
124    /// same ID from [`protocol_id`](super::ServerProtocol::protocol_id).
125    pub const fn metadata_routed<P: MetadataRoutedProtocol>(protocol_id: &'static str) -> Self {
126        Self {
127            protocol_id,
128            build: build_metadata_routed::<P>,
129            order: &[],
130        }
131    }
132
133    /// Registers the body-routed protocol `P` for the protocol trait `protocol_id`.
134    ///
135    /// `P` is built only for services whose schema declares `protocol_id`, and must answer the
136    /// same ID from [`protocol_id`](super::ServerProtocol::protocol_id). The routing service
137    /// hands `P` only the service's non-streaming operations; see
138    /// [`BodyRoutedProtocol`].
139    pub const fn body_routed<P: BodyRoutedProtocol>(protocol_id: &'static str) -> Self {
140        Self {
141            protocol_id,
142            build: build_body_routed::<P>,
143            order: &[],
144        }
145    }
146
147    /// Constrains where the protocol sits in a multi-protocol service's claim order.
148    pub const fn with_order(mut self, order: &'static [ProtocolOrder]) -> Self {
149        self.order = order;
150        self
151    }
152
153    pub(crate) fn protocol_id(&self) -> &'static str {
154        self.protocol_id
155    }
156
157    pub(crate) fn order(&self) -> &'static [ProtocolOrder] {
158        self.order
159    }
160
161    pub(crate) fn build(&self, ctx: &ProtocolBuildContext<'_>) -> Result<SharedServerProtocol, RouterBuildError> {
162        let protocol = (self.build)(ctx)?;
163        if protocol.protocol_id().as_str() != self.protocol_id {
164            return Err(RouterBuildError::Configuration(format!(
165                "protocol registered as `{}` answers `{}` from `protocol_id()`",
166                self.protocol_id,
167                protocol.protocol_id()
168            )));
169        }
170        Ok(protocol)
171    }
172}
173
174impl std::fmt::Debug for ProtocolRegistration {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.debug_struct("ProtocolRegistration")
177            .field("protocol_id", &self.protocol_id)
178            .finish_non_exhaustive()
179    }
180}
181
182/// A const collection of [`ProtocolRegistration`]s.
183///
184/// Each protocol may be registered exactly once across every registry a service consults;
185/// a duplicate fails the build. Order within a registry carries no meaning.
186///
187/// [`MultiProtocolRoutingService`](crate::schema::routing::MultiProtocolRoutingService)
188/// always includes [`Self::BUILTIN`]. Claim priority comes only from [`ProtocolOrder`]
189/// constraints, resolved transitively over all registrations, including protocols the service
190/// does not serve. Reordering registries or the service's protocol declarations does not change
191/// priority. Constraints must order every pair of served protocols; unordered pairs and cycles
192/// fail the build.
193#[derive(Clone, Copy, Debug)]
194pub struct ProtocolRegistry {
195    registrations: &'static [ProtocolRegistration],
196}
197
198impl ProtocolRegistry {
199    /// The built-in protocols, constrained to the following claim order (first to last):
200    ///
201    /// 1. `smithy.protocols#rpcv2Cbor`
202    /// 2. `aws.protocols#awsJson1_0`
203    /// 3. `aws.protocols#awsJson1_1`
204    /// 4. `aws.protocols#restJson1`
205    /// 5. `aws.protocols#restXml`
206    ///
207    /// This follows Smithy's [AWS service protocol precision order] for the supported protocols.
208    /// See also [server protocol selection] for protocol identification and claiming.
209    ///
210    /// [AWS service protocol precision order]: https://smithy.io/2.0/guides/wire-protocol-selection.html#aws-service-protocol-precision
211    /// [server protocol selection]: https://smithy.io/2.0/guides/wire-protocol-selection.html#server-protocol-selection
212    ///
213    /// Only protocols declared by the service participate in request routing. Their relative
214    /// order remains the same when the service declares a subset of the built-ins.
215    /// Additional registrations can position custom protocols using [`ProtocolOrder`], but
216    /// there is no per-service override of the built-in relative order. Registering a built-in
217    /// again fails the build.
218    ///
219    /// This order affects wire behavior when requests match multiple protocols: the first
220    /// definite claim wins; if none exists, the first deferred claim wins, followed by the
221    /// first deferred rejection. See the [routing documentation](crate::schema::routing).
222    /// For example, an eligible request without `Content-Type` receives deferred claims from
223    /// both restJson1 and restXml; restJson1 wins if no protocol claims definitively.
224    ///
225    /// Adding a built-in protocol or changing ordering constraints can change the selected
226    /// protocol and its response format for ambiguous requests. Such changes require a wire
227    /// compatibility review.
228    pub const BUILTIN: ProtocolRegistry = ProtocolRegistry::new(&[
229        ProtocolRegistration::metadata_routed::<crate::schema::protocol::RpcV2Cbor>("smithy.protocols#rpcv2Cbor"),
230        ProtocolRegistration::metadata_routed::<crate::schema::protocol::AwsJson1_0>("aws.protocols#awsJson1_0")
231            .with_order(&[ProtocolOrder::After("smithy.protocols#rpcv2Cbor")]),
232        ProtocolRegistration::metadata_routed::<crate::schema::protocol::AwsJson1_1>("aws.protocols#awsJson1_1")
233            .with_order(&[ProtocolOrder::After("aws.protocols#awsJson1_0")]),
234        ProtocolRegistration::metadata_routed::<crate::schema::protocol::RestJson1>("aws.protocols#restJson1")
235            .with_order(&[ProtocolOrder::After("aws.protocols#awsJson1_1")]),
236        ProtocolRegistration::metadata_routed::<crate::schema::protocol::RestXml>("aws.protocols#restXml")
237            .with_order(&[ProtocolOrder::After("aws.protocols#restJson1")]),
238    ]);
239
240    /// Creates a registry over `registrations`.
241    pub const fn new(registrations: &'static [ProtocolRegistration]) -> Self {
242        Self { registrations }
243    }
244
245    pub(crate) fn registrations(&self) -> &'static [ProtocolRegistration] {
246        self.registrations
247    }
248
249    /// Builds the protocol with shape ID `protocol_id` for `service_schema`, without settings.
250    ///
251    /// Returns `Ok(None)` when this registry does not register `protocol_id` or the schema does not declare
252    /// it. Factory failures and registered-ID mismatches return `Err`.
253    /// Use [`Self::resolve_id_with_settings`] to supply a protocol's configuration.
254    pub fn resolve_id(
255        &self,
256        service_schema: &'static ServiceSchema<'static>,
257        protocol_id: &str,
258    ) -> Result<Option<SharedServerProtocol>, RouterBuildError> {
259        self.resolve_id_with_settings(service_schema, protocol_id, None)
260    }
261
262    /// Builds one declared protocol with its own settings document.
263    ///
264    /// Generated response tests pass the protocol's section from `customizationConfig.protocols`
265    /// so serialization uses the same options as the generated service builder.
266    /// Availability checks and factory errors behave as in [`Self::resolve_id`].
267    pub fn resolve_id_with_settings(
268        &self,
269        service_schema: &'static ServiceSchema<'static>,
270        protocol_id: &str,
271        settings: Option<&Document>,
272    ) -> Result<Option<SharedServerProtocol>, RouterBuildError> {
273        let declared = service_schema
274            .protocols()
275            .iter()
276            .any(|protocol| protocol.as_str() == protocol_id);
277        if !declared {
278            return Ok(None);
279        }
280        self.registrations
281            .iter()
282            .find(|registration| registration.protocol_id() == protocol_id)
283            .map(|registration| registration.build(&ProtocolBuildContext::new(service_schema).with_settings(settings)))
284            .transpose()
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use crate::schema::ServiceSchema;
291    use aws_smithy_schema::{shape_id, ShapeId};
292
293    use super::*;
294
295    const SERVICE_ID: ShapeId<'static> = shape_id!("example", "Service");
296
297    static REST_JSON_1: [ShapeId<'static>; 1] = [shape_id!("aws.protocols", "restJson1")];
298    static UNKNOWN: [ShapeId<'static>; 1] = [shape_id!("example.protocols", "myProtocol")];
299
300    static REST_JSON_1_SERVICE: ServiceSchema<'static> = ServiceSchema::new(SERVICE_ID, None, &REST_JSON_1, &[]);
301    static UNKNOWN_SERVICE: ServiceSchema<'static> = ServiceSchema::new(SERVICE_ID, None, &UNKNOWN, &[]);
302
303    #[test]
304    fn builtin_resolves_each_builtin_protocol() {
305        static PROTOCOLS: [[ShapeId<'static>; 1]; 5] = [
306            [shape_id!("aws.protocols", "restJson1")],
307            [shape_id!("aws.protocols", "restXml")],
308            [shape_id!("aws.protocols", "awsJson1_0")],
309            [shape_id!("aws.protocols", "awsJson1_1")],
310            [shape_id!("smithy.protocols", "rpcv2Cbor")],
311        ];
312        static SERVICES: [ServiceSchema<'static>; 5] = [
313            ServiceSchema::new(SERVICE_ID, None, &PROTOCOLS[0], &[]),
314            ServiceSchema::new(SERVICE_ID, None, &PROTOCOLS[1], &[]),
315            ServiceSchema::new(SERVICE_ID, None, &PROTOCOLS[2], &[]),
316            ServiceSchema::new(SERVICE_ID, None, &PROTOCOLS[3], &[]),
317            ServiceSchema::new(SERVICE_ID, None, &PROTOCOLS[4], &[]),
318        ];
319        for (service, id) in SERVICES.iter().zip([
320            "aws.protocols#restJson1",
321            "aws.protocols#restXml",
322            "aws.protocols#awsJson1_0",
323            "aws.protocols#awsJson1_1",
324            "smithy.protocols#rpcv2Cbor",
325        ]) {
326            let protocol = ProtocolRegistry::BUILTIN
327                .resolve_id(service, id)
328                .expect("factory succeeds")
329                .expect(id);
330            assert_eq!(protocol.protocol_id().as_str(), id);
331        }
332    }
333
334    #[test]
335    fn unknown_protocol_resolves_to_none() {
336        assert!(ProtocolRegistry::BUILTIN
337            .resolve_id(&UNKNOWN_SERVICE, "example.protocols#myProtocol")
338            .expect("unavailable protocol is not a build error")
339            .is_none());
340    }
341
342    #[test]
343    fn undeclared_protocol_resolves_to_none() {
344        assert!(ProtocolRegistry::BUILTIN
345            .resolve_id(&REST_JSON_1_SERVICE, "aws.protocols#restXml")
346            .expect("unavailable protocol is not a build error")
347            .is_none());
348    }
349
350    #[test]
351    fn resolution_passes_settings_to_the_selected_factory() {
352        static AWS_JSON_SERVICE: ServiceSchema<'static> =
353            ServiceSchema::new(SERVICE_ID, None, &[shape_id!("aws.protocols", "awsJson1_0")], &[]);
354        let settings = crate::schema::settings::parse_settings_json(br#"{"responseBindings":"invalid"}"#);
355        let err = ProtocolRegistry::BUILTIN
356            .resolve_id_with_settings(&AWS_JSON_SERVICE, "aws.protocols#awsJson1_0", Some(&settings))
357            .expect_err("AWS JSON must validate the supplied settings");
358        assert!(matches!(err, RouterBuildError::Configuration(message) if message.contains("responseBindings")));
359    }
360
361    #[test]
362    fn a_mismatched_protocol_id_is_an_error() {
363        static REGISTRY: ProtocolRegistry = ProtocolRegistry::new(&[ProtocolRegistration::metadata_routed::<
364            crate::schema::protocol::RestXml,
365        >("aws.protocols#restJson1")]);
366        let err = REGISTRY
367            .resolve_id(&REST_JSON_1_SERVICE, "aws.protocols#restJson1")
368            .expect_err("IDs disagree");
369        assert!(matches!(err, RouterBuildError::Configuration(_)));
370    }
371
372    #[test]
373    fn factory_failure_is_preserved() {
374        static REGISTRY: ProtocolRegistry = ProtocolRegistry::new(&[ProtocolRegistration {
375            protocol_id: "aws.protocols#restJson1",
376            build: |_| Err(RouterBuildError::Configuration("factory failed".into())),
377            order: &[],
378        }]);
379        let err = REGISTRY
380            .resolve_id(&REST_JSON_1_SERVICE, "aws.protocols#restJson1")
381            .unwrap_err();
382        assert!(matches!(err, RouterBuildError::Configuration(message) if message == "factory failed"));
383    }
384}