From a026f6f6d6bed5e628b9b1a37198019c11257aa7 Mon Sep 17 00:00:00 2001 From: Harry Barber <106155934+hlbarber@users.noreply.github.com> Date: Tue, 1 Nov 2022 18:04:39 +0000 Subject: [PATCH] Add FromParts documentation (#1930) --- design/src/server/from-parts.md | 101 ++++++++++++++++++ design/src/server/overview.md | 1 + .../aws-smithy-http-server/src/extension.rs | 2 +- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 design/src/server/from-parts.md diff --git a/design/src/server/from-parts.md b/design/src/server/from-parts.md new file mode 100644 index 0000000000..695b1bb0ab --- /dev/null +++ b/design/src/server/from-parts.md @@ -0,0 +1,101 @@ +# Accessing Un-modelled Data + +For every [Smithy Operation](https://awslabs.github.io/smithy/2.0/spec/service-types.html#operation) an input, output, and optional error are specified. This in turn constrains the function signature of the handler provided to the service builder - the input to the handler must be the input specified by the operation etc. + +But what if we, the customer, want to access data in the handler which is _not_ modelled by our Smithy model? Smithy Rust provides an escape hatch in the form of the `FromParts` trait. In [`axum`](https://docs.rs/axum/latest/axum/index.html) these are referred to as ["extractors"](https://docs.rs/axum/latest/axum/extract/index.html). + + + +```rust +/// 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; +} +``` + +Here [`Parts`](https://docs.rs/http/latest/http/request/struct.Parts.html) is the struct containing all items in a [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) except for the HTTP body. + +A prolific example of a `FromParts` implementation is `Extension`: + +```rust +/// Generic extension type stored in and extracted from [request extensions]. +/// +/// This is commonly used to share state across handlers. +/// +/// If the extension is missing it will reject the request with a `500 Internal +/// Server Error` response. +/// +/// [request extensions]: https://docs.rs/http/latest/http/struct.Extensions.html +#[derive(Debug, Clone)] +pub struct Extension(pub 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 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: Send + Sync + 'static, +{ + type Rejection = MissingExtension; + + fn from_parts(parts: &mut http::request::Parts) -> Result { + parts.extensions.remove::().map(Extension).ok_or(MissingExtension) + } +} +``` + +This allows the service builder to accept the following handler + +```rust +async fn handler(input: ModelInput, extension: Extension) -> ModelOutput { + /* ... */ +} +``` + +where `ModelInput` and `ModelOutput` are specified by the Smithy Operation and `SomeStruct` is a struct which has been inserted, by middleware, into the [`http::Request::extensions`](https://docs.rs/http/latest/http/request/struct.Request.html#method.extensions). + +Up to 32 structures implementing `FromParts` can be provided to the handler with the constraint that they _must_ be provided _after_ the `ModelInput`: + +```rust +async fn handler(input: ModelInput, ext1: Extension, ext2: Extension, other: Other /* : FromParts */, /* ... */) -> ModelOutput { + /* ... */ +} +``` + +Note that the `parts.extensions.remove::()` in `Extensions::from_parts` will cause multiple `Extension` arguments in the handler to fail. The first extraction failure to occur is serialized via the `IntoResponse` trait (notice `type Error: IntoResponse`) and returned. + +The `FromParts` trait is public so customers have the ability specify their own implementations: + +```rust +struct CustomerDefined { + /* ... */ +} + +impl

FromParts

for CustomerDefined { + type Error = /* ... */; + + fn from_parts(parts: &mut Parts) -> Result { + // Construct `CustomerDefined` using the request headers. + let header_value = parts.headers.get("header-name").ok_or(/* ... */)?; + Ok(CustomerDefined { /* ... */ }) + } +} + +async fn handler(input: ModelInput, arg: CustomerDefined) -> ModelOutput { + /* ... */ +} +``` diff --git a/design/src/server/overview.md b/design/src/server/overview.md index e710ea2695..7af8b53585 100644 --- a/design/src/server/overview.md +++ b/design/src/server/overview.md @@ -7,3 +7,4 @@ Smithy Rust provides the ability to generate a server whose operations are provi - [Instrumentation](./instrumentation.md) + diff --git a/rust-runtime/aws-smithy-http-server/src/extension.rs b/rust-runtime/aws-smithy-http-server/src/extension.rs index 736dfd0c44..220b697c78 100644 --- a/rust-runtime/aws-smithy-http-server/src/extension.rs +++ b/rust-runtime/aws-smithy-http-server/src/extension.rs @@ -185,7 +185,7 @@ impl IntoResponse for MissingExtension { impl FromParts for Extension where - T: Clone + Send + Sync + 'static, + T: Send + Sync + 'static, { type Rejection = MissingExtension;