diff --git a/rust-runtime/aws-smithy-http-server/src/extension.rs b/rust-runtime/aws-smithy-http-server/src/extension.rs index bf06b96d99..d0b4c1cdd9 100644 --- a/rust-runtime/aws-smithy-http-server/src/extension.rs +++ b/rust-runtime/aws-smithy-http-server/src/extension.rs @@ -50,9 +50,14 @@ use std::ops::Deref; +use http::StatusCode; use thiserror::Error; -use crate::request::RequestParts; +use crate::{ + body::{empty, BoxBody}, + request::{FromParts, RequestParts}, + response::IntoResponse, +}; /// Extension type used to store information about Smithy operations in HTTP responses. /// This extension type is set when it has been correctly determined that the request should be @@ -165,6 +170,30 @@ impl Deref for Extension { } } +/// The extension has not been added to the [`Request`](http::Request) or has been previously removed. +#[derive(Debug, Error)] +#[error("the `Extension` is not present in the `http::Request`")] +pub struct MissingExtension; + +impl IntoResponse for MissingExtension { + fn into_response(self) -> http::Response { + let mut response = http::Response::new(empty()); + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + response + } +} + +impl FromParts for Extension +where + T: Clone + Send + Sync + 'static, +{ + type Rejection = MissingExtension; + + fn from_parts(parts: &mut http::request::Parts) -> Result { + parts.extensions.remove::().map(Extension).ok_or(MissingExtension) + } +} + /// Extract an [`Extension`] from a request. /// This is essentially the implementation of `FromRequest` for `Extension`, but with a /// protocol-agnostic rejection type. The actual code-generated implementation simply delegates to diff --git a/rust-runtime/aws-smithy-http-server/src/lib.rs b/rust-runtime/aws-smithy-http-server/src/lib.rs index 0b2cb95484..b032060bbc 100644 --- a/rust-runtime/aws-smithy-http-server/src/lib.rs +++ b/rust-runtime/aws-smithy-http-server/src/lib.rs @@ -16,6 +16,8 @@ pub mod extension; #[doc(hidden)] pub mod logging; #[doc(hidden)] +pub mod operation; +#[doc(hidden)] pub mod protocols; #[doc(hidden)] pub mod rejection; diff --git a/rust-runtime/aws-smithy-http-server/src/operation/handler.rs b/rust-runtime/aws-smithy-http-server/src/operation/handler.rs new file mode 100644 index 0000000000..4f0c8255e9 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/operation/handler.rs @@ -0,0 +1,153 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{ + convert::Infallible, + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; + +use futures_util::{ + future::{Map, MapErr}, + FutureExt, TryFutureExt, +}; +use tower::Service; + +use super::{OperationError, OperationShape}; + +/// A utility trait used to provide an even interface for all operation handlers. +pub trait Handler +where + Op: OperationShape, +{ + type Future: Future>; + + fn call(&mut self, input: Op::Input, exts: Exts) -> Self::Future; +} + +/// A utility trait used to provide an even interface over return types `Result`/`Ok`. +trait IntoResult { + fn into_result(self) -> Result; +} + +// We can convert from `Result` to `Result`. +impl IntoResult for Result { + fn into_result(self) -> Result { + self + } +} + +// We can convert from `T` to `Result`. +impl IntoResult for Ok { + fn into_result(self) -> Result { + Ok(self) + } +} + +// fn(Input) -> Output +impl Handler for F +where + Op: OperationShape, + F: Fn(Op::Input) -> Fut, + Fut: Future, + Fut::Output: IntoResult, +{ + type Future = Map Result>; + + fn call(&mut self, input: Op::Input, _exts: ()) -> Self::Future { + (self)(input).map(IntoResult::into_result) + } +} + +// fn(Input, Ext0) -> Output +impl Handler for F +where + Op: OperationShape, + F: Fn(Op::Input, Ext0) -> Fut, + Fut: Future, + Fut::Output: IntoResult, +{ + type Future = Map Result>; + + fn call(&mut self, input: Op::Input, exts: (Ext0,)) -> Self::Future { + (self)(input, exts.0).map(IntoResult::into_result) + } +} + +// fn(Input, Ext0, Ext1) -> Output +impl Handler for F +where + Op: OperationShape, + F: Fn(Op::Input, Ext0, Ext1) -> Fut, + Fut: Future, + Fut::Output: IntoResult, +{ + type Future = Map Result>; + + fn call(&mut self, input: Op::Input, exts: (Ext0, Ext1)) -> Self::Future { + (self)(input, exts.0, exts.1).map(IntoResult::into_result) + } +} + +/// An extension trait for [`Handler`]. +pub trait HandlerExt: Handler +where + Op: OperationShape, +{ + /// Convert the [`Handler`] into a [`Service`]. + fn into_service(self) -> IntoService + where + Self: Sized, + { + IntoService { + handler: self, + _operation: PhantomData, + } + } +} + +impl HandlerExt for H +where + Op: OperationShape, + H: Handler, +{ +} + +/// A [`Service`] provided for every [`Handler`]. +pub struct IntoService { + handler: H, + _operation: PhantomData, +} + +impl Clone for IntoService +where + H: Clone, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + _operation: PhantomData, + } + } +} + +impl Service<(Op::Input, Exts)> for IntoService +where + Op: OperationShape, + H: Handler, +{ + type Response = Op::Output; + type Error = OperationError; + type Future = MapErr Self::Error>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, (input, exts): (Op::Input, Exts)) -> Self::Future { + self.handler.call(input, exts).map_err(OperationError::Model) + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/operation/mod.rs b/rust-runtime/aws-smithy-http-server/src/operation/mod.rs new file mode 100644 index 0000000000..70199714c2 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/operation/mod.rs @@ -0,0 +1,266 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! # Operations. +//! +//! The shape of a [Smithy operation] is modelled by the [`OperationShape`] trait. Its associated types +//! [`OperationShape::Input`], [`OperationShape::Output`], and [`OperationShape::Error`] map to the structures +//! representing the Smithy inputs, outputs, and errors respectively. When an operation error is not specified +//! [`OperationShape::Error`] is [`Infallible`](std::convert::Infallible). +//! +//! We should generate a zero-sized type (ZST) for each Smithy operation and [`OperationShape`] should be implemented +//! on it. This will be used as a helper - providing static methods and parameterizing other traits. +//! +//! The model +//! +//! ```smithy +//! operation GetShopping { +//! input: CartIdentifier, +//! output: ShoppingCart, +//! errors: [...] +//! } +//! ``` +//! +//! is identified with the implementation +//! +//! ```rust,no_run +//! # use aws_smithy_http_server::operation::OperationShape; +//! # pub struct CartIdentifier; +//! # pub struct ShoppingCart; +//! # pub enum GetShoppingError {} +//! pub struct GetShopping; +//! +//! impl OperationShape for GetShopping { +//! const NAME: &'static str = "GetShopping"; +//! +//! type Input = CartIdentifier; +//! type Output = ShoppingCart; +//! type Error = GetShoppingError; +//! } +//! ``` +//! +//! The behavior of a Smithy operation is encoded by an [`Operation`]. The [`OperationShape`] ZSTs can be used to +//! construct specific operations using [`OperationShapeExt::from_handler`] and [`OperationShapeExt::from_service`]. +//! The [from_handler](OperationShapeExt::from_handler) constructor takes a [`Handler`] whereas the +//! [from_service](OperationShapeExt::from_service) takes a [`OperationService`]. Both traits serve a similar purpose - +//! they provide a common interface over a class of structures. +//! +//! ## [`Handler`] +//! +//! The [`Handler`] trait is implemented by all closures which accept [`OperationShape::Input`] as their first +//! argument, the remaining arguments implement [`FromParts`](crate::request::FromParts), and return either +//! [`OperationShape::Output`] when [`OperationShape::Error`] is [`Infallible`](std::convert::Infallible) or +//! [`Result`]<[`OperationShape::Output`],[`OperationShape::Error`]>. The following are examples of closures which +//! implement [`Handler`]: +//! +//! ```rust,no_run +//! # use aws_smithy_http_server::Extension; +//! # pub struct CartIdentifier; +//! # pub struct ShoppingCart; +//! # pub enum GetShoppingError {} +//! # pub struct Context; +//! # pub struct ExtraContext; +//! // Simple handler where no error is modelled. +//! async fn handler_a(input: CartIdentifier) -> ShoppingCart { +//! todo!() +//! } +//! +//! // Handler with an extension where no error is modelled. +//! async fn handler_b(input: CartIdentifier, ext: Extension) -> ShoppingCart { +//! todo!() +//! } +//! +//! // More than one extension can be provided. +//! async fn handler_c(input: CartIdentifier, ext_1: Extension, ext_2: Extension) -> ShoppingCart { +//! todo!() +//! } +//! +//! // When an error is modelled we must return a `Result`. +//! async fn handler_d(input: CartIdentifier, ext: Extension) -> Result { +//! todo!() +//! } +//! ``` +//! +//! ## [`OperationService`] +//! +//! Similarly, the [`OperationService`] trait is implemented by all `Service<(Op::Input, ...)>` with +//! `Response = Op::Output`, and `Error = OperationError`. +//! +//! We use [`OperationError`], with a `PollError` not constrained by the model, to allow the user to provide a custom +//! [`Service::poll_ready`](tower::Service::poll_ready) implementation. +//! +//! The following are examples of [`Service`](tower::Service)s which implement [`OperationService`]: +//! +//! - `Service>`. +//! - `Service<(CartIdentifier, Extension), Response = ShoppingCart, Error = OperationError>`. +//! - `Service<(CartIdentifier, Extension, Extension), Response = ShoppingCart, Error = OperationError)`. +//! +//! Notice the parallels between [`OperationService`] and [`Handler`]. +//! +//! ## Constructing an [`Operation`] +//! +//! The following is an example of using both construction approaches: +//! +//! ```rust,no_run +//! # use std::task::{Poll, Context}; +//! # use aws_smithy_http_server::operation::*; +//! # use tower::Service; +//! # pub struct CartIdentifier; +//! # pub struct ShoppingCart; +//! # pub enum GetShoppingError {} +//! # pub struct GetShopping; +//! # impl OperationShape for GetShopping { +//! # const NAME: &'static str = "GetShopping"; +//! # +//! # type Input = CartIdentifier; +//! # type Output = ShoppingCart; +//! # type Error = GetShoppingError; +//! # } +//! # type OpFuture = std::future::Ready>>; +//! // Construction of an `Operation` from a `Handler`. +//! +//! async fn op_handler(input: CartIdentifier) -> Result { +//! todo!() +//! } +//! +//! let operation = GetShopping::from_handler(op_handler); +//! +//! // Construction of an `Operation` from a `Service`. +//! +//! pub struct PollError; +//! +//! pub struct OpService; +//! +//! impl Service for OpService { +//! type Response = ShoppingCart; +//! type Error = OperationError; +//! type Future = OpFuture; +//! +//! fn poll_ready(&mut self, cx: &mut Context) -> Poll> { +//! // NOTE: This MUST NOT return `Err(OperationError::Model(_))`. +//! todo!() +//! } +//! +//! fn call(&mut self, request: CartIdentifier) -> Self::Future { +//! // NOTE: This MUST NOT return `Err(OperationError::Poll(_))`. +//! todo!() +//! } +//! } +//! +//! let operation = GetShopping::from_service(OpService); +//! +//! ``` +//! +//! ## Upgrading Smithy services to HTTP services +//! +//! Both [`Handler`] and [`OperationService`] accept and return Smithy model structures. After an [`Operation`] is +//! constructed they are converted to a canonical form +//! `Service<(Op::Input, Exts), Response = Op::Output, Error = OperationError>`. The +//! [`UpgradeLayer`] acts upon such services by converting them to +//! `Service`. +//! +//! Note that the `PollError` is still exposed, for two reasons: +//! +//! - Smithy is agnostic to `PollError` and therefore we have no prescribed way to serialize it to a [`http::Response`] +//! , unlike the operation errors. +//! - The intention of `PollError` is to signal that the underlying service is no longer able to take requests, so +//! should be discarded. See [`Service::poll_ready`](tower::Service::poll_ready). +//! +//! The [`UpgradeLayer`] and it's [`Layer::Service`] [`Upgrade`] are both parameterized by a protocol. This allows +//! for upgrading to `Service` to be protocol dependent. +//! +//! The [`Operation::upgrade`] will apply [`UpgradeLayer`] to `S` then apply the [`Layer`] `L`. The service builder +//! provided to the user will perform this composition on `build`. +//! +//! [Smithy operation]: https://awslabs.github.io/smithy/2.0/spec/service-types.html#operation + +mod handler; +mod operation_service; +mod shape; +mod upgrade; + +use tower::{ + layer::util::{Identity, Stack}, + Layer, +}; + +pub use handler::*; +pub use operation_service::*; +pub use shape::*; +pub use upgrade::*; + +/// A Smithy operation, represented by a [`Service`](tower::Service) `S` and a [`Layer`] `L`. +/// +/// The `L` is held and applied lazily during [`Operation::upgrade`]. +pub struct Operation { + inner: S, + layer: L, +} + +type StackedUpgradeService = , L> as Layer>::Service; + +impl Operation { + /// Applies a [`Layer`] to the operation _after_ it has been upgraded via [`Operation::upgrade`]. + pub fn layer(self, layer: NewL) -> Operation> { + Operation { + inner: self.inner, + layer: Stack::new(self.layer, layer), + } + } + + /// Takes the [`Operation`], containing the inner [`Service`](tower::Service) `S`, the HTTP [`Layer`] `L` and + /// composes them together using [`UpgradeLayer`] for a specific protocol and [`OperationShape`]. + /// + /// The composition is made explicit in the method constraints and return type. + pub fn upgrade(self) -> StackedUpgradeService + where + UpgradeLayer: Layer, + L: Layer< as Layer>::Service>, + { + let Self { inner, layer } = self; + let layer = Stack::new(UpgradeLayer::new(), layer); + layer.layer(inner) + } +} + +impl Operation> { + /// Creates an [`Operation`] from a [`Service`](tower::Service). + pub fn from_service(inner: S) -> Self + where + Op: OperationShape, + S: OperationService, + { + Self { + inner: inner.canonicalize(), + layer: Identity::new(), + } + } +} + +impl Operation> { + /// Creates an [`Operation`] from a [`Handler`]. + pub fn from_handler(handler: H) -> Self + where + Op: OperationShape, + H: Handler, + { + Self { + inner: handler.into_service(), + layer: Identity::new(), + } + } +} + +/// A marker struct indicating an [`Operation`] has not been set in a builder. +pub struct OperationNotSet; + +/// The operation [`Service`](tower::Service) has two classes of failure modes - those specified by the Smithy model +/// and those associated with [`Service::poll_ready`](tower::Service::poll_ready). +pub enum OperationError { + /// An error modelled by the Smithy model occurred. + Model(ModelError), + /// A [`Service::poll_ready`](tower::Service::poll_ready) failure occurred. + PollReady(PollError), +} diff --git a/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs b/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs new file mode 100644 index 0000000000..150cd2c65a --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs @@ -0,0 +1,133 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{ + marker::PhantomData, + task::{Context, Poll}, +}; + +use tower::Service; + +use super::{OperationError, OperationShape}; + +/// A utility trait used to provide an even interface for all operation services. +/// +/// This serves to take [`Service`]s of the form `Service<(Op::Input, Ext0, Ext1, ...)>` to the canonical +/// representation of `Service<(Input, (Ext0, Ext1, ...))>` inline with +/// [`IntoService`](super::IntoService). +pub trait OperationService: + Service> +where + Op: OperationShape, +{ + type Normalized; + + // Normalize the request type. + fn normalize(input: Op::Input, exts: Exts) -> Self::Normalized; +} + +// `Service` +impl OperationService for S +where + Op: OperationShape, + S: Service>, +{ + type Normalized = Op::Input; + + fn normalize(input: Op::Input, _exts: ()) -> Self::Normalized { + input + } +} + +// `Service<(Op::Input, Ext0)>` +impl OperationService for S +where + Op: OperationShape, + S: Service<(Op::Input, Ext0), Response = Op::Output, Error = OperationError>, +{ + type Normalized = (Op::Input, Ext0); + + fn normalize(input: Op::Input, exts: (Ext0,)) -> Self::Normalized { + (input, exts.0) + } +} + +// `Service<(Op::Input, Ext0, Ext1)>` +impl OperationService for S +where + Op: OperationShape, + S: Service<(Op::Input, Ext0, Ext1), Response = Op::Output, Error = OperationError>, +{ + type Normalized = (Op::Input, Ext0, Ext1); + + fn normalize(input: Op::Input, exts: (Ext0, Ext1)) -> Self::Normalized { + (input, exts.0, exts.1) + } +} + +/// An extension trait of [`OperationService`]. +pub trait OperationServiceExt: OperationService +where + Op: OperationShape, +{ + /// Convert the [`OperationService`] into a canonicalized [`Service`]. + fn canonicalize(self) -> Normalize + where + Self: Sized, + { + Normalize { + inner: self, + _operation: PhantomData, + _poll_error: PhantomData, + } + } +} + +impl OperationServiceExt for F +where + Op: OperationShape, + F: OperationService, +{ +} + +/// A [`Service`] normalizing the request type of a [`OperationService`]. +#[derive(Debug)] +pub struct Normalize { + inner: S, + _operation: PhantomData, + _poll_error: PhantomData, +} + +impl Clone for Normalize +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _operation: PhantomData, + _poll_error: PhantomData, + } + } +} + +impl Service<(Op::Input, Exts)> for Normalize +where + Op: OperationShape, + S: OperationService, +{ + type Response = S::Response; + type Error = S::Error; + type Future = >::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, (input, exts): (Op::Input, Exts)) -> Self::Future { + let req = S::normalize(input, exts); + self.inner.call(req) + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/operation/shape.rs b/rust-runtime/aws-smithy-http-server/src/operation/shape.rs new file mode 100644 index 0000000000..9990326279 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/operation/shape.rs @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use super::{Handler, IntoService, Normalize, Operation, OperationService}; + +/// Models the [Smithy Operation shape]. +/// +/// [Smithy Operation shape]: https://awslabs.github.io/smithy/1.0/spec/core/model.html#operation +pub trait OperationShape { + /// The name of the operation. + const NAME: &'static str; + + /// The operation input. + type Input; + /// The operation output. + type Output; + /// The operation error. [`Infallible`](std::convert::Infallible) in the case where no error + /// exists. + type Error; +} + +/// An extension trait over [`OperationShape`]. +pub trait OperationShapeExt: OperationShape { + /// Creates a new [`Operation`] for well-formed [`Handler`]s. + fn from_handler(handler: H) -> Operation> + where + H: Handler, + Self: Sized, + { + Operation::from_handler(handler) + } + + /// Creates a new [`Operation`] for well-formed [`Service`](tower::Service)s. + fn from_service(svc: S) -> Operation> + where + S: OperationService, + Self: Sized, + { + Operation::from_service(svc) + } +} + +impl OperationShapeExt for S where S: OperationShape {} diff --git a/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs b/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs new file mode 100644 index 0000000000..17dd7b29ae --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs @@ -0,0 +1,213 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{ + future::Future, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_util::ready; +use pin_project_lite::pin_project; +use tower::{Layer, Service}; + +use crate::{ + request::{FromParts, FromRequest}, + response::IntoResponse, +}; + +use super::{OperationError, OperationShape}; + +/// A [`Layer`] responsible for taking an operation [`Service`], accepting and returning Smithy +/// types and converting it into a [`Service`] taking and returning [`http`] types. +/// +/// See [`Upgrade`]. +#[derive(Debug, Clone)] +pub struct UpgradeLayer { + _protocol: PhantomData, + _operation: PhantomData, + _exts: PhantomData, + _body: PhantomData, +} + +impl Default for UpgradeLayer { + fn default() -> Self { + Self { + _protocol: PhantomData, + _operation: PhantomData, + _exts: PhantomData, + _body: PhantomData, + } + } +} + +impl UpgradeLayer { + /// Creates a new [`UpgradeLayer`]. + pub fn new() -> Self { + Self::default() + } +} + +impl Layer for UpgradeLayer { + type Service = Upgrade; + + fn layer(&self, inner: S) -> Self::Service { + Upgrade { + _protocol: PhantomData, + _operation: PhantomData, + _body: PhantomData, + _exts: PhantomData, + inner, + } + } +} + +/// An alias allowing for quick access to [`UpgradeLayer`]s target [`Service`]. +pub type UpgradedService = as Layer>::Service; + +/// A [`Service`] responsible for wrapping an operation [`Service`] accepting and returning Smithy +/// types, and converting it into a [`Service`] accepting and returning [`http`] types. +pub struct Upgrade { + _protocol: PhantomData, + _operation: PhantomData, + _exts: PhantomData, + _body: PhantomData, + inner: S, +} + +impl Clone for Upgrade +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + _protocol: PhantomData, + _operation: PhantomData, + _body: PhantomData, + _exts: PhantomData, + inner: self.inner.clone(), + } + } +} + +pin_project! { + #[project = InnerProj] + #[project_replace = InnerProjReplace] + enum Inner { + FromRequest { + #[pin] + inner: FromFut + }, + Inner { + #[pin] + call: HandlerFut + } + } +} + +type InnerAlias = Inner<<(Input, Exts) as FromRequest>::Future, Fut>; + +pin_project! { + /// The [`Service::Future`] of [`Upgrade`]. + pub struct UpgradeFuture + where + Operation: OperationShape, + (Operation::Input, Exts): FromRequest, + S: Service<(Operation::Input, Exts)>, + { + service: S, + #[pin] + inner: InnerAlias + } +} + +impl Future for UpgradeFuture +where + // `Op` is used to specify the operation shape + Op: OperationShape, + // Smithy input must convert from a HTTP request + Op::Input: FromRequest, + // Smithy output must convert into a HTTP response + Op::Output: IntoResponse

