###### tags: `Rust` `Rocket` `Request`
# Rocket Framework 入門 1
Rocketとは
- Rustで書かれたweb framework
- 書きやすい
- 速い
- 安全
- 柔軟性や使いやすさ、型安全を犠牲にしない
# Get Started
Nightly Rustでしか動作しない
`rustup default nightly`
開発用ツールもインストールしとく
`rustup component add rls rust-src rust-analysis`
Cargo.toml にrocketを書く
```toml
[dependencies]
rocket = "0.4.2"
```
# Hello, world
```rust=
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
```
解説
- #![feature()] : nightlyの機能を有効にするのに使う
- #[macro_use] : extern crate の上で使うと、そのクレートのマクロをロードする
- #[get()] : routeアトリビュート。ハンドラのルートを決める
- rocket::
- ignite() : Rocketオブジェクトを作成して返す
- mount("namespace", [handlers...]) : リクエストハンドラーをマウントする。
- launch() : サーバーのlistenを開始
ロケットに積荷を搭載して発射するイメージ
# 概要
## ライフサイクル
httpリクエストに対する処理の流れ
1. ルーティング : urlに対するリクエストをmountされたハンドラに渡す
2. validation : リクエストのタイプがあっているか検証。あっていなかったら、次のルートに行くかエラーハンドラーを呼ぶ
3. processing : ハンドラに定義したビジネス路切っくを実行
4. Response : 適切なhttpレスポンスを返す。
## Namespacing
mod 機能を使って、ハンドラーをモジュール化するときは
module名まで含めないとエラーになる。
route!マクロがコード生成できないため
```rust=
mod other {
#[get("/world")]
pub fn world() -> &'static str {
"Hello, world!"
}
}
#[get("/hello")]
pub fn hello() -> &'static str {
"Hello, outside world!"
}
use other::world;
fn main() {
// error[E0425]: cannot find value `static_rocket_route_info_for_world` in this scope
rocket::ignite().mount("/hello", routes![hello, world]);
}
// こっちで書く
rocket::ignite().mount("/hello", routes![hello, other::world]);
```
# Requests
全部訳すとだるいので要点だけ
## route attr
- #[get()]などのrouteアトリビュートでパスを記述
- get
- put
- post
- delete
- head
- patch
- option
## Dynamic Path
- Dynamic Path は、パスの一部が任意の引数になる
- "/hello/\<name>"みたいに書く
- routeアトリビュートに書いたあと、fn の引数に&RawStr型として書く
```rust=
#[get("/hello/<name>")]
fn hello(name: &RawStr) -> String {
format!("Hello, {}!", name.as_str())
}
```
- 何個でも書ける
- pathのパラメータとして取れる型は、rocket::request::FromParamトレイトによれば
- i/u 8/15/32/64/128
- bool
- IpAddr, Ipv4Addr, Ipv6Addr
- SocketAddr, SocketAddrV4, SocketAddrV6
- &RawStr : デコードされていない生の文字列
- String : デコードされた文字列
- Cow
- Option\<T> where T:FromParam
- Result\<T, T::Error> where T: FromParam
- 標準ライブラリにあるデータ型はだいたいパスパラメータとして取れる
- Rocketだけの特殊な型もある
- ハンドラの引数に型を書くことで、自動的にParseとValidateを行う
```rust=
#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String { ... }
```
- 合わなかったら、次の優先度のハンドラへ
- ハンドラの優先度は、mounte()のroutes!マクロの左順
## Fowarding
- パスのパラメータで取った値が合わなかったら、次にマッチしたrouteにリクエストを回す
- routeがなくなるまで続ける
- なくなったら、404errorを返す
## routeのランク
同じようなrouteがあった場合、ランクが高い方を優先してアクセスする。
routeアトリビュートに、rank = で明示できる
```rust=
#[get("/user/<id>")]
fn user(id: usize) -> T { ... }
#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) -> T { ... }
#[get("/user/<id>", rank = 3)]
fn user_str(id: &RawStr) -> T { ... }
fn main() {
rocket::ignite()
.mount("/", routes![user, user_int, user_str])
.launch();
}
```
### デフォルトランキング
-6 ~ -1まで
変数を含んでいなかったり、? から始まるクエリパラメータを
含んでいると高くなるというルール。
## static file
静的ファイルのホスティングは、以下のコードでできる。
```rust=
rocket.mount("/public", StaticFiles::from("/static"))
```
/static/ フォルダにあるファイルを /public/path
でアクセスできるようにする。
## Option パラメータ
あってもなくてもいいパスのときは、ハンドラで
Option型で受け取る。
```rust=
#[get("/hello?wave&<name>")]
fn hello(name: Option<&RawStr>) -> String {
name.map(|name| format!("Hi, {}!", name))
.unwrap_or_else(|| "Hello!".into())
}
```
## 複数セグメント
?...&...&...のような、複数クエリを取って
#[derive(FromForm)] を実装した構造体に割り当てられる
```rust=
use rocket::request::Form;
#[derive(FromForm)]
struct User {
name: String,
account: usize,
}
#[get("/item?<id>&<user..>")]
fn item(id: usize, user: Form<User>) { /* ... */ }
```
url例
`/item?id=100&name=sandal&account=400`
## リクエストガード
リクエストに含まれる情報に基づいて、誤ってハンドラを
呼び出さないようにする。
もっと詳しく言えば、リクエストガードは任意の検証ポリシーである
FromRequestトレイトとして実装する。
リクエストガードはハンドラへの入力として扱われる。
Rocketはハンドラを呼び出す前に、リクエストガードの実装を自動的に呼び出す。
すべてのガードを通過したとき、ハンドラにリクエストをディスパッチする。
下記のハンドラでは、A, B, Cの3つのリクエストガードを使う(そんな名前のないけど)
```rust=
#[get("/ <param>" )]
fn index (param:isize 、a:A 、b:B 、c:C ) -> ... { ... }
```
いくつかのFromRequest実装は、Rocketに組み込みで用意されている
- Method : 現在のリクエストからhttpメソッドを抽出
- &Origin : リクエストの原点のURI
- &Route
- Cookies
- ContentType : ContentTypeを抽出する。指定されないときはリクエストが次のrouteに行く
- SocketAddr : SocketAddrを抽出する。指定されないときはリクエストが次のrouteに行く
- Option
- Result
### リクエストガードの使いみち
主に、httpリクエストのヘッダに含まれる情報を抽出するのに使える
- ヘッダに認証コードを含んで、API認証
- APIkey
### カスタムリクエストガード
実装方法はrocketのapiドキュメントの rocket::request::FromRequest トレイト
の実装例を見る
### 簡単な認証システムの実装
User と AdminUser の2つのリクエストガードを使って、
管理ユーザーしか見られないページを実装する
(User と AdminUser はカスタムリクエストガード)
```rust=
#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
"Hello, administrator. This is the admin panel!"
}
#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
"Sorry, you must be an administrator to access this page."
}
#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
Redirect::to("/login")
}
```
### Cookies
```rust=
use rocket::http::Cookies;
#[get("/")]
fn index(cookies: Cookies) -> Option<String> {
cookies.get("message")
.map(|value| format!("Message: {}", value))
}
```
# Format
routeアトリビュートは、リクエストが送ってくるデータフォーマットを定義できる
payload を設定できる put, post, delete, patchメソッドでの
Jsonリクエストの場合は format="application/json" となる
```rust=
#[post("/user", format = "application/json", data = "<user>")]
fn new_user(user: Json<User>) -> T { ... }
```
dataは \<user> パラメータで受け取っているので、引数では user: Json\<T> 型で
受け取れる
**また、 "application/json" の部分は、 "json" と省略もできる**
利用できる略称のリストは、 (rocket::http::ConentType::parse_flexible())[https://api.rocket.rs/v0.4/rocket/http/struct.ContentType.html#method.parse_flexible]
の説明を見る
payloadがないタイプのメソッドでも、HttpレスポンスのAcceptの値がjsonに
なってるかどうかをチェックする。
ハンドラの戻り値がjsonである必要があるならformatを記述するべき
```rust=
#[get("/user/<id>", format = "json")]
fn user(id: usize) -> Json<User> { ... }
```
## Bodyデータのの受け取り
routeアトリビュートで data="\<変数名>" にして ハンドラの同名引数で受け取る
FromDataトレイトを実装する型で受け取れる
```rust=
#[post("/", data = "<input>")]
fn new(input: T) -> String { ... }
```
# Forms
httpのformで入力したキーバリューを含んだpostの場合、
#[derive(FromForm)]を実装した構造体に、formの値を割り当てられる
```rust=
#[derive(FromForm)]
struct Task {
complete: bool,
description: String,
}
#[post("/todo", data = "<task>")]
fn new(task: Form<Task>) -> String { ... }
```
Form\<T> のTに、構造体の型を入れる
formの値は、Task構造体にパースされ
ハンドラの中で使えるようになる。
パースできなかったり不正な値のときは
400 - Bad Request or 422 - Unprocessable Entity
Option型やResult型でも受け取れる
```rust=
#[post("/todo", data = "<task>")]
fn new(task: Option<Form<Task>>) -> String { ... }
```
# Lenient Parsing
lenient = ゆるい、寛大な
Form\<T>型で受け取ると、formのキーと値のペアが割り当てる構造体に
過不足ない状態でないとエラーになる
LenientForm\<T>で受け取ると、フィールドが多すぎる場合は問題なく
必要な部分だけパースするといったことを行ってくれる
例.
- 構造体のフィールド : a, c
- フォームの入力 : a, b, c
こういう場合はok
```rust=
#[derive(FromForm)]
struct Task { .. }
#[post("/todo", data = "<task>")]
fn new(task: LenientForm<Task>) { .. }
```
## フィールドのリネーム
structのフィールドについて、Rustのプログラム上で使う名前と
Form\<T>などで割り当てられる名前が一致しない場合、
Form割当のとき使う名前をアトリビュートで設定できる。
```rust=
#[derive(FromForm)]
struct External {
#[form(field = "type")]
api_type: String
}
```
# Formのバリデーション
1. 構造体のフィールドの型を独自の1要素タプル構造体にする
2. 独自のタプル構造体に対して、FromFormValue トレイトを実装する。
- from_form_value()メソッドはFormデータを構造体に割り当てるときに呼ばれる。
```rust=
struct AdultAge(usize);
impl<'v> FromFormValue<'v> for AdultAge {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<AdultAge, &'v RawStr> {
match form_value.parse::<usize>() {
Ok(age) if age >= 21 => Ok(AdultAge(age)),
_ => Err(form_value),
}
}
}
#[derive(FromForm)]
struct Person {
age: AdultAge
}
```
もしもInvalidなら、ハンドラが呼ばれない
FromFormValueトレイトは、enum型にも実装できる
その際は#[derive(FromFormValue)]でいける
Valiantに含まれる値以外ならinvalid
```rust=
#[derive(FromFormValue)]
enum MyValue {
First,
Second,
Third,
}
```
# Jsonの取扱い
```rust=
#[derive(Deserialize)]
struct Task {
description: String,
complete: bool
}
#[post("/todo", data = "<task>")]
fn new(task: Json<Task>) -> String { ... }
```
構造体に、#[derive(Deserialize)] を実装すると
httpリクエストのbodyのjson文字列から、構造体に直せる
Json type は rocket_contriveに含まれる
Deserializeトレイとは Serde というクレートに依存している。
githubのrocket getstarted json example をみる
# Streaming
streaming (流れるように送られてくる)データを扱いたい場合
Rocketが用意している Data 型を使う
format は "plane"
```rust=
#[post("/upload", format = "plain", data = "<data>")]
fn upload(data: Data) -> io::Result<String> {
data.stream_to_file("/tmp/upload.txt").map(|n| n.to_string())
}
```
## 注意 : streaming data は必ず take()を使う
DoS攻撃を防止するためにも、受信するデータ量は制限する必要がある。
take()メソッドを使うのが簡単
```rust=
data.open().take(LIMIT)
```
# Errorキャッチャー
ルーティングは失敗する可能性をはらんでいる
- レスポンスガード
- matchするrouteがない
- Responderが失敗する
これらが発生したら、Rocketはエラーをクライアントに返す。
その時、httpステータスコードに応じてcatcherハンドラーを実行する。
デフォルトで設定されているが、書き換えることができる。
```rust=
#[catch(404)]
fn not_found(req: &Request) -> T { .. }
#[catch(404)]
fn not_found(req: &Request) -> String {
format!("Sorry, '{}' is not a valid path.", req.uri())
}
// 設定したら、registerメソッドでRocketオブジェクトに関数を登録する
rocket::ignite().register(catchers![not_found])
```
- register() : chartcherのリストを登録する
- catcher![] : マクロ