|
| 1 | +# Accessing Un-modelled Data |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +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). |
| 6 | + |
| 7 | +<!-- TODO(IntoParts): Dually, what if we want to return data from the handler which is _not_ modelled by our Smithy model? --> |
| 8 | + |
| 9 | +```rust |
| 10 | +/// Provides a protocol aware extraction from a [`Request`]. This borrows the |
| 11 | +/// [`Parts`], in contrast to [`FromRequest`]. |
| 12 | +pub trait FromParts<Protocol>: Sized { |
| 13 | + type Rejection: IntoResponse<Protocol>; |
| 14 | + |
| 15 | + /// Extracts `self` from a [`Parts`] synchronously. |
| 16 | + fn from_parts(parts: &mut Parts) -> Result<Self, Self::Rejection>; |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +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. |
| 21 | + |
| 22 | +A prolific example of a `FromParts` implementation is `Extension<T>`: |
| 23 | + |
| 24 | +```rust |
| 25 | +/// Generic extension type stored in and extracted from [request extensions]. |
| 26 | +/// |
| 27 | +/// This is commonly used to share state across handlers. |
| 28 | +/// |
| 29 | +/// If the extension is missing it will reject the request with a `500 Internal |
| 30 | +/// Server Error` response. |
| 31 | +/// |
| 32 | +/// [request extensions]: https://docs.rs/http/latest/http/struct.Extensions.html |
| 33 | +#[derive(Debug, Clone)] |
| 34 | +pub struct Extension<T>(pub T); |
| 35 | + |
| 36 | +/// The extension has not been added to the [`Request`](http::Request) or has been previously removed. |
| 37 | +#[derive(Debug, Error)] |
| 38 | +#[error("the `Extension` is not present in the `http::Request`")] |
| 39 | +pub struct MissingExtension; |
| 40 | + |
| 41 | +impl<Protocol> IntoResponse<Protocol> for MissingExtension { |
| 42 | + fn into_response(self) -> http::Response<BoxBody> { |
| 43 | + let mut response = http::Response::new(empty()); |
| 44 | + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; |
| 45 | + response |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +impl<Protocol, T> FromParts<Protocol> for Extension<T> |
| 50 | +where |
| 51 | + T: Send + Sync + 'static, |
| 52 | +{ |
| 53 | + type Rejection = MissingExtension; |
| 54 | + |
| 55 | + fn from_parts(parts: &mut http::request::Parts) -> Result<Self, Self::Rejection> { |
| 56 | + parts.extensions.remove::<T>().map(Extension).ok_or(MissingExtension) |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +This allows the service builder to accept the following handler |
| 62 | + |
| 63 | +```rust |
| 64 | +async fn handler(input: ModelInput, extension: Extension<SomeStruct>) -> ModelOutput { |
| 65 | + /* ... */ |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +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). |
| 70 | + |
| 71 | +Up to 32 structures implementing `FromParts` can be provided to the handler with the constraint that they _must_ be provided _after_ the `ModelInput`: |
| 72 | + |
| 73 | +```rust |
| 74 | +async fn handler(input: ModelInput, ext1: Extension<SomeStruct1>, ext2: Extension<SomeStruct2>, other: Other /* : FromParts */, /* ... */) -> ModelOutput { |
| 75 | + /* ... */ |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +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. |
| 80 | + |
| 81 | +The `FromParts` trait is public so customers have the ability specify their own implementations: |
| 82 | + |
| 83 | +```rust |
| 84 | +struct CustomerDefined { |
| 85 | + /* ... */ |
| 86 | +} |
| 87 | + |
| 88 | +impl<P> FromParts<P> for CustomerDefined { |
| 89 | + type Error = /* ... */; |
| 90 | + |
| 91 | + fn from_parts(parts: &mut Parts) -> Result<Self, Self::Error> { |
| 92 | + // Construct `CustomerDefined` using the request headers. |
| 93 | + let header_value = parts.headers.get("header-name").ok_or(/* ... */)?; |
| 94 | + Ok(CustomerDefined { /* ... */ }) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +async fn handler(input: ModelInput, arg: CustomerDefined) -> ModelOutput { |
| 99 | + /* ... */ |
| 100 | +} |
| 101 | +``` |
0 commit comments