, + // Smithy error must convert into a HTTP response + OpError: IntoResponse

, + + // Must be able to convert extensions + Exts: FromParts

, + + // The signature of the inner service is correct + S: Service<(Op::Input, Exts), Response = Op::Output, Error = OperationError>, +{ + type Output = Result, PollError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + let mut this = self.as_mut().project(); + let this2 = this.inner.as_mut().project(); + + let call = match this2 { + InnerProj::FromRequest { inner } => { + let result = ready!(inner.poll(cx)); + match result { + Ok(ok) => this.service.call(ok), + Err(err) => return Poll::Ready(Ok(err.into_response())), + } + } + InnerProj::Inner { call } => { + let result = ready!(call.poll(cx)); + let output = match result { + Ok(ok) => ok.into_response(), + Err(OperationError::Model(err)) => err.into_response(), + Err(OperationError::PollReady(_)) => { + unreachable!("poll error should not be raised") + } + }; + return Poll::Ready(Ok(output)); + } + }; + + this.inner.as_mut().project_replace(Inner::Inner { call }); + } + } +} + +impl Service> for Upgrade +where + // `Op` is used to specify the operation shape + Op: OperationShape, + // Smithy input must convert from a HTTP request + Op::Input: FromRequest, + // Smithy output must convert into a HTTP response + Op::Output: IntoResponse

