aws_smithy_http_server/routing/
mod.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP routing that adheres to the [Smithy specification].
7//!
8//! [Smithy specification]: https://smithy.io/2.0/spec/http-bindings.html
9
10mod into_make_service;
11mod into_make_service_with_connect_info;
12#[cfg(feature = "aws-lambda")]
13#[cfg_attr(docsrs, doc(cfg(feature = "aws-lambda")))]
14mod lambda_handler;
15
16#[doc(hidden)]
17pub mod request_spec;
18
19mod route;
20
21pub(crate) mod tiny_map;
22
23use std::{
24    error::Error,
25    fmt,
26    future::{ready, Future, Ready},
27    marker::PhantomData,
28    pin::Pin,
29    task::{Context, Poll},
30};
31
32use bytes::Bytes;
33use futures_util::{
34    future::{Either, MapOk},
35    TryFutureExt,
36};
37use http::Response;
38use http_body::Body as HttpBody;
39use tower::{util::Oneshot, Service, ServiceExt};
40
41use crate::{
42    body::{boxed, BoxBody},
43    error::BoxError,
44    response::IntoResponse,
45};
46
47#[cfg(feature = "aws-lambda")]
48#[cfg_attr(docsrs, doc(cfg(feature = "aws-lambda")))]
49pub use self::lambda_handler::LambdaHandler;
50
51#[allow(deprecated)]
52pub use self::{
53    into_make_service::IntoMakeService,
54    into_make_service_with_connect_info::{Connected, IntoMakeServiceWithConnectInfo},
55    route::Route,
56};
57
58pub(crate) const UNKNOWN_OPERATION_EXCEPTION: &str = "UnknownOperationException";
59
60/// Constructs common response to method disallowed.
61pub(crate) fn method_disallowed() -> http::Response<BoxBody> {
62    let mut responses = http::Response::default();
63    *responses.status_mut() = http::StatusCode::METHOD_NOT_ALLOWED;
64    responses
65}
66
67/// An interface for retrieving an inner [`Service`] given a [`http::Request`].
68pub trait Router<B> {
69    type Service;
70    type Error;
71
72    /// Matches a [`http::Request`] to a target [`Service`].
73    fn match_route(&self, request: &http::Request<B>) -> Result<Self::Service, Self::Error>;
74}
75
76/// A [`Service`] using the [`Router`] `R` to redirect messages to specific routes.
77///
78/// The `Protocol` parameter is used to determine the serialization of errors.
79pub struct RoutingService<R, Protocol> {
80    router: R,
81    _protocol: PhantomData<Protocol>,
82}
83
84impl<R, P> fmt::Debug for RoutingService<R, P>
85where
86    R: fmt::Debug,
87{
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("RoutingService")
90            .field("router", &self.router)
91            .field("_protocol", &self._protocol)
92            .finish()
93    }
94}
95
96impl<R, P> Clone for RoutingService<R, P>
97where
98    R: Clone,
99{
100    fn clone(&self) -> Self {
101        Self {
102            router: self.router.clone(),
103            _protocol: PhantomData,
104        }
105    }
106}
107
108impl<R, P> RoutingService<R, P> {
109    /// Creates a [`RoutingService`] from a [`Router`].
110    pub fn new(router: R) -> Self {
111        Self {
112            router,
113            _protocol: PhantomData,
114        }
115    }
116
117    /// Maps a [`Router`] using a closure.
118    pub fn map<RNew, F>(self, f: F) -> RoutingService<RNew, P>
119    where
120        F: FnOnce(R) -> RNew,
121    {
122        RoutingService {
123            router: f(self.router),
124            _protocol: PhantomData,
125        }
126    }
127}
128
129type EitherOneshotReady<S, B> = Either<
130    MapOk<Oneshot<S, http::Request<B>>, fn(<S as Service<http::Request<B>>>::Response) -> http::Response<BoxBody>>,
131    Ready<Result<http::Response<BoxBody>, <S as Service<http::Request<B>>>::Error>>,
132>;
133
134pin_project_lite::pin_project! {
135    pub struct RoutingFuture<S, B> where S: Service<http::Request<B>> {
136        #[pin]
137        inner: EitherOneshotReady<S, B>
138    }
139}
140
141impl<S, B> RoutingFuture<S, B>
142where
143    S: Service<http::Request<B>>,
144{
145    /// Creates a [`RoutingFuture`] from [`ServiceExt::oneshot`].
146    pub(super) fn from_oneshot<RespB>(future: Oneshot<S, http::Request<B>>) -> Self
147    where
148        S: Service<http::Request<B>, Response = http::Response<RespB>>,
149        RespB: HttpBody<Data = Bytes> + Send + 'static,
150        RespB::Error: Into<BoxError>,
151    {
152        Self {
153            inner: Either::Left(future.map_ok(|x| x.map(boxed))),
154        }
155    }
156
157    /// Creates a [`RoutingFuture`] from [`Service::Response`].
158    pub(super) fn from_response(response: http::Response<BoxBody>) -> Self {
159        Self {
160            inner: Either::Right(ready(Ok(response))),
161        }
162    }
163}
164
165impl<S, B> Future for RoutingFuture<S, B>
166where
167    S: Service<http::Request<B>>,
168{
169    type Output = Result<http::Response<BoxBody>, S::Error>;
170
171    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
172        self.project().inner.poll(cx)
173    }
174}
175
176impl<R, P, B, RespB> Service<http::Request<B>> for RoutingService<R, P>
177where
178    R: Router<B>,
179    R::Service: Service<http::Request<B>, Response = http::Response<RespB>> + Clone,
180    R::Error: IntoResponse<P> + Error,
181    RespB: HttpBody<Data = Bytes> + Send + 'static,
182    RespB::Error: Into<BoxError>,
183{
184    type Response = Response<BoxBody>;
185    type Error = <R::Service as Service<http::Request<B>>>::Error;
186    type Future = RoutingFuture<R::Service, B>;
187
188    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
189        Poll::Ready(Ok(()))
190    }
191
192    fn call(&mut self, req: http::Request<B>) -> Self::Future {
193        tracing::debug!("inside routing service call");
194        match self.router.match_route(&req) {
195            // Successfully routed, use the routes `Service::call`.
196            Ok(ok) => RoutingFuture::from_oneshot(ok.oneshot(req)),
197            // Failed to route, use the `R::Error`s `IntoResponse<P>`.
198            Err(error) => {
199                tracing::debug!(%error, "failed to route");
200                RoutingFuture::from_response(error.into_response())
201            }
202        }
203    }
204}