Unverified Commit 66f96a54 authored by Harry Barber's avatar Harry Barber Committed by GitHub
Browse files

Add new service builder machinery (#1679)

* Add protocol specific `FromRequest` and `FromParts`.

* Add `OperationShape` trait to model Smithy operations.

* Add `Handler` and `OperationService` traits.

* Add `Upgrade` `Service` and `UpgradeLayer` `Layer`.
parent 2cb76641
Loading
Loading
Loading
Loading
+30 −1
Original line number Diff line number Diff line
@@ -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<T> Deref for Extension<T> {
    }
}

/// 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<Protocol> IntoResponse<Protocol> for MissingExtension {
    fn into_response(self) -> http::Response<BoxBody> {
        let mut response = http::Response::new(empty());
        *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
        response
    }
}

impl<Protocol, T> FromParts<Protocol> for Extension<T>
where
    T: Clone + Send + Sync + 'static,
{
    type Rejection = MissingExtension;

    fn from_parts(parts: &mut http::request::Parts) -> Result<Self, Self::Rejection> {
        parts.extensions.remove::<T>().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
+2 −0
Original line number Diff line number Diff line
@@ -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;
+153 −0
Original line number Diff line number Diff line
/*
 * 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<Op, Exts>
where
    Op: OperationShape,
{
    type Future: Future<Output = Result<Op::Output, Op::Error>>;

    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, Error>`/`Ok`.
trait IntoResult<Ok, Error> {
    fn into_result(self) -> Result<Ok, Error>;
}

// We can convert from `Result<Ok, Error>` to `Result<Ok, Error>`.
impl<Ok, Error> IntoResult<Ok, Error> for Result<Ok, Error> {
    fn into_result(self) -> Result<Ok, Error> {
        self
    }
}

// We can convert from `T` to `Result<T, Infallible>`.
impl<Ok> IntoResult<Ok, Infallible> for Ok {
    fn into_result(self) -> Result<Ok, Infallible> {
        Ok(self)
    }
}

// fn(Input) -> Output
impl<Op, F, Fut> Handler<Op, ()> for F
where
    Op: OperationShape,
    F: Fn(Op::Input) -> Fut,
    Fut: Future,
    Fut::Output: IntoResult<Op::Output, Op::Error>,
{
    type Future = Map<Fut, fn(Fut::Output) -> Result<Op::Output, Op::Error>>;

    fn call(&mut self, input: Op::Input, _exts: ()) -> Self::Future {
        (self)(input).map(IntoResult::into_result)
    }
}

// fn(Input, Ext0) -> Output
impl<Op, F, Fut, Ext0> Handler<Op, (Ext0,)> for F
where
    Op: OperationShape,
    F: Fn(Op::Input, Ext0) -> Fut,
    Fut: Future,
    Fut::Output: IntoResult<Op::Output, Op::Error>,
{
    type Future = Map<Fut, fn(Fut::Output) -> Result<Op::Output, Op::Error>>;

    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<Op, F, Fut, Ext0, Ext1> Handler<Op, (Ext0, Ext1)> for F
where
    Op: OperationShape,
    F: Fn(Op::Input, Ext0, Ext1) -> Fut,
    Fut: Future,
    Fut::Output: IntoResult<Op::Output, Op::Error>,
{
    type Future = Map<Fut, fn(Fut::Output) -> Result<Op::Output, Op::Error>>;

    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<Op, Exts>: Handler<Op, Exts>
where
    Op: OperationShape,
{
    /// Convert the [`Handler`] into a [`Service`].
    fn into_service(self) -> IntoService<Op, Self>
    where
        Self: Sized,
    {
        IntoService {
            handler: self,
            _operation: PhantomData,
        }
    }
}

impl<Op, Exts, H> HandlerExt<Op, Exts> for H
where
    Op: OperationShape,
    H: Handler<Op, Exts>,
{
}

/// A [`Service`] provided for every [`Handler`].
pub struct IntoService<Op, H> {
    handler: H,
    _operation: PhantomData<Op>,
}

impl<Op, H> Clone for IntoService<Op, H>
where
    H: Clone,
{
    fn clone(&self) -> Self {
        Self {
            handler: self.handler.clone(),
            _operation: PhantomData,
        }
    }
}

impl<Op, Exts, H> Service<(Op::Input, Exts)> for IntoService<Op, H>
where
    Op: OperationShape,
    H: Handler<Op, Exts>,
{
    type Response = Op::Output;
    type Error = OperationError<Op::Error, Infallible>;
    type Future = MapErr<H::Future, fn(Op::Error) -> Self::Error>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, (input, exts): (Op::Input, Exts)) -> Self::Future {
        self.handler.call(input, exts).map_err(OperationError::Model)
    }
}
+266 −0
Original line number Diff line number Diff line
/*
 * 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<Context>) -> ShoppingCart {
//!     todo!()
//! }
//!
//! // More than one extension can be provided.
//! async fn handler_c(input: CartIdentifier, ext_1: Extension<Context>, ext_2: Extension<ExtraContext>) -> ShoppingCart {
//!     todo!()
//! }
//!
//! // When an error is modelled we must return a `Result`.
//! async fn handler_d(input: CartIdentifier, ext: Extension<Context>) -> Result<ShoppingCart, GetShoppingError> {
//!     todo!()
//! }
//! ```
//!
//! ## [`OperationService`]
//!
//! Similarly, the [`OperationService`] trait is implemented by all `Service<(Op::Input, ...)>` with
//! `Response = Op::Output`, and `Error = OperationError<Op::Error, PollError>`.
//!
//! 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<CartIdentifier, Response = ShoppingCart, Error = OperationError<Infallible, Infallible>>`.
//! - `Service<(CartIdentifier, Extension<Context>), Response = ShoppingCart, Error = OperationError<GetShoppingCartError, Infallible>>`.
//! - `Service<(CartIdentifier, Extension<Context>, Extension<ExtraContext>), Response = ShoppingCart, Error = OperationError<GetShoppingCartError, PollError>)`.
//!
//! 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<Result<ShoppingCart, OperationError<GetShoppingError, PollError>>>;
//! // Construction of an `Operation` from a `Handler`.
//!
//! async fn op_handler(input: CartIdentifier) -> Result<ShoppingCart, GetShoppingError> {
//!     todo!()
//! }
//!
//! let operation = GetShopping::from_handler(op_handler);
//!
//! // Construction of an `Operation` from a `Service`.
//!
//! pub struct PollError;
//!
//! pub struct OpService;
//!
//! impl Service<CartIdentifier> for OpService {
//!     type Response = ShoppingCart;
//!     type Error = OperationError<GetShoppingError, PollError>;
//!     type Future = OpFuture;
//!
//!     fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
//!         // 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<Op::Error, PollError>>`. The
//! [`UpgradeLayer`] acts upon such services by converting them to
//! `Service<http::Request, Response = http::Response, Error = PollError>`.
//!
//! 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<http::Request, Response = http::Response, Error = PollError>` 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<S, L = Identity> {
    inner: S,
    layer: L,
}

type StackedUpgradeService<P, Op, E, B, L, S> = <Stack<UpgradeLayer<P, Op, E, B>, L> as Layer<S>>::Service;

impl<S, L> Operation<S, L> {
    /// Applies a [`Layer`] to the operation _after_ it has been upgraded via [`Operation::upgrade`].
    pub fn layer<NewL>(self, layer: NewL) -> Operation<S, Stack<L, NewL>> {
        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<P, Op, E, B>(self) -> StackedUpgradeService<P, Op, E, B, L, S>
    where
        UpgradeLayer<P, Op, E, B>: Layer<S>,
        L: Layer<<UpgradeLayer<P, Op, E, B> as Layer<S>>::Service>,
    {
        let Self { inner, layer } = self;
        let layer = Stack::new(UpgradeLayer::new(), layer);
        layer.layer(inner)
    }
}

impl<Op, S, PollError> Operation<Normalize<Op, S, PollError>> {
    /// Creates an [`Operation`] from a [`Service`](tower::Service).
    pub fn from_service<Exts>(inner: S) -> Self
    where
        Op: OperationShape,
        S: OperationService<Op, Exts, PollError>,
    {
        Self {
            inner: inner.canonicalize(),
            layer: Identity::new(),
        }
    }
}

impl<Op, H> Operation<IntoService<Op, H>> {
    /// Creates an [`Operation`] from a [`Handler`].
    pub fn from_handler<Exts>(handler: H) -> Self
    where
        Op: OperationShape,
        H: Handler<Op, Exts>,
    {
        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<ModelError, PollError> {
    /// An error modelled by the Smithy model occurred.
    Model(ModelError),
    /// A [`Service::poll_ready`](tower::Service::poll_ready) failure occurred.
    PollReady(PollError),
}
+133 −0
Original line number Diff line number Diff line
/*
 * 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<Op, Exts, PollError>:
    Service<Self::Normalized, Response = Op::Output, Error = OperationError<Op::Error, PollError>>
where
    Op: OperationShape,
{
    type Normalized;

    // Normalize the request type.
    fn normalize(input: Op::Input, exts: Exts) -> Self::Normalized;
}

// `Service<Op::Input>`
impl<Op, S, PollError> OperationService<Op, (), PollError> for S
where
    Op: OperationShape,
    S: Service<Op::Input, Response = Op::Output, Error = OperationError<Op::Error, PollError>>,
{
    type Normalized = Op::Input;

    fn normalize(input: Op::Input, _exts: ()) -> Self::Normalized {
        input
    }
}

// `Service<(Op::Input, Ext0)>`
impl<Op, Ext0, S, PollError> OperationService<Op, (Ext0,), PollError> for S
where
    Op: OperationShape,
    S: Service<(Op::Input, Ext0), Response = Op::Output, Error = OperationError<Op::Error, PollError>>,
{
    type Normalized = (Op::Input, Ext0);

    fn normalize(input: Op::Input, exts: (Ext0,)) -> Self::Normalized {
        (input, exts.0)
    }
}

// `Service<(Op::Input, Ext0, Ext1)>`
impl<Op, Ext0, Ext1, S, PollError> OperationService<Op, (Ext0, Ext1), PollError> for S
where
    Op: OperationShape,
    S: Service<(Op::Input, Ext0, Ext1), Response = Op::Output, Error = OperationError<Op::Error, PollError>>,
{
    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<Op, Exts, PollError>: OperationService<Op, Exts, PollError>
where
    Op: OperationShape,
{
    /// Convert the [`OperationService`] into a canonicalized [`Service`].
    fn canonicalize(self) -> Normalize<Op, Self, PollError>
    where
        Self: Sized,
    {
        Normalize {
            inner: self,
            _operation: PhantomData,
            _poll_error: PhantomData,
        }
    }
}

impl<F, Op, Exts, PollError> OperationServiceExt<Op, Exts, PollError> for F
where
    Op: OperationShape,
    F: OperationService<Op, Exts, PollError>,
{
}

/// A [`Service`] normalizing the request type of a [`OperationService`].
#[derive(Debug)]
pub struct Normalize<Op, S, PollError> {
    inner: S,
    _operation: PhantomData<Op>,
    _poll_error: PhantomData<PollError>,
}

impl<Op, S, PollError> Clone for Normalize<Op, S, PollError>
where
    S: Clone,
{
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            _operation: PhantomData,
            _poll_error: PhantomData,
        }
    }
}

impl<Op, S, Exts, PollError> Service<(Op::Input, Exts)> for Normalize<Op, S, PollError>
where
    Op: OperationShape,
    S: OperationService<Op, Exts, PollError>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = <S as Service<S::Normalized>>::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        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)
    }
}
Loading