Skip to content

Commit a026f6f

Browse files
authored
Add FromParts documentation (#1930)
1 parent beb8a68 commit a026f6f

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

design/src/server/from-parts.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
```

design/src/server/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Smithy Rust provides the ability to generate a server whose operations are provi
77
<!-- - [Middleware](./middleware.md) -->
88
- [Instrumentation](./instrumentation.md)
99
<!-- - [The Anatomy of a Service](./anatomy.md) -->
10+
<!-- - [Accessing Un-modelled Data](./from-parts.md) -->

rust-runtime/aws-smithy-http-server/src/extension.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ impl<Protocol> IntoResponse<Protocol> for MissingExtension {
185185

186186
impl<Protocol, T> FromParts<Protocol> for Extension<T>
187187
where
188-
T: Clone + Send + Sync + 'static,
188+
T: Send + Sync + 'static,
189189
{
190190
type Rejection = MissingExtension;
191191

0 commit comments

Comments
 (0)