Skip to content

Commit d720c1f

Browse files
committed
Extractor pattern: Construction instead of description
1 parent 3afd951 commit d720c1f

1 file changed

Lines changed: 114 additions & 84 deletions

File tree

_drafts/2019-01-01-extractor-pattern.md

Lines changed: 114 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,89 +3,119 @@ title: The Extractor pattern
33
categories:
44
- rust
55
---
6-
A very good questions.
7-
There are no nasty code generation or unsafe shenanigans as far as I know,
8-
just good old Rust traits.
9-
It uses something often called "extractor pattern".
10-
You can find a high-level overview on the usage
11-
[here](https://actix.rs/docs/extractors/)
12-
but you have probably already seen that.
13-
14-
Let's dig into the docs!
15-
(I believe it's helpful to see how I came to understand this
16-
by clicking through the API docs.
17-
Skip the next 3 paragraphs if you only care about the "magic" way
18-
actix-web allows you to extract data from requests.)
19-
20-
The example you linked to contains
21-
`App::new().resource("/{name}/{id}/index.html", |r| r.with(index))`.
22-
Typing `App::resource` into the doc search
23-
gives use the [right method](https://docs.rs/actix-web/0.7.17/actix_web/struct.App.html#method.resource) it seems.
24-
The interesting part here is the closure type
25-
(`FnOnce(&mut Resource<S>) -> R`)
26-
because it tells use the `r` is a reference to a `Resource`.
27-
Clicking on that
28-
and scrolling down
29-
leads us to [`Resource::with`](https://docs.rs/actix-web/0.7.17/actix_web/dev/struct.Resource.html#method.with).
30-
Nice. But now it gets complicated.
31-
32-
The full signature for `with` is:
33-
34-
pub fn with<T, F, R>(&mut self, handler: F) where
35-
F: WithFactory<T, S, R>,
36-
R: Responder + 'static,
37-
T: FromRequest<S> + 'static,
38-
39-
Let's unwrap that a bit.
40-
The parameter we give to `with` has to be something that implements the `WithFactory` trait.
41-
But now it gets weird:
42-
This trait is private!
43-
So, from the docs,
44-
we can only infer that it has three type parameters.
45-
The first is something that implements `FromRequest`,
46-
the second (`S`) is probably some state (guessing from the name only),
47-
and the last one is something that implements `Responder`.
48-
So I'd guess we are dealing with something that
49-
takes some data from a request,
50-
some state,
51-
and returns something new
52-
that can be used as a response.
53-
Sounds useful in the context of a web framework.
54-
55-
The part we are interested in is [`FromRequest`](https://docs.rs/actix-web/0.7.17/actix_web/trait.FromRequest.html).
56-
This is a trait that abstracts over extracting data from a request structure
57-
(its two methods are `from_request` and `extract`!).
58-
59-
This is a long docs page.
60-
The part you ask about is almost at the bottom in the "Implementors" section.
61-
For example,
62-
[`impl<T, S> FromRequest<S> for Form<T>`](https://docs.rs/actix-web/0.7.17/actix_web/trait.FromRequest.html#impl-FromRequest%3CS%3E-18),
63-
or [`impl<T, S> FromRequest<S> for Path<T>`](https://docs.rs/actix-web/0.7.17/actix_web/trait.FromRequest.html#impl-FromRequest%3CS%3E-20).
64-
And this is basically all there is to it!
65-
These types allow you to use them in a context
66-
where you want to extract data from a request!
67-
68-
The concrete usage of that
69-
and the way that the obscure `WithFactory` comes into play is also quite interesting.
70-
I wrote above that "no code generation magic" was used
71-
-- I might have lied a bit.
72-
To support *multiple* parameters/extractors in the functions you pass to `with`
73-
the `WithFactory` trait must be implemented for functions/closures
74-
that have multiple parameters.
75-
For that, the actix developers use [a macro](https://github.com/actix/actix-web/blob/0745a1a9f8d43840454c6aae24df5e2c6f781c36/src/with.rs#L291-L306) internally
76-
to generate implementations of `WithFactory`
77-
for functions that take tuples of up to 10 fields that implement `FromRequest`.
78-
79-
I couldn't find this documented in the API docs,
80-
but the website contains user documentation, too,
81-
and as mentioned above has a page on [Extractors](https://actix.rs/docs/extractors/)
82-
with [this section](https://actix.rs/docs/extractors/#multiple-extractors)
83-
showing an example of using a function with multiple extractors.
84-
So, all in all,
85-
this means that you can write `.with(index)` and have this functions:
86-
87-
fn index((path, query): (Path<(u32, String)>, Query<Info>)) -> String {
88-
format!("Welcome {}!", query.username)
6+
Let's assume we receive some kind of data:
7+
8+
```rust
9+
struct Blob { header: String, content: Vec<u8>, }
10+
11+
let incoming_data = Blob {
12+
header: "Lorem ipsum\nDolor sit amet".into(),
13+
content: r#"{"text": "Hello world"}"#.into()
14+
};
15+
```
16+
17+
We can write custom functions
18+
that try to get some of the information
19+
out of this `Blob`:
20+
21+
```rust
22+
fn get_topic(input: &Blob) -> String {
23+
if let Some(line) = input.header.lines().next() {
24+
line.to_string()
25+
} else {
26+
String::from("[no topic set]")
8927
}
28+
}
29+
```
30+
31+
This is easy enough to use:
32+
33+
```rust
34+
let topic = get_topic(&incoming_data);
35+
println!("{:?}", topic);
36+
```
37+
38+
This works but is quite imperative:
39+
For every piece of data we want,
40+
we have to call a function (or method) like this.
41+
42+
It would be super neat if we could instead write a list of the data we need,
43+
and have these functions called for us.
44+
This is what the extractor pattern is all about.
45+
46+
Since this pattern makes heavy use of generic programming
47+
and introducing abstractions,
48+
we'll look at it step by step.
49+
50+
Let's start with something that looks simple:
51+
A function called `extract`.
52+
This is what we'll call internally eventually,
53+
but for now we'll do it explicitly.
54+
55+
It is not a _typical_ function, though:
56+
In addition to the blob we also need to specify a type parameter.
57+
58+
```rust
59+
let topic = extract::<Topic>(&incoming_data).unwrap();
60+
println!("{:?}", topic);
61+
62+
fn extract<E: Extractor>(input: &Blob) -> Result<E::Target, Error> {
63+
E::extract_from(input)
64+
}
65+
```
66+
67+
```rust
68+
trait Extractor {
69+
type Target;
70+
71+
fn extract_from(input: &Blob) -> Result<Self::Target, Error>;
72+
}
73+
type Error = Box<dyn std::error::Error>;
74+
```
75+
76+
```rust
77+
struct Topic;
78+
79+
impl Extractor for Topic {
80+
type Target = String;
81+
82+
fn extract_from(input: &Blob) -> Result<Self::Target, Error> {
83+
if let Some(line) = input.header.lines().next() {
84+
Ok(line.to_string())
85+
} else {
86+
Err(String::from("no topic set").into())
87+
}
88+
}
89+
}
90+
```
91+
92+
```rust
93+
let message = extract::<Json<Message>>(&incoming_data).unwrap();
94+
println!("{:?}", message);
95+
96+
#[derive(Debug, serde_derive::Deserialize)]
97+
struct Message {
98+
text: String,
99+
}
100+
101+
struct Json<T> { target: std::marker::PhantomData<T> }
102+
103+
impl<T: serde::de::DeserializeOwned> Extractor for Json<T> {
104+
type Target = T;
105+
106+
fn extract_from(input: &Blob) -> Result<Self::Target, Error> {
107+
let res = serde_json::from_slice(&input.content)?;
108+
Ok(res)
109+
}
110+
}
111+
```
112+
113+
<!--
114+
115+
Goal: `fn foo(topic: Topic, message: Json<Message>) {}`
116+
117+
Issues:
118+
- Type to impl extract on is type of parameter
119+
- Generic impls for function only with macros
90120
91-
I hope this explained the pattern well enough!
121+
-->

0 commit comments

Comments
 (0)