From e9005aa3aa718f959c381b015bf53924ef8bbabc Mon Sep 17 00:00:00 2001 From: Harry Barber Date: Tue, 16 Aug 2022 13:34:55 +0000 Subject: [PATCH 1/3] Add basic implementation --- Cargo.toml | 2 + generated/Cargo.toml | 17 ++ generated/examples/usage.rs | 67 ++++++ generated/src/lib.rs | 3 + generated/src/operations/empty_operation.rs | 44 ++++ .../src/operations/get_pokemon_species.rs | 52 +++++ generated/src/operations/mod.rs | 5 + generated/src/services/mod.rs | 3 + generated/src/services/pokemon_service.rs | 152 +++++++++++++ generated/src/structures.rs | 9 + runtime/Cargo.toml | 12 + runtime/src/lib.rs | 5 + runtime/src/make_service.rs | 35 +++ runtime/src/operation/handler.rs | 155 +++++++++++++ runtime/src/operation/mod.rs | 71 ++++++ runtime/src/operation/shape.rs | 43 ++++ runtime/src/operation/upgrade.rs | 209 ++++++++++++++++++ runtime/src/protocols.rs | 8 + runtime/src/router/mod.rs | 11 + runtime/src/router/rest.rs | 116 ++++++++++ runtime/src/service.rs | 4 + 21 files changed, 1023 insertions(+) create mode 100644 Cargo.toml create mode 100644 generated/Cargo.toml create mode 100644 generated/examples/usage.rs create mode 100644 generated/src/lib.rs create mode 100644 generated/src/operations/empty_operation.rs create mode 100644 generated/src/operations/get_pokemon_species.rs create mode 100644 generated/src/operations/mod.rs create mode 100644 generated/src/services/mod.rs create mode 100644 generated/src/services/pokemon_service.rs create mode 100644 generated/src/structures.rs create mode 100644 runtime/Cargo.toml create mode 100644 runtime/src/lib.rs create mode 100644 runtime/src/make_service.rs create mode 100644 runtime/src/operation/handler.rs create mode 100644 runtime/src/operation/mod.rs create mode 100644 runtime/src/operation/shape.rs create mode 100644 runtime/src/operation/upgrade.rs create mode 100644 runtime/src/protocols.rs create mode 100644 runtime/src/router/mod.rs create mode 100644 runtime/src/router/rest.rs create mode 100644 runtime/src/service.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..91774ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["generated", "runtime"] \ No newline at end of file diff --git a/generated/Cargo.toml b/generated/Cargo.toml new file mode 100644 index 0000000..8d3fe41 --- /dev/null +++ b/generated/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "generated" +version = "0.1.0" +edition = "2021" + +[dependencies] +runtime = { path = "../runtime" } + +futures = "0.3.21" +http = "0.2.8" +http-body = "0.4.5" +hyper = "0.14.20" +tower = "0.4.13" +pin-project-lite = "0.2.9" + +[dev-dependencies] +hyper = { version = "0.14.20", features = ["server", "http2", "tcp"] } diff --git a/generated/examples/usage.rs b/generated/examples/usage.rs new file mode 100644 index 0000000..2b31304 --- /dev/null +++ b/generated/examples/usage.rs @@ -0,0 +1,67 @@ +use std::{convert::Infallible, future::Ready, task::Poll}; + +use generated::{operations::*, services::*, structures::*}; +use runtime::operation::{AdjoinState, OperationError, OperationShapeExt}; +use tower::{util::MapResponseLayer, Service}; + +/// Fallible handler with state +async fn get_pokemon_species_stateful( + _input: GetPokemonSpeciesInput, + _state: usize, +) -> Result { + todo!() +} + +/// Fallible handler without state +async fn get_pokemon_species( + _input: GetPokemonSpeciesInput, +) -> Result { + todo!() +} + +/// Infallible handler without state +async fn empty_operation(_input: EmptyOperationInput) -> EmptyOperationOutput { + todo!() +} + +/// Bespoke implementation of `EmptyOperation`. +#[derive(Clone)] +struct EmptyOperationService; + +impl Service for EmptyOperationService { + type Response = EmptyOperationOutput; + type Error = OperationError; + type Future = Ready>; + + fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> Poll> { + todo!() + } + + fn call(&mut self, _req: EmptyOperationInput) -> Self::Future { + todo!() + } +} + +fn main() { + // Various ways of constructing operations + let _get_pokemon_species = GetPokemonSpecies::from_handler(get_pokemon_species); + let _empty_operation = EmptyOperation::from_handler(empty_operation); + let empty_operation = EmptyOperation::from_service(EmptyOperationService); + let get_pokemon_species = + GetPokemonSpecies::from_handler(get_pokemon_species_stateful.with_state(29)); + + // We can apply a layer to them + let get_pokemon_species = get_pokemon_species.layer(MapResponseLayer::new(|resp| resp)); + + // We can build the `PokemonService` with static type checking + let pokemon_service = PokemonService::builder() + .get_pokemon_species(get_pokemon_species) + .empty_operation(empty_operation) + .build(); + + // We can apply a layer to all routes + let pokemon_service = pokemon_service.layer(MapResponseLayer::new(|resp| resp)); + + let addr = "localhost:8000".parse().unwrap(); + hyper::server::Server::bind(&addr).serve(pokemon_service.into_make_service()); +} diff --git a/generated/src/lib.rs b/generated/src/lib.rs new file mode 100644 index 0000000..cd6335d --- /dev/null +++ b/generated/src/lib.rs @@ -0,0 +1,3 @@ +pub mod operations; +pub mod services; +pub mod structures; diff --git a/generated/src/operations/empty_operation.rs b/generated/src/operations/empty_operation.rs new file mode 100644 index 0000000..374cb46 --- /dev/null +++ b/generated/src/operations/empty_operation.rs @@ -0,0 +1,44 @@ +use std::{convert::Infallible, future::Ready}; + +use http_body::combinators::BoxBody; +use hyper::body::Bytes; +use runtime::{ + operation::{FromRequest, IntoResponse, OperationShape}, + protocols::AWSRestJsonV1, +}; + +use crate::structures::{EmptyOperationInput, EmptyOperationOutput}; + +pub struct EmptyOperation; + +impl OperationShape for EmptyOperation { + const NAME: &'static str = "EmptyOperation"; + + type Input = EmptyOperationInput; + type Output = EmptyOperationOutput; + type Error = Infallible; +} + +pub struct FromRequestError; + +impl IntoResponse for FromRequestError { + fn into_response(self) -> http::Response> { + todo!() + } +} + +impl FromRequest for EmptyOperationInput { + type Error = FromRequestError; + + type Future = Ready>; + + fn from_request(_request: http::Request) -> Self::Future { + todo!() + } +} + +impl IntoResponse for EmptyOperationOutput { + fn into_response(self) -> http::Response> { + todo!() + } +} diff --git a/generated/src/operations/get_pokemon_species.rs b/generated/src/operations/get_pokemon_species.rs new file mode 100644 index 0000000..1fb0b18 --- /dev/null +++ b/generated/src/operations/get_pokemon_species.rs @@ -0,0 +1,52 @@ +use std::future::Ready; + +use http_body::combinators::BoxBody; +use hyper::body::Bytes; +use runtime::{ + operation::{FromRequest, IntoResponse, OperationShape}, + protocols::AWSRestJsonV1, +}; + +use crate::structures::{ + GetPokemonSpeciesInput, GetPokemonSpeciesOutput, ResourceNotFoundException, +}; + +pub struct GetPokemonSpecies; + +impl OperationShape for GetPokemonSpecies { + const NAME: &'static str = "GetPokemonSpecies"; + + type Input = GetPokemonSpeciesInput; + type Output = GetPokemonSpeciesOutput; + type Error = ResourceNotFoundException; +} + +pub struct FromRequestError; + +impl IntoResponse for FromRequestError { + fn into_response(self) -> http::Response> { + todo!() + } +} + +impl FromRequest for GetPokemonSpeciesInput { + type Error = FromRequestError; + + type Future = Ready>; + + fn from_request(_request: http::Request) -> Self::Future { + todo!() + } +} + +impl IntoResponse for GetPokemonSpeciesOutput { + fn into_response(self) -> http::Response> { + todo!() + } +} + +impl IntoResponse for ResourceNotFoundException { + fn into_response(self) -> http::Response> { + todo!() + } +} diff --git a/generated/src/operations/mod.rs b/generated/src/operations/mod.rs new file mode 100644 index 0000000..8810059 --- /dev/null +++ b/generated/src/operations/mod.rs @@ -0,0 +1,5 @@ +pub mod empty_operation; +pub mod get_pokemon_species; + +pub use empty_operation::EmptyOperation; +pub use get_pokemon_species::GetPokemonSpecies; diff --git a/generated/src/services/mod.rs b/generated/src/services/mod.rs new file mode 100644 index 0000000..fbe7314 --- /dev/null +++ b/generated/src/services/mod.rs @@ -0,0 +1,3 @@ +mod pokemon_service; + +pub use pokemon_service::*; diff --git a/generated/src/services/pokemon_service.rs b/generated/src/services/pokemon_service.rs new file mode 100644 index 0000000..8106983 --- /dev/null +++ b/generated/src/services/pokemon_service.rs @@ -0,0 +1,152 @@ +use std::task::{Context, Poll}; + +use futures::{future::Map, FutureExt}; +use http_body::combinators::BoxBody; +use hyper::{body::Bytes, Error}; +use runtime::{ + make_service::IntoMakeService, + operation::{ + IntoResponse, Operation, OperationNotSet, OperationShape, UpgradeLayer, UpgradedService, + }, + protocols::AWSRestJsonV1, + router::{ + rest::{self, RoutingFuture}, + RouteService, + }, + service::ServiceError, +}; +use tower::{util::BoxCloneService, Layer, Service, ServiceExt}; + +use crate::operations::{get_pokemon_species::GetPokemonSpecies, EmptyOperation}; + +#[derive(Clone)] +pub struct PokemonService { + router: rest::Router, +} + +impl PokemonService { + /// Apply a [`Layer`] uniformly across all routes. + pub fn layer(self, layer: L) -> PokemonService + where + L: Layer, + { + PokemonService { + router: self.router.layer(layer), + } + } + + /// Converts [`PokemonService`] into a [`IntoMakeService`]. + pub fn into_make_service(self) -> IntoMakeService { + IntoMakeService::new(self) + } +} + +impl PokemonService<()> { + /// Creates a empty [`PokemonServiceBuilder`]. + pub fn builder() -> PokemonServiceBuilder { + PokemonServiceBuilder { + get_pokemon_species: OperationNotSet, + empty_operation: OperationNotSet, + } + } +} + +impl Service> for PokemonService +where + S: Service, Response = http::Response>>, + S: Clone, +{ + type Response = http::Response>; + + type Error = S::Error; + + type Future = Map< + RoutingFuture>, + fn( + Result>, + ) -> Result, + >; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.router.poll_ready(cx).map_err(|err| match err { + ServiceError::Routing(_) => unreachable!("routing errors cannot occur during poll"), + ServiceError::Poll(err) => err, + }) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + self.router.call(req).map(|result| match result { + Ok(ok) => Ok(ok), + Err(ServiceError::Poll(err)) => Err(err), + Err(ServiceError::Routing(err)) => Ok(err.into_response()), + }) + } +} + +/// The [`PokemonService`] builder. +pub struct PokemonServiceBuilder { + get_pokemon_species: Op1, + empty_operation: Op2, +} + +impl PokemonServiceBuilder { + pub fn get_pokemon_species( + self, + operation: Operation, + ) -> PokemonServiceBuilder, Op2> { + PokemonServiceBuilder { + get_pokemon_species: operation, + empty_operation: self.empty_operation, + } + } + + pub fn empty_operation( + self, + operation: Operation, + ) -> PokemonServiceBuilder> { + PokemonServiceBuilder { + get_pokemon_species: self.get_pokemon_species, + empty_operation: operation, + } + } +} + +impl PokemonServiceBuilder, Operation> { + pub fn build(self) -> PokemonService> + where + // GetPokemonSpecies composition + UpgradeLayer: Layer, + L1: Layer>, + S1: Service<::Input>, + L1::Service: Service, Response = http::Response>>, + L1::Service: Clone + Send + 'static, + >>::Future: Send + 'static, + >>::Error: + Into> + 'static, + + // EmptyOperation composition + UpgradeLayer: Layer, + L2: Layer>, + S2: Service<::Input>, + L2::Service: Service, Response = http::Response>>, + L2::Service: Clone + Send + 'static, + >>::Future: Send + 'static, + >>::Error: + Into> + 'static, + { + PokemonService { + router: [ + ( + GetPokemonSpecies::NAME, + BoxCloneService::new(self.get_pokemon_species.upgrade().map_err(Into::into)), + ), + ( + EmptyOperation::NAME, + BoxCloneService::new(self.empty_operation.upgrade().map_err(Into::into)), + ), + ] + .into_iter() + .collect(), + } + } +} diff --git a/generated/src/structures.rs b/generated/src/structures.rs new file mode 100644 index 0000000..34abd35 --- /dev/null +++ b/generated/src/structures.rs @@ -0,0 +1,9 @@ +pub struct EmptyOperationInput; + +pub struct EmptyOperationOutput; + +pub struct GetPokemonSpeciesInput; + +pub struct GetPokemonSpeciesOutput; + +pub struct ResourceNotFoundException; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml new file mode 100644 index 0000000..2db0973 --- /dev/null +++ b/runtime/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3.21" +http = "0.2.8" +http-body = "0.4.5" +hyper = "0.14.20" +tower = { version = "0.4.13", features = ["util"] } +pin-project-lite = "0.2.9" diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs new file mode 100644 index 0000000..b458657 --- /dev/null +++ b/runtime/src/lib.rs @@ -0,0 +1,5 @@ +pub mod make_service; +pub mod operation; +pub mod protocols; +pub mod router; +pub mod service; diff --git a/runtime/src/make_service.rs b/runtime/src/make_service.rs new file mode 100644 index 0000000..d266cbf --- /dev/null +++ b/runtime/src/make_service.rs @@ -0,0 +1,35 @@ +use std::{ + convert::Infallible, + future::{ready, Ready}, + task::{Context, Poll}, +}; + +use tower::Service; + +pub struct IntoMakeService { + svc: S, +} + +impl IntoMakeService { + pub fn new(svc: S) -> Self { + Self { svc } + } +} + +impl Service for IntoMakeService +where + S: Clone, +{ + type Response = S; + type Error = Infallible; + type Future = Ready>; + + #[inline] + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _target: T) -> Self::Future { + ready(Ok(self.svc.clone())) + } +} diff --git a/runtime/src/operation/handler.rs b/runtime/src/operation/handler.rs new file mode 100644 index 0000000..29652d1 --- /dev/null +++ b/runtime/src/operation/handler.rs @@ -0,0 +1,155 @@ +use std::{ + convert::Infallible, + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; + +use futures::{ + future::{Map, MapErr}, + FutureExt, TryFutureExt, +}; +use tower::Service; + +/// The operation [`Service`] has two classes of failure modes - the failure models specified by +/// the Smithy model and failures to [`Service::poll_ready`]. +pub enum OperationError { + /// A [`Service::poll_ready`] failure occured. + Poll(PollError), + /// An error modelled by the Smithy model occured. + Smithy(SmithyError), +} + +/// A utility trait used to provide an even interface for all handlers. +pub trait Handler { + type Future: Future>; + + fn call(&mut self, req: Input) -> Self::Future; +} + +/// A utility trait used to provide an even interface over return types `Result`/`Ok`. +trait ToResult { + fn into_result(self) -> Result; +} + +// We can convert from `Result` to `Result`. +impl ToResult for Result { + fn into_result(self) -> Result { + self + } +} + +// We can convert from `Ok` to `Result`. +impl ToResult for Ok { + fn into_result(self) -> Result { + Ok(self) + } +} + +// fn(Input) -> Output +impl Handler for F +where + F: FnMut(Input) -> Fut, + Fut: Future, + Fut::Output: ToResult, +{ + type Future = Map Result>; + + fn call(&mut self, req: Input) -> Self::Future { + (self)(req).map(ToResult::into_result) + } +} + +/// Adjoins state to a `fn(Input, State) -> Output` to create a [`Handler`]. +#[derive(Clone)] +pub struct StatefulHandler { + f: F, + state: T, +} + +// fn(Input, State) -> Output +impl Handler for StatefulHandler +where + T: Clone, + F: Fn(Input, T) -> Fut, + Fut: Future, + Fut::Output: ToResult, +{ + type Future = Map Result>; + + fn call(&mut self, req: Input) -> Self::Future { + let Self { f, state } = self; + f(req, state.clone()).map(ToResult::into_result) + } +} + +/// Provides the ability to [`AdjoinState::with_state`] on closures of the form +/// `(Input, State) -> Output` converting them to a [`StatefulHandler`] and therefore causing them +/// to implement [`Handler`]. +pub trait AdjoinState { + fn with_state(self, state: T) -> StatefulHandler + where + Self: Sized, + { + StatefulHandler { f: self, state } + } +} + +impl AdjoinState for F {} + +/// An extension trait for [`Handler`]. +pub trait HandlerExt: Handler { + /// Convert the [`Handler`] into a [`Service`]. + fn into_service(self) -> IntoService + where + Self: Sized, + { + IntoService { + handler: self, + _error: PhantomData, + _output: PhantomData, + } + } +} + +impl HandlerExt for H where + H: Handler +{ +} + +/// A [`Service`] provided for every [`Handler`]. +pub struct IntoService { + handler: H, + _output: PhantomData, + _error: PhantomData, +} + +impl Clone for IntoService +where + H: Clone, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + _output: PhantomData, + _error: PhantomData, + } + } +} + +impl Service for IntoService +where + H: Handler, +{ + type Response = 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, req: Input) -> Self::Future { + self.handler.call(req).map_err(OperationError::Smithy) + } +} diff --git a/runtime/src/operation/mod.rs b/runtime/src/operation/mod.rs new file mode 100644 index 0000000..1c63e9f --- /dev/null +++ b/runtime/src/operation/mod.rs @@ -0,0 +1,71 @@ +mod handler; +mod shape; +mod upgrade; + +use tower::{ + layer::util::{Identity, Stack}, + Layer, +}; + +pub use handler::*; +pub use shape::*; +pub use upgrade::*; + +/// Represents a Smithy operation, coupled with model [`Layer`] and HTTP [`Layer`]. +pub struct Operation { + inner: S, + layer: L, +} + +impl Operation { + /// Takes the [`Operation`], which contains the inner [`Service`](tower::Service), 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) -> , L> as Layer>::Service + 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 { + Self { + inner, + layer: Identity::new(), + } + } +} + +impl Operation> { + /// Creates an [`Operation`] from a [`Handler`]. + pub fn from_handler(handler: H) -> Self + where + H: Handler, + { + Self { + inner: handler.into_service(), + layer: Identity::new(), + } + } +} + +impl Operation { + /// Applies a [`Layer`] to the operation after is has been upgraded to a [`Service`] accepting + /// and returning [`http`] types. + pub fn layer(self, layer: NewL) -> Operation> { + Operation { + inner: self.inner, + layer: Stack::new(self.layer, layer), + } + } +} + +/// A marker struct indicating an [`Operation`] has not been set in a builder. +pub struct OperationNotSet; diff --git a/runtime/src/operation/shape.rs b/runtime/src/operation/shape.rs new file mode 100644 index 0000000..d1a1dc6 --- /dev/null +++ b/runtime/src/operation/shape.rs @@ -0,0 +1,43 @@ +use tower::Service; + +use super::{Handler, HandlerExt, IntoService, Operation, OperationError}; + +/// Mirrors the Smithy Operation shape. +pub trait OperationShape { + const NAME: &'static str; + + type Input; + type Output; + 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::Output, ::Error, H>> + where + H: Handler< + ::Input, + ::Output, + ::Error, + >, + { + Operation::from_service(handler.into_service()) + } + + /// Creates a new [`Operation`] for well-formed [`Service`]s. + fn from_service(svc: S) -> Operation + where + S: Service< + ::Input, + Response = ::Output, + Error = OperationError::Error>, + >, + { + Operation::from_service(svc) + } +} + +impl OperationShapeExt for S where S: OperationShape {} diff --git a/runtime/src/operation/upgrade.rs b/runtime/src/operation/upgrade.rs new file mode 100644 index 0000000..0661e34 --- /dev/null +++ b/runtime/src/operation/upgrade.rs @@ -0,0 +1,209 @@ +use std::{ + convert::Infallible, + future::Future, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::ready; +use http_body::combinators::BoxBody; +use hyper::body::Bytes; +use pin_project_lite::pin_project; +use tower::{Layer, Service}; + +use super::{OperationError, OperationShape}; + +/// A protocol and operation aware conversion from [`http`] types to Smithy types. +pub trait FromRequest: Sized { + /// Conversion failure. + type Error: IntoResponse; + type Future: Future>; + + fn from_request(request: http::Request) -> Self::Future; +} + +/// A protocol and operation aware conversion from Smithy types to [`http`] types. +pub trait IntoResponse: Sized { + fn into_response(self) -> http::Response>; +} + +impl IntoResponse for Infallible { + fn into_response(self) -> http::Response> { + match self {} + } +} + +/// 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, + _body: PhantomData, +} + +impl Default for UpgradeLayer { + fn default() -> Self { + Self { + _protocol: PhantomData, + _operation: 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, + inner, + } + } +} + +/// A 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, + _body: PhantomData, + inner: S, +} + +impl Clone for Upgrade +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + _protocol: PhantomData, + _operation: PhantomData, + _body: PhantomData, + inner: self.inner.clone(), + } + } +} + +pin_project! { + /// The [`Service::Future`] of [`Upgrade`]. + pub struct UpgradeFuture + where + Operation: OperationShape, + Operation::Input: FromRequest, + S: Service, + { + service: S, + #[pin] + inner: Inner<>::Future, S::Future> + } +} + +pin_project! { + #[project = InnerProj] + #[project_replace = InnerProjReplace] + enum Inner { + FromRequest { + #[pin] + inner: FromFut + }, + Inner { + #[pin] + call: HandlerFut + } + } +} + +impl Future for UpgradeFuture +where + // Op is used to specify the operation shape + Op: OperationShape, + // Smithy input must be convert from a HTTP request + Op::Input: FromRequest, + // Smithy output must be convert into a HTTP response + Op::Output: IntoResponse, + // Smithy error must convert into a HTTP response + OpError: IntoResponse, + + // The signature of the inner service is correct + S: Service> + + Clone, +{ + 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::Smithy(err)) => err.into_response(), + Err(OperationError::Poll(_)) => { + 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: OperationShape, + Op::Input: FromRequest, + Op::Output: IntoResponse, + OpError: IntoResponse, + S: Service> + + 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::Poll(err) => err, + OperationError::Smithy(_) => 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: >::from_request(req), + }, + } + } +} diff --git a/runtime/src/protocols.rs b/runtime/src/protocols.rs new file mode 100644 index 0000000..5a40f90 --- /dev/null +++ b/runtime/src/protocols.rs @@ -0,0 +1,8 @@ +#[derive(Clone)] +pub struct AWSRestJsonV1; + +#[derive(Clone)] +pub struct AWSRestJsonV1Dot1; + +#[derive(Clone)] +pub struct AWSRestXml; diff --git a/runtime/src/router/mod.rs b/runtime/src/router/mod.rs new file mode 100644 index 0000000..6dacbcf --- /dev/null +++ b/runtime/src/router/mod.rs @@ -0,0 +1,11 @@ +pub mod rest; + +use http_body::combinators::BoxBody; +use hyper::{body::Bytes, Error}; +use tower::util::BoxCloneService; + +pub type RouteService = BoxCloneService< + http::Request, + http::Response>, + Box, +>; diff --git a/runtime/src/router/rest.rs b/runtime/src/router/rest.rs new file mode 100644 index 0000000..e6287bf --- /dev/null +++ b/runtime/src/router/rest.rs @@ -0,0 +1,116 @@ +use std::{ + collections::HashMap, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use http_body::combinators::BoxBody; +use hyper::{body::Bytes, Error}; +use pin_project_lite::pin_project; +use tower::{util::Oneshot, Layer, Service, ServiceExt}; + +use crate::{operation::IntoResponse, service::ServiceError}; + +#[derive(Clone)] +pub struct Router { + inner: HashMap<&'static str, S>, +} + +impl FromIterator<(&'static str, S)> for Router { + fn from_iter>(iter: T) -> Self { + Self { + inner: HashMap::from_iter(iter), + } + } +} + +impl Router { + /// Apply a [`Layer`] uniformly across all routes. + pub fn layer(self, layer: L) -> Router + where + L: Layer, + { + Router { + inner: self + .inner + .into_iter() + .map(|(path, svc)| (path, layer.layer(svc))) + .collect(), + } + } +} + +pin_project! { + pub struct RoutingFuture where S: Service { + #[pin] + inner: Inner, RoutingError> + } +} + +impl Future for RoutingFuture +where + S: Service, +{ + type Output = Result>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let this2 = this.inner.project(); + match this2 { + InnerProj::Future { value } => value.poll(cx).map_err(ServiceError::Poll), + InnerProj::Ready { value } => { + let error = value + .take() + .expect("RoutingFuture cannot be polled after completion"); + Poll::Ready(Err(ServiceError::Routing(error))) + } + } + } +} + +pin_project! { + #[project = InnerProj] + enum Inner { + Future { + #[pin] + value: Fut + }, + Ready { value: Option } + } +} + +pub enum RoutingError { + Missing, +} + +impl IntoResponse for RoutingError { + fn into_response(self) -> http::Response> { + todo!() + } +} + +impl Service> for Router +where + S: Service> + Clone, +{ + type Response = S::Response; + type Error = ServiceError; + type Future = RoutingFuture>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = match self.inner.get_mut(req.uri().path()) { + Some(svc) => Inner::Future { + value: svc.clone().oneshot(req), + }, + None => Inner::Ready { + value: Some(RoutingError::Missing), + }, + }; + RoutingFuture { inner } + } +} diff --git a/runtime/src/service.rs b/runtime/src/service.rs new file mode 100644 index 0000000..9227e78 --- /dev/null +++ b/runtime/src/service.rs @@ -0,0 +1,4 @@ +pub enum ServiceError { + Routing(RoutingError), + Poll(PollError), +} From 8f8094052055c7adaa0cea3f95124d2fdc753b12 Mon Sep 17 00:00:00 2001 From: Harry Barber Date: Fri, 19 Aug 2022 12:58:50 +0000 Subject: [PATCH 2/3] Allow handler extensions --- generated/examples/usage.rs | 41 ++++-- generated/src/operations/empty_operation.rs | 2 +- .../src/operations/get_pokemon_species.rs | 2 +- generated/src/services/pokemon_service.rs | 14 +- runtime/src/operation/flattened.rs | 135 ++++++++++++++++++ runtime/src/operation/handler.rs | 121 ++++++++-------- runtime/src/operation/http_conversions.rs | 107 ++++++++++++++ runtime/src/operation/mod.rs | 42 ++++-- runtime/src/operation/shape.rs | 31 ++-- runtime/src/operation/upgrade.rs | 87 ++++++----- 10 files changed, 425 insertions(+), 157 deletions(-) create mode 100644 runtime/src/operation/flattened.rs create mode 100644 runtime/src/operation/http_conversions.rs diff --git a/generated/examples/usage.rs b/generated/examples/usage.rs index 2b31304..a490b67 100644 --- a/generated/examples/usage.rs +++ b/generated/examples/usage.rs @@ -1,34 +1,35 @@ use std::{convert::Infallible, future::Ready, task::Poll}; use generated::{operations::*, services::*, structures::*}; -use runtime::operation::{AdjoinState, OperationError, OperationShapeExt}; +use runtime::operation::{Extension, OperationError, OperationShapeExt}; use tower::{util::MapResponseLayer, Service}; -/// Fallible handler with state +// Fallible handler with extensions. async fn get_pokemon_species_stateful( _input: GetPokemonSpeciesInput, - _state: usize, + _ext_a: Extension, + _ext_b: Extension, ) -> Result { todo!() } -/// Fallible handler without state +// Fallible handler without extensions. async fn get_pokemon_species( _input: GetPokemonSpeciesInput, ) -> Result { todo!() } -/// Infallible handler without state +// Infallible handler without extensions. async fn empty_operation(_input: EmptyOperationInput) -> EmptyOperationOutput { todo!() } -/// Bespoke implementation of `EmptyOperation`. +// Bespoke implementation of `EmptyOperation`. #[derive(Clone)] -struct EmptyOperationService; +struct EmptyOperationServiceA; -impl Service for EmptyOperationService { +impl Service for EmptyOperationServiceA { type Response = EmptyOperationOutput; type Error = OperationError; type Future = Ready>; @@ -42,13 +43,31 @@ impl Service for EmptyOperationService { } } +// Bespoke implementation of `EmptyOperation` with an extension. +#[derive(Clone)] +struct EmptyOperationServiceB; + +impl Service<(EmptyOperationInput, Extension)> for EmptyOperationServiceB { + type Response = EmptyOperationOutput; + type Error = OperationError; + type Future = Ready>; + + fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> Poll> { + todo!() + } + + fn call(&mut self, _req: (EmptyOperationInput, Extension)) -> Self::Future { + todo!() + } +} + fn main() { // Various ways of constructing operations let _get_pokemon_species = GetPokemonSpecies::from_handler(get_pokemon_species); let _empty_operation = EmptyOperation::from_handler(empty_operation); - let empty_operation = EmptyOperation::from_service(EmptyOperationService); - let get_pokemon_species = - GetPokemonSpecies::from_handler(get_pokemon_species_stateful.with_state(29)); + let _empty_operation = EmptyOperation::from_service(EmptyOperationServiceA); + let empty_operation = EmptyOperation::from_service(EmptyOperationServiceB); + let get_pokemon_species = GetPokemonSpecies::from_handler(get_pokemon_species_stateful); // We can apply a layer to them let get_pokemon_species = get_pokemon_species.layer(MapResponseLayer::new(|resp| resp)); diff --git a/generated/src/operations/empty_operation.rs b/generated/src/operations/empty_operation.rs index 374cb46..02c8b8d 100644 --- a/generated/src/operations/empty_operation.rs +++ b/generated/src/operations/empty_operation.rs @@ -32,7 +32,7 @@ impl FromRequest for EmptyOperationInput { type Future = Ready>; - fn from_request(_request: http::Request) -> Self::Future { + fn from_request(_request: &mut http::Request) -> Self::Future { todo!() } } diff --git a/generated/src/operations/get_pokemon_species.rs b/generated/src/operations/get_pokemon_species.rs index 1fb0b18..6f171e8 100644 --- a/generated/src/operations/get_pokemon_species.rs +++ b/generated/src/operations/get_pokemon_species.rs @@ -34,7 +34,7 @@ impl FromRequest for GetPokemonSpeciesIn type Future = Ready>; - fn from_request(_request: http::Request) -> Self::Future { + fn from_request(_request: &mut http::Request) -> Self::Future { todo!() } } diff --git a/generated/src/services/pokemon_service.rs b/generated/src/services/pokemon_service.rs index 8106983..4ac555f 100644 --- a/generated/src/services/pokemon_service.rs +++ b/generated/src/services/pokemon_service.rs @@ -112,12 +112,12 @@ impl PokemonServiceBuilder { } impl PokemonServiceBuilder, Operation> { - pub fn build(self) -> PokemonService> + pub fn build(self) -> PokemonService> where // GetPokemonSpecies composition - UpgradeLayer: Layer, - L1: Layer>, - S1: Service<::Input>, + UpgradeLayer: Layer, + L1: Layer>, + S1: Service<(::Input, Exts1)>, L1::Service: Service, Response = http::Response>>, L1::Service: Clone + Send + 'static, >>::Future: Send + 'static, @@ -125,9 +125,9 @@ impl PokemonServiceBuilder, Operation> Into> + 'static, // EmptyOperation composition - UpgradeLayer: Layer, - L2: Layer>, - S2: Service<::Input>, + UpgradeLayer: Layer, + L2: Layer>, + S2: Service<(::Input, Exts2)>, L2::Service: Service, Response = http::Response>>, L2::Service: Clone + Send + 'static, >>::Future: Send + 'static, diff --git a/runtime/src/operation/flattened.rs b/runtime/src/operation/flattened.rs new file mode 100644 index 0000000..addea0b --- /dev/null +++ b/runtime/src/operation/flattened.rs @@ -0,0 +1,135 @@ +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<(Input, Arg0, Arg1, ...)>` to the canonical representation of +/// `Service<(Input, (Arg0, Arg1, ...))>` inline with [`IntoService`](super::IntoService). +pub trait Flattened: + Service> +where + Op: OperationShape, +{ + type Flattened; + + // Unflatten the request type. + fn unflatten(input: Op::Input, exts: Exts) -> Self::Flattened; +} + +// `Service` +impl Flattened for S +where + Op: OperationShape, + S: Service>, +{ + type Flattened = Op::Input; + + fn unflatten(input: Op::Input, _exts: ()) -> Self::Flattened { + input + } +} + +// `Service<(Op::Input, Arg0)>` +impl Flattened for S +where + Op: OperationShape, + S: Service< + (Op::Input, Arg0), + Response = Op::Output, + Error = OperationError, + >, +{ + type Flattened = (Op::Input, Arg0); + + fn unflatten(input: Op::Input, exts: (Arg0,)) -> Self::Flattened { + (input, exts.0) + } +} + +// `Service<(Op::Input, Arg0, Arg1)>` +impl Flattened for S +where + Op: OperationShape, + S: Service< + (Op::Input, Arg0, Arg1), + Response = Op::Output, + Error = OperationError, + >, +{ + type Flattened = (Op::Input, Arg0, Arg1); + + fn unflatten(input: Op::Input, exts: (Arg0, Arg1)) -> Self::Flattened { + (input, exts.0, exts.1) + } +} + +/// An extension trait of [`Flattened`]. +pub trait FlattenedExt: Flattened +where + Op: OperationShape, +{ + /// Convert the [`Flattened`] into a canonicalized [`Service`]. + fn into_unflatten(self) -> IntoUnflattened + where + Self: Sized, + { + IntoUnflattened { + inner: self, + _operation: PhantomData, + _poll_error: PhantomData, + } + } +} + +impl FlattenedExt for F +where + Op: OperationShape, + F: Flattened, +{ +} + +/// A [`Service`] canonicalizing the request type of a [`Flattened`]. +#[derive(Debug)] +pub struct IntoUnflattened { + inner: S, + _operation: PhantomData, + _poll_error: PhantomData, +} + +impl Clone for IntoUnflattened +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _operation: PhantomData, + _poll_error: PhantomData, + } + } +} + +impl Service<(Op::Input, Exts)> for IntoUnflattened +where + Op: OperationShape, + S: Flattened, +{ + 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::unflatten(input, exts); + self.inner.call(req) + } +} diff --git a/runtime/src/operation/handler.rs b/runtime/src/operation/handler.rs index 29652d1..dc2058e 100644 --- a/runtime/src/operation/handler.rs +++ b/runtime/src/operation/handler.rs @@ -11,20 +11,16 @@ use futures::{ }; use tower::Service; -/// The operation [`Service`] has two classes of failure modes - the failure models specified by -/// the Smithy model and failures to [`Service::poll_ready`]. -pub enum OperationError { - /// A [`Service::poll_ready`] failure occured. - Poll(PollError), - /// An error modelled by the Smithy model occured. - Smithy(SmithyError), -} +use super::{OperationError, OperationShape}; /// A utility trait used to provide an even interface for all handlers. -pub trait Handler { - type Future: Future>; +pub trait Handler +where + Op: OperationShape, +{ + type Future: Future>; - fn call(&mut self, req: Input) -> Self::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`. @@ -47,109 +43,108 @@ impl ToResult for Ok { } // fn(Input) -> Output -impl Handler for F +impl Handler for F where - F: FnMut(Input) -> Fut, + Op: OperationShape, + F: Fn(Op::Input) -> Fut, Fut: Future, - Fut::Output: ToResult, + Fut::Output: ToResult, { - type Future = Map Result>; + type Future = Map Result>; - fn call(&mut self, req: Input) -> Self::Future { - (self)(req).map(ToResult::into_result) + fn call(&mut self, input: Op::Input, _exts: ()) -> Self::Future { + (self)(input).map(ToResult::into_result) } } -/// Adjoins state to a `fn(Input, State) -> Output` to create a [`Handler`]. -#[derive(Clone)] -pub struct StatefulHandler { - f: F, - state: T, -} - -// fn(Input, State) -> Output -impl Handler for StatefulHandler +// fn(Input, Arg0) -> Output +impl Handler for F where - T: Clone, - F: Fn(Input, T) -> Fut, + Op: OperationShape, + F: Fn(Op::Input, Ext0) -> Fut, Fut: Future, - Fut::Output: ToResult, + Fut::Output: ToResult, { - type Future = Map Result>; + type Future = Map Result>; - fn call(&mut self, req: Input) -> Self::Future { - let Self { f, state } = self; - f(req, state.clone()).map(ToResult::into_result) + fn call(&mut self, input: Op::Input, exts: (Ext0,)) -> Self::Future { + (self)(input, exts.0).map(ToResult::into_result) } } -/// Provides the ability to [`AdjoinState::with_state`] on closures of the form -/// `(Input, State) -> Output` converting them to a [`StatefulHandler`] and therefore causing them -/// to implement [`Handler`]. -pub trait AdjoinState { - fn with_state(self, state: T) -> StatefulHandler - where - Self: Sized, - { - StatefulHandler { f: self, state } +// fn(Input, Arg0, Arg1) -> Output +impl Handler for F +where + Op: OperationShape, + F: Fn(Op::Input, Ext0, Ext1) -> Fut, + Fut: Future, + Fut::Output: ToResult, +{ + type Future = Map Result>; + + fn call(&mut self, input: Op::Input, exts: (Ext0, Ext1)) -> Self::Future { + (self)(input, exts.0, exts.1).map(ToResult::into_result) } } -impl AdjoinState for F {} - /// An extension trait for [`Handler`]. -pub trait HandlerExt: Handler { +pub trait HandlerExt: Handler +where + Op: OperationShape, +{ /// Convert the [`Handler`] into a [`Service`]. - fn into_service(self) -> IntoService + fn into_service(self) -> IntoService where Self: Sized, { IntoService { handler: self, - _error: PhantomData, - _output: PhantomData, + _operation: PhantomData, } } } -impl HandlerExt for H where - H: Handler +impl HandlerExt for H +where + Op: OperationShape, + H: Handler, { } /// A [`Service`] provided for every [`Handler`]. -pub struct IntoService { +pub struct IntoService { handler: H, - _output: PhantomData, - _error: PhantomData, + _operation: PhantomData, } -impl Clone for IntoService +impl Clone for IntoService where H: Clone, { fn clone(&self) -> Self { Self { handler: self.handler.clone(), - _output: PhantomData, - _error: PhantomData, + _operation: PhantomData, } } } -impl Service for IntoService +impl Service<(Op::Input, Exts)> for IntoService where - H: Handler, + Op: OperationShape, + H: Handler, { - type Response = Output; - type Error = OperationError; - type Future = MapErr Self::Error>; + 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, req: Input) -> Self::Future { - self.handler.call(req).map_err(OperationError::Smithy) + fn call(&mut self, (input, exts): (Op::Input, Exts)) -> Self::Future { + self.handler + .call(input, exts) + .map_err(OperationError::Smithy) } } diff --git a/runtime/src/operation/http_conversions.rs b/runtime/src/operation/http_conversions.rs new file mode 100644 index 0000000..6321c3f --- /dev/null +++ b/runtime/src/operation/http_conversions.rs @@ -0,0 +1,107 @@ +use std::{convert::Infallible, future::Ready}; + +use futures::{ + future::{MapErr, MapOk, TryJoin}, + Future, TryFutureExt, +}; +use http_body::combinators::BoxBody; +use hyper::body::Bytes; + +/// A protocol and operation aware conversion from [`http`] types to Smithy types. +pub trait FromRequest: Sized { + /// Conversion failure. + type Error: IntoResponse; + type Future: Future>; + + fn from_request(request: &mut http::Request) -> Self::Future; +} + +impl FromRequest for () { + type Error = Infallible; + type Future = Ready>; + + fn from_request(_request: &mut http::Request) -> Self::Future { + std::future::ready(Ok(())) + } +} + +impl FromRequest for (Arg0,) +where + Arg0: FromRequest, +{ + type Error = Arg0::Error; + type Future = MapOk Self>; + + fn from_request(request: &mut http::Request) -> Self::Future { + Arg0::from_request(request).map_ok(|arg0| (arg0,)) + } +} + +/// An wrapper for state stored in the [`http::Extensions`] map. +pub struct Extension(pub T); + +impl FromRequest for Extension +where + T: Sync + Send + 'static, +{ + type Error = Infallible; + type Future = Ready>; + + fn from_request(request: &mut http::Request) -> Self::Future { + std::future::ready(Ok(Extension( + request + .extensions_mut() + .remove() + .expect("request does not contain extension"), + ))) + } +} + +/// Represents one of two errors. +/// +/// Implements [`IntoResponse`] if both inner also implement it. +pub enum Either { + Left(A), + Right(B), +} + +impl IntoResponse for Either +where + A: IntoResponse, + B: IntoResponse, +{ + fn into_response(self) -> http::Response> { + match self { + Either::Left(left) => left.into_response(), + Either::Right(right) => right.into_response(), + } + } +} + +impl FromRequest for (Arg0, Arg1) +where + Arg0: FromRequest, + Arg1: FromRequest, +{ + type Error = Either; + + type Future = TryJoin< + MapErr Self::Error>, + MapErr Self::Error>, + >; + + fn from_request(_request: &mut http::Request) -> Self::Future { + todo!() + } +} + +/// A protocol and operation aware conversion from Smithy types to [`http`] types. +pub trait IntoResponse: Sized { + fn into_response(self) -> http::Response>; +} + +impl IntoResponse for Infallible { + fn into_response(self) -> http::Response> { + match self {} + } +} diff --git a/runtime/src/operation/mod.rs b/runtime/src/operation/mod.rs index 1c63e9f..b239f47 100644 --- a/runtime/src/operation/mod.rs +++ b/runtime/src/operation/mod.rs @@ -1,4 +1,6 @@ +mod flattened; mod handler; +mod http_conversions; mod shape; mod upgrade; @@ -7,7 +9,9 @@ use tower::{ Layer, }; +pub use flattened::*; pub use handler::*; +pub use http_conversions::*; pub use shape::*; pub use upgrade::*; @@ -17,15 +21,18 @@ pub struct Operation { layer: L, } +type StackedUpgradeService = + , L> as Layer>::Service; + impl Operation { /// Takes the [`Operation`], which contains the inner [`Service`](tower::Service), 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) -> , L> as Layer>::Service + pub fn upgrade(self) -> StackedUpgradeService where - UpgradeLayer: Layer, - L: Layer< as Layer>::Service>, + UpgradeLayer: Layer, + L: Layer< as Layer>::Service>, { let Self { inner, layer } = self; let layer = Stack::new(UpgradeLayer::new(), layer); @@ -33,21 +40,26 @@ impl Operation { } } -impl Operation { +impl Operation> { /// Creates an [`Operation`] from a [`Service`](tower::Service). - pub fn from_service(inner: S) -> Self { + pub fn from_service(inner: S) -> Self + where + Op: OperationShape, + S: Flattened, + { Self { - inner, + inner: inner.into_unflatten(), layer: Identity::new(), } } } -impl Operation> { +impl Operation> { /// Creates an [`Operation`] from a [`Handler`]. - pub fn from_handler(handler: H) -> Self + pub fn from_handler(handler: H) -> Self where - H: Handler, + Op: OperationShape, + H: Handler, { Self { inner: handler.into_service(), @@ -57,8 +69,7 @@ impl Operation> { } impl Operation { - /// Applies a [`Layer`] to the operation after is has been upgraded to a [`Service`] accepting - /// and returning [`http`] types. + /// 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, @@ -69,3 +80,12 @@ impl Operation { /// A marker struct indicating an [`Operation`] has not been set in a builder. pub struct OperationNotSet; + +/// The operation [`Service`] has two classes of failure modes - the failure models specified by +/// the Smithy model and failures to [`Service::poll_ready`]. +pub enum OperationError { + /// A [`Service::poll_ready`] failure occurred. + Poll(PollError), + /// An error modelled by the Smithy model occurred. + Smithy(SmithyError), +} diff --git a/runtime/src/operation/shape.rs b/runtime/src/operation/shape.rs index d1a1dc6..32b0ed1 100644 --- a/runtime/src/operation/shape.rs +++ b/runtime/src/operation/shape.rs @@ -1,40 +1,33 @@ -use tower::Service; - -use super::{Handler, HandlerExt, IntoService, Operation, OperationError}; +use super::{Flattened, Handler, IntoService, IntoUnflattened, Operation}; /// Mirrors the Smithy Operation shape. pub trait OperationShape { const NAME: &'static str; + /// The operation input. type Input; + /// The operation output. type Output; + /// The operation error. 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::Output, ::Error, H>> + fn from_handler(handler: H) -> Operation> where - H: Handler< - ::Input, - ::Output, - ::Error, - >, + H: Handler, + Self: Sized, { - Operation::from_service(handler.into_service()) + Operation::from_handler(handler) } - /// Creates a new [`Operation`] for well-formed [`Service`]s. - fn from_service(svc: S) -> Operation + /// Creates a new [`Operation`] for well-formed [`Service`](tower::Service)s. + fn from_service(svc: S) -> Operation> where - S: Service< - ::Input, - Response = ::Output, - Error = OperationError::Error>, - >, + S: Flattened, + Self: Sized, { Operation::from_service(svc) } diff --git a/runtime/src/operation/upgrade.rs b/runtime/src/operation/upgrade.rs index 0661e34..a1eae54 100644 --- a/runtime/src/operation/upgrade.rs +++ b/runtime/src/operation/upgrade.rs @@ -1,5 +1,6 @@ +#![allow(clippy::type_complexity)] + use std::{ - convert::Infallible, future::Future, marker::PhantomData, pin::Pin, @@ -12,82 +13,68 @@ use hyper::body::Bytes; use pin_project_lite::pin_project; use tower::{Layer, Service}; -use super::{OperationError, OperationShape}; - -/// A protocol and operation aware conversion from [`http`] types to Smithy types. -pub trait FromRequest: Sized { - /// Conversion failure. - type Error: IntoResponse; - type Future: Future>; - - fn from_request(request: http::Request) -> Self::Future; -} +use crate::operation::FromRequest; -/// A protocol and operation aware conversion from Smithy types to [`http`] types. -pub trait IntoResponse: Sized { - fn into_response(self) -> http::Response>; -} - -impl IntoResponse for Infallible { - fn into_response(self) -> http::Response> { - match self {} - } -} +use super::{IntoResponse, 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 { +pub struct UpgradeLayer { _protocol: PhantomData, _operation: PhantomData, + _exts: PhantomData, _body: PhantomData, } -impl Default for UpgradeLayer { +impl Default for UpgradeLayer { fn default() -> Self { Self { _protocol: PhantomData, _operation: PhantomData, + _exts: PhantomData, _body: PhantomData, } } } -impl UpgradeLayer { +impl UpgradeLayer { /// Creates a new [`UpgradeLayer`]. pub fn new() -> Self { Self::default() } } -impl Layer for UpgradeLayer { - type Service = Upgrade; +impl Layer for UpgradeLayer { + type Service = Upgrade; fn layer(&self, inner: S) -> Self::Service { Upgrade { _protocol: PhantomData, _operation: PhantomData, _body: PhantomData, + _exts: PhantomData, inner, } } } /// A alias allowing for quick access to [`UpgradeLayer`]s target [`Service`]. -pub type UpgradedService = as Layer>::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 { +pub struct Upgrade { _protocol: PhantomData, _operation: PhantomData, + _exts: PhantomData, _body: PhantomData, inner: S, } -impl Clone for Upgrade +impl Clone for Upgrade where S: Clone, { @@ -96,6 +83,7 @@ where _protocol: PhantomData, _operation: PhantomData, _body: PhantomData, + _exts: PhantomData, inner: self.inner.clone(), } } @@ -103,15 +91,15 @@ where pin_project! { /// The [`Service::Future`] of [`Upgrade`]. - pub struct UpgradeFuture + pub struct UpgradeFuture where Operation: OperationShape, - Operation::Input: FromRequest, - S: Service, + (Operation::Input, Exts): FromRequest, + S: Service<(Operation::Input, Exts)>, { service: S, #[pin] - inner: Inner<>::Future, S::Future> + inner: Inner<<(Operation::Input, Exts) as FromRequest>::Future, S::Future> } } @@ -130,20 +118,22 @@ pin_project! { } } -impl Future for UpgradeFuture +impl Future for UpgradeFuture where - // Op is used to specify the operation shape + // `Op` is used to specify the operation shape Op: OperationShape, - // Smithy input must be convert from a HTTP request + // Smithy input must convert from a HTTP request Op::Input: FromRequest, - // Smithy output must be convert into a HTTP response + // 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 + E: FromRequest, + // The signature of the inner service is correct - S: Service> - + Clone, + S: Service<(Op::Input, E), Response = Op::Output, Error = OperationError>, { type Output = Result>, PollError>; @@ -178,18 +168,27 @@ where } } -impl Service> for Upgrade +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, - S: Service> + + // Must be able to convert extensions + E: FromRequest, + + // The signature of the inner service is correct + S: Service<(Op::Input, E), Response = Op::Output, Error = OperationError> + Clone, { type Response = http::Response>; type Error = PollError; - type Future = UpgradeFuture; + type Future = UpgradeFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(|err| match err { @@ -198,11 +197,11 @@ where }) } - fn call(&mut self, req: http::Request) -> Self::Future { + fn call(&mut self, mut req: http::Request) -> Self::Future { UpgradeFuture { service: self.inner.clone(), inner: Inner::FromRequest { - inner: >::from_request(req), + inner: <(Op::Input, E) as FromRequest>::from_request(&mut req), }, } } From 0f54e40ebbf473bd7a33db40b130ce64c736a7cb Mon Sep 17 00:00:00 2001 From: Harry Barber Date: Tue, 23 Aug 2022 21:13:36 +0000 Subject: [PATCH 3/3] Add high-level overview --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index e4963e3..b67910f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,26 @@ Outer layers {customer} ## Proposal +The trait `OperationShape` models Smithy operations. + +```rust +trait OperationShape { + type Input; + type Output; + type Error; +} +``` + +A service builder sets an operation by accepting `Operation` where `S: Service<(Op::Input, Exts), Response = Op::Output, Error = PollError | Op::Error>>`, `OperationInput: FromRequest`, `Exts: FromRequest`, `Op::Output: IntoResponse`, and `Op::Error: IntoResponse` for `Op: OperationShape`. + +A `Operation` includes two constructors, `from_handler` which accepts a `H: Handler` and `from_service` which accepts a `S: Flattened`. The trait `Handler` is enjoyed by all closures which accept `(Op::Input, ...)` and return `Result`. The trait `Flattened` is enjoyed by all `Service<(Op::Input, ...), Response = Op::Output, Error = PollError | Op::Error>`. Both `Handler` and `Flattened` work to provide a common interface to convert to `S: Service<(Op::Input, Exts), Response = Op::Output, Error = PollError | Op::Error>` in `Operation`. + +The `UpgradeLayer` is a `Layer`, applied to such `S`. It uses the `FromRequest` and `IntoResponse` to wrap `S` in middleware - converting `S: Service<(Op::Input, Exts), Response = Op::Output, Error = PollError | Op::Error>` to `S: Service` in a protocol and operation aware way. + +The `Operation::upgrade` takes `S`, applies `UpgradeLayer`, then applies the `L: Layer`. The `L` in `Operation` can be set by the user to provide operation specific HTTP middleware. The `Operation::upgrade` is called in the service builder `build` method and the composition is immediately `Box`'d and collected up into the protocol specific router alongside the other routes. + +In this way the customer can provide, for a specific operation, middleware around `S` _and_ `S` after it's upgraded to a HTTP service via `L`. + ``` Outer layers {customer} |- Router {runtime + codegen}