IronでWebサービスを作りました

IronというWeb Frameworkがありまして、以前から気になっていたのですがちゃんと書けていなかったので趣味プロジェクトとして短縮URLサービスを作りました。

Ironを使って開発していくにあたって必要となったミドルウェアや書き方などを紹介していきます。

公式リポジトリこのあたりのサンプルコードを使って自分で動かしながら読んでいく方法がおすすめです。

まずはルーティング

このままでは動かないコードですが、雰囲気だけでも伝えたいので一部省略したコードを載せます。
実際にWebサービスとして必要となるルーティングを行った際、下記のような構成になりました。

main.rs
let mut index_chain = Chain::new(application::service::index_service::index_service);

let mut router = Router::new();
router.get("/", index_chain, "index");
router.post("/", application::service::shorter_service::post, "post");
router.get("/ping", application::service::ping_service::ping_service, "ping");
router.get("/:query", application::service::shorter_service::get, "query");

let mut router_chain = Chain::new(router);

let mut mount = Mount::new();
mount.mount("/", router_chain);
mount.mount("/assets/", Static::new(Path::new("./assets")));

let addr: String = format!("localhost:{}", 8080i32).parse().unwrap();
Iron::new(mount).http(addr).unwrap();

短縮URLサービスなのでトップページはもともとなかったのですが、トップページを表示させるためだけに index_chain を用意しました。Chainとは、ページを表示させるロジックの前後に例えばページの読み込みを開始した時間の計測と読み込みを完了した時間の記録といった機能を入れたい場合など、処理毎に束ねることができる機能です。

続いてRouter::new(); で行っていることは getやpostなどの実際のルーティングです。直接任意のメソッドを呼んだり、チェインを実行したりすることができるようです。こちらはミドルウェアなので router が必要になってきます。

Mount::new(); は、ディレクトリのマウント機能を有したミドルウェアで staticfileというミドルウェアと組み合わせることで静的ファイルをマウントできるようになります。mountを最終的な親のルーティングにしてIronに渡してあげないといけない所が詰みポイントでした。

Chainについて深掘り

公式のサンプルコードを少しファイルを分けただけですが、こちらを読んでいきます。

hello_world.rs
use iron;
use iron::prelude::*;


pub fn hello_world(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Hello World")))
}
main.rs
extern crate iron;
use iron::prelude::*;
use iron::{BeforeMiddleware, AfterMiddleware, typemap};
use iron::status;

struct ResponseTime;
impl typemap::Key for ResponseTime { type Value = u64; }

impl BeforeMiddleware for ResponseTime {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        req.extensions.insert::<ResponseTime>(precise_time_ns());
        Ok(())
    }
}

impl AfterMiddleware for ResponseTime {
    fn after(&self, req: &mut Request, res: Response) -> IronResult<Response> {
        let delta = precise_time_ns() - *req.extensions.get::<ResponseTime>().unwrap();
        println!("Request took: {} ms", (delta as f64) / 1000000.0);
        Ok(res)
    }
}

let mut chain = Chain::new(hello_world);
chain.link_before(ResponseTime);
chain.link_after(ResponseTime);

ResponseTime structをベースとして、hello_world が呼ばれる前と呼ばれる後に処理を追加しているようです。
ResponseTimeは元はただのstructですが、iron側で用意されている BeforeMiddlewareAfterMiddlewareをimplして実際の処理を書いているようです。

ちょっと気になったのは、 req.extensions.insert。こちらは、typemap::Keyをimplすることによって元々ResponseTimeは何もない空のstructだったのですが、typemap によって値を持つことが出来るようです。

typemapのgithubを見ると、すぐにサンプルを確認することが出来るようになっていて、

 extern crate typemap;
use typemap::{TypeMap, Key};

struct KeyType;

#[derive(Debug, PartialEq)]
struct Value(i32);

impl Key for KeyType { type Value = Value; }

#[test] fn test_pairing() {
    let mut map = TypeMap::new();
    map.insert::<KeyType>(Value(42));
    assert_eq!(*map.get::<KeyType>().unwrap(), Value(42));
}

