この記事では、コンパイルが速い!バイナリ実行!シンタックスがかっこいい!と3拍子揃ったRustでWebサービスを作成します。
HTTPハンドラの部分はIronというフレームワークを使います。
Ironはルーティング等のコア機能といくつかのプラグイン(jsonパーザ等)という素軽い構成のWebフレームワークです。
hyperというもっとプリミティブなフレームワークにビルトインする形で作られています。
- 0. わたしの開発環境
- 1. プロジェクトの作成
- 2. HTTPサービスを実行する
- 3. ルーティングする
- 4. 空のJSONを返す
- 5. 構造体をシリアライズしたJSONを返す
- 6. 1px透過GIFを返す
- 7. ルーティングを別モジュールにする
ソースコードはGitHubで公開しています。
https://github.com/x1-/rust_web_service
0. わたしの開発環境
- OS ・・・ MacOSX 10.10.5
- rust ・・・ 1.14.0
- ビルドシステム ・・・ Cargo
- IDE ・・・ emacs + racer + company
rustのインストールは公式サイトに書いてある通りにするのが一番良さそうです。
rustupがcargoのセットアップからrustcのインストールまで行ってくれます。
※2017.01.05 追記) Rust Version Manager, rsvmの導入を検討しても良いかもしれません。
1. プロジェクトの作成
Cargoを使ってWebサービス用のプロジェクト rust_web_service を作成します。
$ cargo new rust_web_service --bin $ cd rust_web_service $ tree -a . ├── .git │ ├── HEAD │ : ├── .gitignore ├── Cargo.toml └── src └── main.rs
cargo new は git init も同時に行ってくれます。
Cargo.toml , main.rs にはそれぞれ次のように初期コードが出力されます。
わかりやすいですね。
Cargo.toml
[package] name = "rust_web_service" version = "0.1.0" authors = ["x1- <[email protected]>"] [dependencies]
main.rs
fn main() { println!("Hello, world!"); }
main()関数はバイナリのランタイムに実行される関数です。
このまま特に編集しないで実行できます。
$ cargo run Compiling rust_web_service v0.1.0 (file:///Users/xxxx/repos/rust_web_service) Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs Running `target/debug/rust_web_service` Hello, world!
cargoによりコンパイル&実行されてコンソールに Hello World! が表示されます。
2. HTTPサービスを実行する
Ironフレームワークを使ってHTTPサービスを実行します。
Cargo.toml にIronへの依存を追加します。
[package] name = "rust_web_service" version = "0.1.0" authors = ["x1- <[email protected]>"] [dependencies] [dependencies.iron] version = "*"
Iron に用意されている examples/hello.rs を参考に main.rs を変更します。
main.rs
extern crate iron; use iron::prelude::*; use iron::status; fn main() { Iron::new(|_: &mut Request| { Ok(Response::with((status::Ok, "Hello world!"))) }).http("localhost:3000").unwrap(); }
これを実行するとポート3000番にバインドしてHTTPサーバが起動します。
ブラウザから http://localhost:3000 にアクセスすると、 Hello world! が表示されます。
Iron::new() の中の |_: &mut Request| {..} はクロージャです。
Rustのクロージャはパイプ(|)の間に書きます。
see: クロージャ|プログラミング言語Rust
3. ルーティングする
エンドポイントを増やせるようにルーティングします。
ルーティングも examples/simple_routing.rs を参考に、 main.rs に書いていきます。
extern crate iron; use std::collections::HashMap; use iron::prelude::*; use iron::{Handler}; use iron::status; struct Router { // キーにパス、値にハンドラを取るHashMap。 routes: HashMap<String, Box<Handler>> } impl Router { fn new() -> Self { Router { routes: HashMap::new() } } fn add_route<H>(&mut self, path: String, handler: H) where H: Handler { self.routes.insert(path, Box::new(handler)); } } impl Handler for Router { fn handle(&self, req: &mut Request) -> IronResult<Response> { match self.routes.get(&req.url.path().join("/")) { Some(handler) => handler.handle(req), None => Ok(Response::with(status::NotFound)) } } } fn main() { let mut router = Router::new(); router.add_route("hello".to_string(), |_: &mut Request| { Ok(Response::with((status::Ok, "Hello world !"))) }); router.add_route("error".to_string(), |_: &mut Request| { Ok(Response::with(status::BadRequest)) }); let host = "localhost:3000"; println!("binding on {}", host); Iron::new(router).http(host).unwrap(); }
また cargo run を実行します。
今度はブラウザから http://localhost:3000/error にアクセスすると 400 Bad Request が返るようになります。
4. 空のJSONを返す
次は空のJSONを返すエンドポイントを作成します。
/json にアクセスすると空のjsonが返るようにします。
use iron::headers::ContentType;
を追加して、
router.add_route(“hello”.to_string()… の部分を変更します。
main.rs
extern crate iron; use std::collections::HashMap; use iron::prelude::*; use iron::{Handler}; use iron::status; // ↓ ここを追加 use iron::headers::ContentType; ~省略~ // hello から json に変更 router.add_route("json".to_string(), |_: &mut Request| { Ok(Response::with((ContentType::json().0, status::Ok, "{}"))) });
cargo run を実行します。
ブラウザから http://localhost:3000/json にアクセスすると空のjsonが返ります。
examples/content_type.rs にあるように、 Content-Type はいろいろな方法で指定できるようです。
json を出力するなら ContentType::json().0 を使うのが一番スマートに思いますが、 image/gif のように予めメソッドが用意されていない Content-Type を使いたいときは mime!(Image/gif); を使うのが直感的に感じました。
5. 構造体をシリアライズしたJSONを返す
空のJSONを返しても意味が無いので(笑)、構造体をシリアライズしたJSONを返すように修正します。
構造体のシリアライズにはrustc_serialize を使うので、まずは Cargo.toml を変更します。
Cargo.toml
[package] ~省略~ [dependencies] rustc-serialize = "*" [dependencies.iron] version = "*"
main.rs に Letter という構造体を作成します。
main.rs
extern crate iron; extern crate rustc_serialize; use iron::status; use iron::headers::ContentType; use iron::prelude::*; use rustc_serialize::json; // 構造体 // #[derive(RustcEncodable)] ≒ シリアライズ可能属性(rustc_serializeのattribute) #[derive(RustcEncodable)] pub struct Letter { title: String, message: String } ~省略~
ついでに、 fn main() 内で add_route していたクロージャも 名前付き関数に切り出します。
main.rs
extern crate iron; extern crate rustc_serialize; use iron::status; use iron::headers::ContentType; use iron::prelude::*; use rustc_serialize::json; #[derive(RustcEncodable)] pub struct Letter { title: String, message: String } struct Router { ~省略~ } fn json(_: &mut Request) -> IronResult<Response> { let letter = Letter { title: "PPAP!".to_string(), message: "I have a pen. I have an apple.".to_string() }; let payload = json::encode(&letter).unwrap(); Ok(Response::with((ContentType::json().0, status::Ok, payload))) } fn bad(_: &mut Request) -> IronResult<Response> { Ok(Response::with(status::BadRequest)) } fn main() { let mut router = Router::new(); router.add_route("json".to_string(), json); router.add_route("error".to_string(), bad); let host = "localhost:3000"; println!("binding on {}", host); Iron::new(router).http(host).unwrap(); }
rustc_serialize::json の encode メソッドに構造体を渡すだけでシリアライズできます。
cargo run を実行してブラウザから http://localhost:3000/json にアクセスすると今度はシリアライズされたJSONが返ります。
ところで、 _: &mut Request の _ ですが、これは名前無し引数を表します。
インターフェイスとして Request を引数にとりますが、関数内では使用していないのでこんなことをしています。
&mut はミュータブル参照です。
ミュータブル参照に束縛された変数は変化しうります。
ミュータブル参照とは何ぞや、とかRustの超重要概念である参照・借用 については プログラミング言語Rust をご一読頂くのが良いかと思います。
6. 1px透過GIFを返す
Webサービスあるあるで、1px透過GIFを返すエンドポイントも作成します。
今度は mime クレイトを使うので、また Cargo.toml を変更します。
Cargo.toml
[package] ~省略~ [dependencies] mime = "*" rustc-serialize = "*" [dependencies.iron] version = "*"
main.rs に mime クレイトと rustc_serialize::base64 を追加します。
mime は Gifの Content-Type を指定するために使います。
rustc_serialize::base64 は 1px透過GIFのBase64文字列をデコードするために使います。
main.rs
#[macro_use] extern crate mime; extern crate iron; extern crate rustc_serialize; use iron::status; use iron::headers::ContentType; use iron::prelude::*; use rustc_serialize::base64::FromBase64; use rustc_serialize::json;
/gif エンドポイントを追加します。
main.rs
~省略~ fn bad(_: &mut Request) -> IronResult<Response> { Ok(Response::with(status::BadRequest)) } fn gif(_: &mut Request) -> IronResult<Response> { // 1px透過GIF文字列 let px1 = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; Ok(Response::with((mime!(Image/Gif), status::Ok, px1.from_base64().unwrap()))) } fn main() { let mut router = routing::Router::new(); router.add_route("json".to_string(), json); // /gifを追加 router.add_route("gif".to_string(), gif); router.add_route("error".to_string(), bad); let host = "localhost:3000"; println!("binding on {}", host); Iron::new(router).http(host).unwrap(); }
cargo run を実行してブラウザから http://localhost:3000/gif にアクセスするとgifが返ります。
rustc_serialize::base64::FromBase64 トレイトの from_base64() メソッドは、 str と u8に対して実装されています。( str と u8 で使えると思って頂ければと)
で、 Result
↑の例では px1.from_base64().unwrap() と、特に注意を払わず Vec
※ この例では px1 が デコードできることは変化しないので、 unwrap() で十分ですが。
7. ルーティングを別モジュールにする
最後に、ルーティングの部分を別モジュールに切り出します。
routing.rs を作成します。
routing.rs
extern crate iron; use std::collections::HashMap; use iron::{Handler}; use iron::status; use iron::prelude::*; pub struct Router { // Routes here are simply matched with the url path. routes: HashMap<String, Box<Handler>> } impl Router { pub fn new() -> Self { Router { routes: HashMap::new() } } pub fn add_route<H>(&mut self, path: String, handler: H) where H: Handler { self.routes.insert(path, Box::new(handler)); } } impl Handler for Router { fn handle(&self, req: &mut Request) -> IronResult<Response> { match self.routes.get(&req.url.path().join("/")) { Some(handler) => handler.handle(req), None => Ok(Response::with(status::NotFound)) } } }
main.rs で routing モジュールを使うように変更します。
main.rs
~省略~ use rustc_serialize::json; // ↓この行を追加 mod routing; #[derive(RustcEncodable)] pub struct Letter { ~省略~ fn main() { // router = Router::new() から router = routing::Router::new() に変更 let mut router = routing::Router::new(); router.add_route("json".to_string(), json); ~省略~ }
今回は以上になります。
rust によるWebサービスはもっと深掘りしてみたいと思います。