diff --git a/design/src/server/anatomy.md b/design/src/server/anatomy.md index 4c856a1fdc..466e66722f 100644 --- a/design/src/server/anatomy.md +++ b/design/src/server/anatomy.md @@ -634,6 +634,44 @@ pub struct PokemonService { } ``` +The following schematic summarizes the composition: + +```mermaid +stateDiagram-v2 + state in <> + state "GetPokemonSpecies" as C1 + state "GetStorage" as C2 + state "DoNothing" as C3 + state "..." as C4 + direction LR + [*] --> in : HTTP Request + UpgradeLayer --> [*]: HTTP Response + state PokemonService { + state RoutingService { + in --> UpgradeLayer: HTTP Request + in --> C2: HTTP Request + in --> C3: HTTP Request + in --> C4: HTTP Request + state C1 { + state L { + state UpgradeLayer { + direction LR + [*] --> S: Model Input + S --> [*] : Model Output + } + } + } + C2 + C3 + C4 + } + + } + C2 --> [*]: HTTP Response + C3 --> [*]: HTTP Response + C4 --> [*]: HTTP Response +``` + ## Plugins diff --git a/design/src/server/middleware.md b/design/src/server/middleware.md new file mode 100644 index 0000000000..8d9130286a --- /dev/null +++ b/design/src/server/middleware.md @@ -0,0 +1,350 @@ +# Middleware + +The following document provides a brief survey of the various positions middleware can be inserted in Smithy Rust. + +We use the [Pokémon service](https://github.com/awslabs/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy) as a reference model throughout. + +```smithy +/// A Pokémon species forms the basis for at least one Pokémon. +@title("Pokémon Species") +resource PokemonSpecies { + identifiers: { + name: String + }, + read: GetPokemonSpecies, +} + +/// A users current Pokémon storage. +resource Storage { + identifiers: { + user: String + }, + read: GetStorage, +} + +/// The Pokémon Service allows you to retrieve information about Pokémon species. +@title("Pokémon Service") +@restJson1 +service PokemonService { + version: "2021-12-01", + resources: [PokemonSpecies, Storage], + operations: [ + GetServerStatistics, + DoNothing, + CapturePokemon, + CheckHealth + ], +} +``` + +## Introduction to Tower + +Smithy Rust is built on top of [`tower`](https://github.com/tower-rs/tower). + +> Tower is a library of modular and reusable components for building robust networking clients and servers. + +The `tower` library is centered around two main interfaces, the [`Service`](https://docs.rs/tower/latest/tower/trait.Service.html) trait and the [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html) trait. + +The `Service` trait can be thought of as an asynchronous function from a request to a response, `async fn(Request) -> Result`, coupled with a mechanism to [handle back pressure](https://docs.rs/tower/latest/tower/trait.Service.html#backpressure), while the `Layer` trait can be thought of as a way of decorating a `Service`, transforming either the request or response. + +Middleware in `tower` typically conforms to the following pattern, a `Service` implementation of the form + +```rust +pub struct NewService { + inner: S, + /* auxillary data */ +} +``` + +and a complementary + +```rust +pub struct NewLayer { + /* auxiliary data */ +} + +impl Layer for NewLayer { + type Service = NewService; + + fn layer(&self, inner: S) -> Self::Service { + NewService { + inner, + /* auxiliary fields */ + } + } +} +``` + +The `NewService` modifies the behavior of the inner `Service` `S` while the `NewLayer` takes auxiliary data and constructs `NewService` from `S`. + +Customers are then able to stack middleware by composing `Layer`s using combinators such as [`ServiceBuilder::layer`](https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html#method.layer) and [`Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html). + + + +## Applying Middleware + +One of the primary goals is to provide configurability and extensibility through the application of middleware. The customer is able to apply `Layer`s in a variety of key places during the request/response lifecycle. The following schematic labels each configurable middleware position from A to D: + +```mermaid +stateDiagram-v2 + state in <> + state "GetPokemonSpecies" as C1 + state "GetStorage" as C2 + state "DoNothing" as C3 + state "..." as C4 + direction LR + [*] --> in : HTTP Request + UpgradeLayer --> [*]: HTTP Response + state A { + state PokemonService { + state RoutingService { + in --> UpgradeLayer: HTTP Request + in --> C2: HTTP Request + in --> C3: HTTP Request + in --> C4: HTTP Request + state B { + state C1 { + state C { + state UpgradeLayer { + direction LR + [*] --> Handler: Model Input + Handler --> [*] : Model Output + state D { + Handler + } + } + } + } + C2 + C3 + C4 + } + } + } + } + C2 --> [*]: HTTP Response + C3 --> [*]: HTTP Response + C4 --> [*]: HTTP Response +``` + +where `UpgradeLayer` is the `Layer` converting Smithy model structures to HTTP structures and the `RoutingService` is responsible for routing requests to the appropriate operation. + +### A) Outer Middleware + +The output of the Smithy service builder provides the user with a `Service` implementation. A `Layer` can be applied around the entire `Service`. + +```rust +// This is a HTTP `Service`. +let app /* : PokemonService> */ = PokemonService::builder() + .get_pokemon_species(/* handler */) + /* ... */ + .build(); + +// Construct `TimeoutLayer`. +let timeout_layer = TimeoutLayer::new(Duration::from_secs(3)); + +// Apply a 3 second timeout to all responses. +let app = timeout_layer.layer(app); +``` + +### B) Route Middleware + +A _single_ layer can be applied to _all_ routes inside the `Router`. This exists as a method on the output of the service builder. + +```rust +// Construct `TraceLayer`. +let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); + +let app /* : PokemonService> */ = PokemonService::builder() + .get_pokemon_species(/* handler */) + /* ... */ + .build() + // Apply HTTP logging after routing. + .layer(&trace_layer); +``` + +Note that requests pass through this middleware immediately _after_ routing succeeds and therefore will _not_ be encountered if routing fails. This means that the [TraceLayer](https://docs.rs/tower-http/latest/tower_http/trace/struct.TraceLayer.html) in the example above does _not_ provide logs unless routing has completed. This contrasts to [middleware A](#a-outer-middleware), which _all_ requests/responses pass through when entering/leaving the service. + +### C) Operation Specific HTTP Middleware + +A "HTTP layer" can be applied to specific operations. + +```rust +// Construct `TraceLayer`. +let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); + +// Apply HTTP logging to only the `GetPokemonSpecies` operation. +let layered_handler = GetPokemonSpecies::from_handler(/* handler */).layer(trace_layer); + +let app /* : PokemonService> */ = PokemonService::builder() + .get_pokemon_species_operation(layered_handler) + /* ... */ + .build(); +``` + +This middleware transforms the operations HTTP requests and responses. + +### D) Operation Specific Model Middleware + +A "model layer" can be applied to specific operations. + +```rust +// A handler `Service`. +let handler_svc = service_fn(/* handler */); + +// Construct `BufferLayer`. +let buffer_layer = BufferLayer::new(3); + +// Apply a 3 item buffer to `handler_svc`. +let handler_svc = buffer_layer.layer(handler_svc); + +let layered_handler = GetPokemonSpecies::from_service(handler_svc); + +let app /* : PokemonService> */ = PokemonService::builder() + .get_pokemon_species_operation(layered_handler) + /* ... */ + .build(); +``` + +In contrast to [position C](#c-operation-specific-http-middleware), this middleware transforms the operations modelled inputs to modelled outputs. + +## Plugin System + +Suppose we want to apply a different `Layer` to every operation. In this case, position B (`PokemonService::layer`) will not suffice because it applies a single `Layer` to all routes and while position C (`Operation::layer`) would work, it'd require the customer constructs the `Layer` by hand for every operation. + +Consider the following middleware: + +```rust +/// A [`Service`] that adds a print log. +#[derive(Clone, Debug)] +pub struct PrintService { + inner: S, + name: &'static str, +} + +impl Service for PrintService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: R) -> Self::Future { + println!("Hi {}", self.name); + self.inner.call(req) + } +} + +/// A [`Layer`] which constructs the [`PrintService`]. +#[derive(Debug)] +pub struct PrintLayer { + name: &'static str, +} +impl Layer for PrintLayer { + type Service = PrintService; + + fn layer(&self, service: S) -> Self::Service { + PrintService { + inner: service, + name: self.name, + } + } +} +``` + +The plugin system provides a way to construct then apply `Layer`s in position [C](#c-operation-specific-http-middleware) and [D](#d-operation-specific-model-middleware), using the [protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/index.html) and [operation shape](https://awslabs.github.io/smithy/2.0/spec/service-types.html#service-operations) as parameters. + +An example of a `PrintPlugin` which applies a layer printing the operation name: + +```rust +/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. +#[derive(Debug)] +pub struct PrintPlugin; + +impl Plugin for PrintPlugin +where + Op: OperationShape, +{ + type Service = S; + type Layer = Stack; + + fn map(&self, input: Operation) -> Operation { + input.layer(PrintLayer { name: Op::NAME }) + } +} +``` + +An alternative example which applies a layer for a given protocol: + +```rust +/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. +#[derive(Debug)] +pub struct PrintPlugin; + +impl Plugin for PrintPlugin +{ + type Service = S; + type Layer = Stack; + + fn map(&self, input: Operation) -> Operation { + input.layer(PrintLayer { name: "AWS REST JSON v1" }) + } +} + +impl Plugin for PrintPlugin +{ + type Service = S; + type Layer = Stack; + + fn map(&self, input: Operation) -> Operation { + input.layer(PrintLayer { name: "AWS REST XML" }) + } +} +``` + +A `Plugin` can then be applied to all operations using the `Pluggable::apply` method + +```rust +pub trait Pluggable { + type Output; + + /// Applies a [`Plugin`] to the service builder. + fn apply(self, plugin: NewPlugin) -> Self::Output; +} +``` + +which is implemented on every service builder. + +The plugin system is designed to hide the details of the `Plugin` and `Pluggable` trait from the average consumer. Such customers should instead interact with utility methods on the service builder which are vended by extension traits and enjoy self contained documentation. + +```rust +/// An extension trait of [`Pluggable`]. +/// +/// This provides a [`print`](PrintExt::print) method to all service builders. +pub trait PrintExt: Pluggable { + /// Causes all operations to print the operation name when called. + /// + /// This works by applying the [`PrintPlugin`]. + fn print(self) -> Self::Output + where + Self: Sized, + { + self.apply(PrintPlugin) + } +} +``` + +which allows for + +```rust +let app /* : PokemonService> */ = PokemonService::builder() + .get_pokemon_species_operation(layered_handler) + /* ... */ + .print() + .build(); +``` diff --git a/design/src/server/overview.md b/design/src/server/overview.md index 8957eb45d0..e710ea2695 100644 --- a/design/src/server/overview.md +++ b/design/src/server/overview.md @@ -4,5 +4,6 @@ Smithy Rust provides the ability to generate a server whose operations are provi - [Generating Common Service Code](./code_generation.md) - [Generating the Pokémon Service](./pokemon_service.md) + - [Instrumentation](./instrumentation.md) diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs index cb05ef2e4e..baf9a69b11 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs @@ -11,7 +11,7 @@ use tower::{layer::util::Stack, Layer, Service}; use std::task::{Context, Poll}; -/// A [`Service`](tower::Service) that adds a print log. +/// A [`Service`] that adds a print log. #[derive(Clone, Debug)] pub struct PrintService { inner: S, @@ -36,7 +36,7 @@ where } } -/// A [`Layer`](tower::Layer) which constructs the [`PrintService`]. +/// A [`Layer`] which constructs the [`PrintService`]. #[derive(Debug)] pub struct PrintLayer { name: &'static str, @@ -68,7 +68,9 @@ where } } -/// An extension to service builders to add the `print()` function. +/// An extension trait of [`Pluggable`]. +/// +/// This provides a [`print`](PrintExt::print) method to all service builders. pub trait PrintExt: Pluggable { /// Causes all operations to print the operation name when called. ///