Skip to main content

aws_smithy_http_server/schema/
service.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Runtime descriptors for a service and its operations.
7//!
8//! [`Schema`] describes a single shape. Services and operations additionally
9//! relate shapes to one another: an operation has an input, an output, and a
10//! set of errors; a service has a version, a set of protocols, and a set of
11//! operations. [`OperationSchema`] and [`ServiceSchema`] carry those
12//! relationships as references to the shape schemas, so a generated
13//! `&'static ServiceSchema<'static>` is a complete, read-only description of
14//! the service the router serves.
15//!
16//! Operation-level traits are read from the shapes that carry them: codegen
17//! records an operation's `@http` binding on its input schema (see
18//! [`OperationSchema::http`]).
19//!
20//! Like [`Schema`], both descriptors are `const`-constructible and covariant
21//! in `'a`, so codegen-emitted `'static` descriptors coerce to any shorter
22//! lifetime at call sites.
23
24use aws_smithy_schema::traits::HttpTrait;
25use aws_smithy_schema::{Schema, ShapeId};
26
27/// Runtime descriptor for a Smithy operation shape.
28#[derive(Debug)]
29pub struct OperationSchema<'a> {
30    shape_id: ShapeId<'a>,
31    input: &'a Schema<'a>,
32    output: &'a Schema<'a>,
33    errors: &'a [&'a Schema<'a>],
34}
35
36impl<'a> OperationSchema<'a> {
37    /// Creates an operation descriptor.
38    pub const fn new(
39        shape_id: ShapeId<'a>,
40        input: &'a Schema<'a>,
41        output: &'a Schema<'a>,
42        errors: &'a [&'a Schema<'a>],
43    ) -> Self {
44        Self {
45            shape_id,
46            input,
47            output,
48            errors,
49        }
50    }
51
52    /// Returns the operation shape ID.
53    pub const fn shape_id(&self) -> &ShapeId<'a> {
54        &self.shape_id
55    }
56
57    /// Returns the operation's `@http` binding, if it has one.
58    ///
59    /// Codegen records the binding on the operation's input schema, so this
60    /// reads it from there.
61    pub fn http(&self) -> Option<&'a HttpTrait<'a>> {
62        self.input.http()
63    }
64
65    /// Returns the operation input shape schema.
66    pub fn input(&self) -> &'a Schema<'a> {
67        self.input
68    }
69
70    /// Returns the operation output shape schema.
71    pub fn output(&self) -> &'a Schema<'a> {
72        self.output
73    }
74
75    /// Returns the schemas of the errors modeled on this operation.
76    pub fn errors(&self) -> &'a [&'a Schema<'a>] {
77        self.errors
78    }
79}
80
81/// Runtime descriptor for a Smithy service shape.
82#[derive(Debug)]
83pub struct ServiceSchema<'a> {
84    shape_id: ShapeId<'a>,
85    version: Option<&'a str>,
86    protocols: &'a [ShapeId<'a>],
87    operations: &'a [&'a OperationSchema<'a>],
88    /// Traits applied to the service shape that runtime behavior can depend on, such as the
89    /// protocol traits with their members (see [`crate::schema::traits`]).
90    traits: Option<&'a std::sync::LazyLock<aws_smithy_schema::TraitMap>>,
91}
92
93impl<'a> ServiceSchema<'a> {
94    /// Creates a service descriptor.
95    ///
96    /// `protocols` lists the shape IDs of the protocols the service serves,
97    /// and `operations` lists every operation bound to the service, including
98    /// operations bound through resources.
99    pub const fn new(
100        shape_id: ShapeId<'a>,
101        version: Option<&'a str>,
102        protocols: &'a [ShapeId<'a>],
103        operations: &'a [&'a OperationSchema<'a>],
104    ) -> Self {
105        Self {
106            shape_id,
107            version,
108            protocols,
109            operations,
110            traits: None,
111        }
112    }
113
114    /// Attaches the traits applied to the service shape. Mirrors [`Schema::with_traits`].
115    pub const fn with_traits(mut self, traits: &'a std::sync::LazyLock<aws_smithy_schema::TraitMap>) -> Self {
116        self.traits = Some(traits);
117        self
118    }
119
120    /// Returns the traits recorded for the service shape, initializing them on first access.
121    pub fn traits(&self) -> Option<&aws_smithy_schema::TraitMap> {
122        self.traits.map(|traits| &**traits)
123    }
124
125    /// Returns the service shape ID.
126    pub const fn shape_id(&self) -> &ShapeId<'a> {
127        &self.shape_id
128    }
129
130    /// Returns the modeled service version, if any.
131    pub const fn version(&self) -> Option<&'a str> {
132        self.version
133    }
134
135    /// Returns the shape IDs of the protocols applied to this service.
136    pub fn protocols(&self) -> &'a [ShapeId<'a>] {
137        self.protocols
138    }
139
140    /// Returns the operations bound to this service.
141    pub fn operations(&self) -> &'a [&'a OperationSchema<'a>] {
142        self.operations
143    }
144
145    /// Returns the operation with the given shape ID, if bound to this service.
146    pub fn operation(&self, id: &ShapeId<'_>) -> Option<&'a OperationSchema<'a>> {
147        self.operations
148            .iter()
149            .copied()
150            .find(|operation| operation.shape_id().as_str() == id.as_str())
151    }
152}
153
154// Covariance in `'a` is what lets a codegen-emitted `&'static` descriptor be
155// passed where a `&ServiceSchema<'_>` is expected.
156#[allow(dead_code)]
157fn _assert_operation_schema_covariant<'a, 'b: 'a>(s: OperationSchema<'b>) -> OperationSchema<'a> {
158    s
159}
160
161#[allow(dead_code)]
162fn _assert_service_schema_covariant<'a, 'b: 'a>(s: ServiceSchema<'b>) -> ServiceSchema<'a> {
163    s
164}
165
166#[cfg(test)]
167mod test {
168    use super::*;
169    use aws_smithy_schema::{shape_id, ShapeType};
170
171    static UNIT: Schema<'static> = Schema::new(shape_id!("smithy.api", "Unit"), ShapeType::Structure);
172    static GET_INPUT: Schema<'static> = Schema::new(shape_id!("example", "GetInput"), ShapeType::Structure)
173        .with_http(HttpTrait::new("GET", "/get/{id}", None));
174    static NOT_FOUND: Schema<'static> = Schema::new(shape_id!("example", "NotFound"), ShapeType::Structure);
175    static GET_ERRORS: &[&Schema<'static>] = &[&NOT_FOUND];
176    static GET: OperationSchema<'static> =
177        OperationSchema::new(shape_id!("example", "Get"), &GET_INPUT, &UNIT, GET_ERRORS);
178    static PUT: OperationSchema<'static> = OperationSchema::new(shape_id!("example", "Put"), &UNIT, &UNIT, &[]);
179    static PROTOCOLS: &[ShapeId<'static>] = &[shape_id!("aws.protocols", "restJson1")];
180    static OPERATIONS: &[&OperationSchema<'static>] = &[&GET, &PUT];
181    static SERVICE: ServiceSchema<'static> = ServiceSchema::new(
182        shape_id!("example", "Service"),
183        Some("2024-01-01"),
184        PROTOCOLS,
185        OPERATIONS,
186    );
187
188    #[test]
189    fn operation_descriptor_exposes_shapes_and_http_binding() {
190        assert_eq!(GET.shape_id().as_str(), "example#Get");
191        assert!(std::ptr::eq(GET.input(), &GET_INPUT));
192        assert!(std::ptr::eq(GET.output(), &UNIT));
193        assert_eq!(GET.errors().len(), 1);
194        assert_eq!(GET.errors()[0].shape_id().as_str(), "example#NotFound");
195
196        let http = GET.http().expect("@http on the operation input");
197        assert_eq!(http.method(), "GET");
198        assert_eq!(http.uri(), "/get/{id}");
199        assert!(PUT.http().is_none());
200    }
201
202    #[test]
203    fn service_descriptor_exposes_version_protocols_and_operations() {
204        assert_eq!(SERVICE.shape_id().as_str(), "example#Service");
205        assert_eq!(SERVICE.version(), Some("2024-01-01"));
206        assert_eq!(SERVICE.protocols().len(), 1);
207        assert_eq!(SERVICE.protocols()[0].as_str(), "aws.protocols#restJson1");
208        assert_eq!(SERVICE.operations().len(), 2);
209    }
210
211    #[test]
212    fn service_descriptor_looks_up_operations_by_shape_id() {
213        let put = SERVICE
214            .operation(&shape_id!("example", "Put"))
215            .expect("bound operation");
216        assert!(std::ptr::eq(put, &PUT));
217        assert!(SERVICE.operation(&shape_id!("example", "Missing")).is_none());
218    }
219
220    #[test]
221    fn descriptors_can_be_built_with_a_non_static_lifetime() {
222        let operation = OperationSchema::new(ShapeId::from_parts("ns#Op", "ns", "Op"), &UNIT, &UNIT, &[]);
223        let operations = [&operation];
224        let service = ServiceSchema::new(ShapeId::from_parts("ns#Svc", "ns", "Svc"), None, &[], &operations);
225
226        assert_eq!(service.operations()[0].shape_id().as_str(), "ns#Op");
227        assert!(service.version().is_none());
228    }
229}