Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FromParts documentation #1930

Merged
merged 6 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions design/src/server/from-parts.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- TODO(IntoParts): Dually, what if we want to return data from the handler which is _not_ modelled by our Smithy model? -->

```rust
/// Provides a protocol aware extraction from a [`Request`]. This borrows the
/// [`Parts`], in contrast to [`FromRequest`].
pub trait FromParts<Protocol>: Sized {
type Rejection: IntoResponse<Protocol>;

/// Extracts `self` from a [`Parts`] synchronously.
fn from_parts(parts: &mut Parts) -> Result<Self, Self::Rejection>;
}
```

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<T>`:

```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<T>(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<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: 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)
}
}
```

This allows the service builder to accept the following handler

```rust
async fn handler(input: ModelInput, extension: Extension<SomeStruct>) -> 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<SomeStruct1>, ext2: Extension<SomeStruct2>, other: Other /* : FromParts */, /* ... */) -> ModelOutput {
/* ... */
}
```

Note that the `parts.extensions.remove::<T>()` in `Extensions::from_parts` will cause multiple `Extension<SomeStruct>` arguments in the handler to fail. The first extraction failure to occur is serialized via the `IntoResponse` trait (notice `type Error: IntoResponse<Protocol>`) and returned.

The `FromParts` trait is public so customers have the ability specify their own implementations:

```rust
struct CustomerDefined {
/* ... */
}

impl<P> FromParts<P> for CustomerDefined {
type Error = /* ... */;

fn from_parts(parts: &mut Parts) -> Result<Self, Self::Error> {
// 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 {
/* ... */
}
```
1 change: 1 addition & 0 deletions design/src/server/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Smithy Rust provides the ability to generate a server whose operations are provi
<!-- - [Middleware](./middleware.md) -->
- [Instrumentation](./instrumentation.md)
<!-- - [The Anatomy of a Service](./anatomy.md) -->
<!-- - [Accessing Un-modelled Data](./from-parts.md) -->
2 changes: 1 addition & 1 deletion rust-runtime/aws-smithy-http-server/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl<Protocol> IntoResponse<Protocol> for MissingExtension {

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

Expand Down