実際のtypemapの挙動を確認することが出来ます。structの中にデータを持っているわけではなく、store という 一つのハッシュマップの中にデータを集めて、structの型をキーとして状態を保持する仕組みのようです。
ironでは内部でこのようにしてChainを実現させているようです。

Router

こちらも公式リポジトリのサンプルコードを持ってきています。

extern crate iron;
extern crate router;

use iron::prelude::*;
use iron::status;
use router::Router;

fn main() {
    let mut router = Router::new();
    router.get("/", handler, "index");
    router.get("/:query", handler, "query");
    Iron::new(router).http("localhost:3000").unwrap();
    fn handler(req: &mut Request) -> IronResult<Response> {
        let ref query = req.extensions.get::<Router>().unwrap().find("query").unwrap_or("/");
        Ok(Response::with((status::Ok, *query)))
    }
}

routerでは、:query で任意のURLにマッチさせることができるなどよくあるRouterの実装になっているようです。
Routerという型をキー内にqueryが入っていて取り出すことができるようです。こうして見ると、typemapに依存している部分がとても多いですね。

Mountstaticfile

こちらもMountの公式リポジトリからコードを持ってきます。

fn send_hello(req: &mut Request) -> IronResult<Response> {
    println!("Running send_hello handler, URL path: {:?}", req.url.path());
    Ok(Response::with((status::Ok, "Hello!")))
}

fn intercept(req: &mut Request) -> IronResult<Response> {
    println!("Running intercept handler, URL path: {:?}", req.url.path());
    Ok(Response::with((status::Ok, "Blocked!")))
}

fn main() {
    let mut mount = Mount::new();
    mount.mount("/blocked/", intercept).mount("/", send_hello);

    Iron::new(mount).http("localhost:3000").unwrap();
}

MountはRouterほど細かな制御をせず、サブディレクトリの粒度でざっくりとハンドラに渡すもののようです。
シンプルかつ一番強力なミドルウェアです。なので、mountの中にrouterを入れました。 routerの中にmountを入れたらうまくいきませんでした。

そしてstaticfileは、

let mut mount = Mount::new();
mount.mount("/", Static::new(Path::new("target/doc/")));
mount.mount("/doc/", Static::new(Path::new("target/doc/staticfile/")));
mount.mount("/src/", Static::new(Path::new("target/doc/src/staticfile/lib.rs.html")));
Iron::new(mount).http("127.0.0.1:3000").unwrap();

このように、ディレクトリやファイルをそのままマウントできるようです。
nginxやapacheが必要なくなります。短縮URLサービスではパスの衝突をロジックで避ける上でアプリケーションで静的したかったのでこのようにしました。

テンプレート

Web frameworkで欠かすことの出来ない存在テンプレート。HTMLなどのファイルに変数を埋め込み書き換えてくれる機能です。

main.rs
let mut index_chain = Chain::new(application::service::index_service::index_service);
let mut hbse = HandlebarsEngine::new();
hbse.add(Box::new(DirectorySource::new("./templates/", ".hbs")));
if let Err(r) = hbse.reload() {
    panic!("{}", r);
}

こちらは、HandlebarsEngine で実現します。
実際のテンプレートのサンプルコードはこのあたりにあります。

hbse.add でやっていることは、テンプレートが置かれているディレクトリを指定してあげています。

そして実際にテンプレートエンジンを使った例が下記の通りです。

use iron;
use iron::prelude::*;
extern crate staticfile;
extern crate mount;
use self::mount::Mount;
use self::staticfile::Static;
use std::path::Path;
use hbs;
use hbs::Template;
use iron::prelude::*;
use hbs::handlebars::to_json;
use std;
use serde_json;
use serde_json::{Value, Map};

mod data {
    use hbs::handlebars::to_json;
    use serde_json::value::{Value, Map};

    #[derive(Serialize, Debug)]
    pub struct Team {
        name: String,
        pts: u16,
    }