, + // Smithy error must convert into a HTTP response + OpError: IntoResponse

, + + // Must be able to convert extensions + Exts: FromParts

, + + // The signature of the inner service is correct + S: Service<(Op::Input, Exts), Response = Op::Output, Error = OperationError> + Clone, +{ + type Response = http::Response; + type Error = PollError; + type Future = UpgradeFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(|err| match err { + OperationError::PollReady(err) => err, + OperationError::Model(_) => unreachable!("operation error should not be raised"), + }) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + UpgradeFuture { + service: self.inner.clone(), + inner: Inner::FromRequest { + inner: <(Op::Input, Exts) as FromRequest>::from_request(req), + }, + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/rejection.rs b/rust-runtime/aws-smithy-http-server/src/rejection.rs index f5accce847..a382902156 100644 --- a/rust-runtime/aws-smithy-http-server/src/rejection.rs +++ b/rust-runtime/aws-smithy-http-server/src/rejection.rs @@ -43,6 +43,8 @@ use strum_macros::Display; +use crate::response::IntoResponse; + /// Rejection used for when failing to extract an [`crate::Extension`] from an incoming [request's /// extensions]. Contains one variant for each way the extractor can fail. /// @@ -265,3 +267,22 @@ convert_to_request_rejection!(hyper::Error, HttpBody); // Required in order to accept Lambda HTTP requests using `Router`. convert_to_request_rejection!(lambda_http::Error, HttpBody); + +/// A sum type rejection, implementing [`IntoResponse`] when both variants do. +pub enum EitherRejection { + Left(Left), + Right(Right), +} + +impl IntoResponse

