この記事では、コンパイルが速い!バイナリ実行!シンタックスがかっこいい!と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- <viva008@gmail.com>"] [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- <viva008@gmail.com>"] [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サービスはもっと深掘りしてみたいと思います。