    pub fn make_data() -> Map<String, Value> {
        let mut data = Map::new();

        data.insert("year".to_string(), to_json(&"2015".to_owned()));

        let teams = vec![Team {
            name: "Jiangsu Sainty".to_string(),
            pts: 43u16,
        },
                         Team {
                             name: "Beijing Guoan".to_string(),
                             pts: 27u16,
                         },
                         Team {
                             name: "Guangzhou Evergrand".to_string(),
                             pts: 22u16,
                         },
                         Team {
                             name: "Shandong Luneng".to_string(),
                             pts: 12u16,
                         }];

        data.insert("teams".to_string(), to_json(&teams));
        data.insert("engine".to_string(), to_json(&"serde_json".to_owned()));
        data
    }
}
use self::data::*;


pub fn index_service(_: &mut Request) -> IronResult<Response> {
    let mut resp = Response::new();
    let data = make_data();
    // resp.set_mut(Template::new("index", data)).set_mut(iron::status::Status::Ok);
    resp.set_mut(Template::with(include_str!("../../templates/index.hbs"), data))
        .set_mut(iron::status::Status::Ok);
    Ok(resp)
}

extern と use 部分整理できていないので調整してください。:pensive:
ほぼ公式のサンプルのままです。サンプルデータを modを使って名前空間を分けてあるのが凄くいいなと思いました。serde_jsonの扱いもスッキリします。
大きなMapを読み込ませてテンプレートに反映する仕組みのようです。

シングルトンインスタンス

短縮URLサービスを作る上で、データの保持にdynamodbを使ったのですがdynamodbをrustを扱う場合

infrastructure/resource/shorter_resource.rs
extern crate rusoto;
extern crate hyper;

use std::default::Default;

use self::rusoto::{DefaultCredentialsProvider, Region};
use self::rusoto::dynamodb::{DynamoDbClient, ListTablesInput};
use self::rusoto::default_tls_client;
use self::rusoto::ProvideAwsCredentials;
use self::rusoto::DispatchSignedRequest;
use self::rusoto::AwsCredentials;
use std;
use maplit;

pub struct ShorterResource{
    pub client: rusoto::dynamodb::DynamoDbClient<rusoto::BaseAutoRefreshingProvider<rusoto::ChainProvider, std::cell::RefCell<rusoto::AwsCredentials>>, hyper::client::Client>,
    pub list_tables_input: ListTablesInput,
}

impl ShorterResource {
    pub fn new() -> ShorterResource {
        let provider = DefaultCredentialsProvider::new().unwrap(); // aws configure
        ShorterResource {
            client: DynamoDbClient::new(default_tls_client().unwrap(), provider, Region::ApNortheast1),
            list_tables_input: Default::default()
        }
    }
}

このような感じで、インスタンス生成時のオーバーヘッドが大きくリクエストの度に作っていては重いのでシングルトンにして使いまわしたいと考えました。
本来であればコネクションプールや非同期まで考えないといけない所ですが趣味開発なので気にしないでおきます。

シングルトンインスタンスですが、こちらのstack overflowの投稿を参考にさせて頂きました。

#[derive(Clone)]
struct SingletonReader {
    inner: Arc<Mutex<resource::shorter_resource::ShorterResource>>
}
static mut SINGLETON: *const SingletonReader = 0 as *const SingletonReader;
static ONCE: Once = ONCE_INIT;
fn common_singleton() -> SingletonReader {
    let shortResourceSingletonReader = unsafe {
        ONCE.call_once(|| {
            let singleton = SingletonReader {
                inner: Arc::new(Mutex::new(resource::shorter_resource::ShorterResource::new()))
            };
            SINGLETON = mem::transmute(Box::new(singleton));
        });
        (*SINGLETON).clone()
    };
    shortResourceSingletonReader
}

あとは、お好みのハンドラで let shortResourceSingletonReader = common_singleton(); のようにしてインスタンスを取得するだけです。
Mutexを使っているので、スレッドが増えてもここでロックが発生してしまうのが惜しいところです。

最後に

Ironはミドルウェアが細かく別れていて、癖とかもそこまでなさそうなので(typemapをうまく使いこなせる気がしないですが)、要点を抑えればサクサクと開発していけそうな感じがしました。

1473683947