for EitherRejection +where + L: IntoResponse

, + R: IntoResponse

, +{ + fn into_response(self) -> http::Response { + match self { + EitherRejection::Left(left) => left.into_response(), + EitherRejection::Right(right) => right.into_response(), + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/request.rs b/rust-runtime/aws-smithy-http-server/src/request.rs index 6b28d255b6..2775a68cf5 100644 --- a/rust-runtime/aws-smithy-http-server/src/request.rs +++ b/rust-runtime/aws-smithy-http-server/src/request.rs @@ -32,7 +32,15 @@ * DEALINGS IN THE SOFTWARE. */ -use http::{Extensions, HeaderMap, Request, Uri}; +use std::future::{ready, Future, Ready}; + +use futures_util::{ + future::{try_join, MapErr, MapOk, TryJoin}, + TryFutureExt, +}; +use http::{request::Parts, Extensions, HeaderMap, Request, Uri}; + +use crate::{rejection::EitherRejection, response::IntoResponse}; #[doc(hidden)] #[derive(Debug)] @@ -54,7 +62,7 @@ impl RequestParts { #[doc(hidden)] pub fn new(req: Request) -> Self { let ( - http::request::Parts { + Parts { uri, headers, extensions, @@ -99,3 +107,66 @@ impl RequestParts { self.extensions.as_ref() } } + +/// Provides a protocol aware extraction from a [`Request`]. This borrows the +/// [`Parts`], in contrast to [`FromRequest`]. +pub trait FromParts: Sized { + type Rejection: IntoResponse; + + /// Extracts `self` from a [`Parts`] synchronously. + fn from_parts(parts: &mut Parts) -> Result; +} + +impl FromParts

for (T1, T2) +where + T1: FromParts

, + T2: FromParts

, +{ + type Rejection = EitherRejection; + + fn from_parts(parts: &mut Parts) -> Result { + let t1 = T1::from_parts(parts).map_err(EitherRejection::Left)?; + let t2 = T2::from_parts(parts).map_err(EitherRejection::Right)?; + Ok((t1, t2)) + } +} + +/// Provides a protocol aware extraction from a [`Request`]. This consumes the +/// [`Request`], in contrast to [`FromParts`]. +pub trait FromRequest: Sized { + type Rejection: IntoResponse; + type Future: Future>; + + /// Extracts `self` from a [`Request`] asynchronously. + fn from_request(request: Request) -> Self::Future; +} + +impl FromRequest for (T1,) +where + T1: FromRequest, +{ + type Rejection = T1::Rejection; + type Future = MapOk (T1,)>; + + fn from_request(request: Request) -> Self::Future { + T1::from_request(request).map_ok(|t1| (t1,)) + } +} + +impl FromRequest for (T1, T2) +where + T1: FromRequest, + T2: FromParts

, +{ + type Rejection = EitherRejection; + type Future = TryJoin Self::Rejection>, Ready>>; + + fn from_request(request: Request) -> Self::Future { + let (mut parts, body) = request.into_parts(); + let t2_result = T2::from_parts(&mut parts).map_err(EitherRejection::Right); + try_join( + T1::from_request(Request::from_parts(parts, body)).map_err(EitherRejection::Left), + ready(t2_result), + ) + } +}