ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

REST APIを実行して学ぶRust

こんにちは。火村です。 ChatWork Advent Calendar 2017の7日目の記事です。

さまざまなプログラミング言語を学ぶと、違う部分や同じ部分が見えてきて、プログラミングの本質が見えてきて楽しいです。 ところで、新しいプログラミング言語に挑戦するときは、皆さんどうしていますか? 定番は「Hello, World」の出力ですが、次のステップは他の言語でやったことあること簡単なことに挑戦してみることが多いのではないでしょうか。

というわけで、Rustを使ってチャットワークAPIを実行してみました。 そこからRustの様々な要素を掴んでもらいたいなというのが今回の内容です。

はじめに

Haskellを学ぶときもそうだったのですが、簡単なサンプルでもウェブにおいてあると、入門しやすくなります。当時はあまり簡単なサンプルがなく聞く人もおらず四苦八苦したものです。 そんなわけで、ちょっとしたサンプルもWeb上に残すようにしています。

さて、プログラミング言語を試してみようとおもったとき実用的なことができると、モチベーションが上がりますよね。今時HTTPリクエストができれば沢山のことができます。 今回は人間に優しいHTTPクライアントライブラリであるreqwestというcrateををつかって、HTTPリクエストをしてみることにしました。 リクエスト先は、冒頭でも上げたようにチャットワークAPIです。

きっと簡単につくることができるはずです。

解説

簡単に終わるだろうと思って始めたのですが、作ってみたらたくさんの要素がありました。 真面目に解説すると、業務に支障をきたしそうなので、ひたすらコメントで書くことにしました。

// $ rustc --version
// rustc 1.22.1 (05e2e1c41 2017-11-22)

// 使用する crate を宣言
extern crate reqwest; // シンプルなHTTPクライアント
extern crate serde;  // シリアライズライブラリ
extern crate serde_json; // serdeでJSONを扱うライブラリ
extern crate url;

// `#[derive(Serialize, Deserialize)`を使えるようにする
#[macro_use]
extern crate serde_derive;

// HTTPライブラリのデファクトスタンダード
// header! を使うだけ
#[macro_use]
extern crate hyper;

// reqwest::Url と毎回書くのは大変なので Url にする
use reqwest::Url;
use hyper::header::Headers;

// HTTPヘッダー用の構造体を生成してくれる
header! { (XChatWorkToken, "X-ChatWorkToken") => [String] }

/// HTTPリクエスト用とのマッピング用の構造体
// POSTパラメータにあわせて、構造体定義しておけば勝手にいい感じにしてくれる。便利
#[derive(Serialize)]
struct PostMessageRequest {
    body: String, // Bodyパラメータを設定する
}

/// メッセージ投稿APIのレスポンスとのマッピング用の構造体
// 帰ってくるJSONにあわせて構造体定義しておけば勝手にいい感じにしてくれる。便利
// #[serde(untagged)] でどのようにマッピングするか指定します
// #[derive](Debug)]を書いておくとDebug出力を自動生成してくれます
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum PostMessageResponse {
    Error { errors: Vec<String> },
    MessageId { message_id: String },
}

/// PostMessageResponseのままだと使いにくいので用意
#[derive(Debug)]
struct MessageId {
    message_id: String,
}

/// post_message関数で発生するエラーを一つの型にするためのenum
// 型を合わせる必要があるため作成、文字列にしてしまう手もある
#[derive(Debug)]
enum PostMessageError {
    Reqwest(reqwest::Error),
    UrlParse(url::ParseError),
    API(Vec<String>),
}

/// post_message関数でreqwest::Errorを返す関数を呼ぶときに勝手に変換できるようにする
// PostMessageErrorにFromトレイトを実装している
impl From<reqwest::Error> for PostMessageError {
    fn from(e: reqwest::Error) -> PostMessageError {
        PostMessageError::Reqwest(e)
    }
}

/// post_message関数でurl::ParserErrorを返す関数を呼ぶときに勝手に変換できるようにする
// PostMessageErrorにFromトレイトを実装している
impl From<url::ParseError> for PostMessageError {
    fn from(e: url::ParseError) -> PostMessageError {
        PostMessageError::UrlParse(e)
    }
}

/// みんなだいすきエントリーポイント
fn main() {
    // unwrap すると Result<A,B>な型のとき Aがかえってくる Bの値をもってるときはpanicがおきる
    // ResultはいわゆるEither型
    // `left` `right`ではなく `Ok` `Err`
    // 自分が使うツールぐらいだったら Resultな型はmain関数でunwrapしています
    // unwarpはサンプルコードでよくみかけます
    let (room_id, body) = parse_args().unwrap();
    let token = env_chatwork_token().unwrap();
    // &tokenで渡せるように関数をつくらないと tokenはここで使えくなってしまう(後述)
    let response = post_message(&token, room_id, &body).unwrap();
    // {:?} を使うとデバッグ形式で出力できます
    println!("{:?}", response);
}

/// 環境変数 CHATWORK_API_TOKENから値を取り出す
fn env_chatwork_token() -> Result<std::string::String, String> {
    std::env::var("CHATWORK_API_TOKEN")
        // そのままのだとエラーの原因がよくわからないエラーメッセージを作成
        // 文字列は&strなので Stringに変換
        // &'static str のままでも値をかえせますが、今回のコードは Stringで統一しています
        // to_stringするのにはメモリアロケーションが発生するので、必要がないなら避けるべきかもしれません
        // エラーメッセージを動的に生成してしまうと、&'static strで返すことができないので、Stringに統一しています
        .map_err(|_| "CHATWORK_API_TOKEN environment variable not present".to_string())
}

/// コマンドライン引数を解析する
// u32は unsigned 32bit 整数
fn parse_args() -> Result<(u32, String), String> {
    // コマンドライン引数の取得
    let mut args = std::env::args();
    args.next(); // プログラムの名前なので無視します
    // 最初のコマンドライン引数を取得
    // Optionが返ってくるのでパターンマッチで分岐
    let room_id = match args.next() {
        Some(s) => s.parse::<u32>()
            // or で失敗したときの値を作成
            .or(Err("arg1 expected number for room_id".to_string())),
        // 最初の引数が取得できなかった場合の値を作成
        None => Err("arg1 expected room_id, found None".to_string()),
    // `?`を利用するとResult型の失敗している値の場合は、そのまま`return`
    // 成功している場合はResultの中から値を取り出せる
    // room_idはu32として利用できる
    }?;

    let body = match args.next() {
        Some(s) => s,
        // 二番目の引数を取得できなかったときの値を作成
        // s はResultではないので、Resultのままにすることはできない
        // `?`を使用してもよいけど `return` するのは明白なので、そのまま`return`しています
        None => return Err("args2 expected body, found None".to_string()),
    };
    // Resultを返さないといけないのでOkで包む
    // Rustでは最後の式が戻り値に
    // セミコロンを付けると () 型になってしまうので書かない
    Ok((room_id, body))
}

/// POSTするURLを作成する
fn post_message_url(room_id: u32) -> Result<Url, url::ParseError> {
    let url_str = format!("https://api.chatwork.com/v2/rooms/{}/messages", room_id);
    Url::parse(&url_str) // 文字列をURLに変換するのは失敗することがある
}

/// アクセストークンをセットしたHTTPヘッダーを作成する
// Stringでなくて &strにしないと関数の引数に使った変数の所有権が移動してしまって使えなくなってしまう
// tokenは何度が使いまわしたいと想像がつくので、 &str にして貸すだけにしてあげてます
// (結局to_stringメソッドでメモリアロケーションが発生していますのであんまり意味はないです)
fn chatwork_api_headers(token: &str) -> Headers {
    // headers.setは () を返すので、ワンラインではかけず…
    // setを使うので mutに
    let mut headers = Headers::new();
    headers.set(XChatWorkToken(token.to_string()));
    headers
}

/// HTTPリクエストをしてREST APIを実行してJSONに
/// Tに使える型 JSONに使える型を制限をかけているだけ
// UrlやHeaderは使いまわしたいかもしれませんが、利用しているライブラリの都合所有権を移動させてしまいます
fn request_chatwork_api<T: serde::Serialize, JSON: serde::de::DeserializeOwned>
    (url: Url,
     headers: Headers,
     body: &T)
     -> Result<JSON, reqwest::Error> {
    reqwest::Client::new()
        .post(url)
        .form(body)
        .headers(headers)
        .send()? // HTTPリクエスト (Resultが返ってくる)
        .json() // JSONに変換
}

/// request_chatwork_api をラップして使いやすく
// u32はコピーされるので関数に渡しても、その後も使いまわせます(Copyトレイトが実装されているため)
// 型の不一致がおきてしまうので、まとめてあつかえるPostMessageErrorを用意
// 静的ディスパッチでなくなってもよいなら Box<std::error::Error>を使う手もたぶんある
fn post_message(token: &str, room_id: u32, body: &str) -> Result<MessageId, PostMessageError> {
    let body = PostMessageRequest { body: body.to_owned() };
    // Err は url::ParseError ですが Fromトレイトを実装しているので、PostMessageErrorに変換してくれます
    let url = post_message_url(room_id)?;
    let headers = chatwork_api_headers(token);
    let response = request_chatwork_api(url, headers, &body)?;
    // 使いやすいように値を変換して返す
    match response {
        PostMessageResponse::Error { errors } => Err(PostMessageError::API(errors)),
        PostMessageResponse::MessageId { message_id } => Ok(MessageId { message_id: message_id }),
    } // ここで`return`しているので、`?`は使う必要はない
}

実行に必要なCargo.tomlも貼り付けておきます。

[package]
Authors = ["Tomohiko Himura <himura@chatwork.com>"]
name = "post_messach_to_chatwork_with_reqwest"
version = "0.1.0"

[dependencies]
hyper = "0.11.6"
reqwest = "0.8.1"
serde = "1.0.15"
serde_derive = "1.0.15"
serde_json = "1.0.3"
url = "1.5.1"

実際に実行したい場合はサンプルプロジェクトを用意しているので、利用してみてください。 rustのインストールはこちらなどを参考ください。

実行方法は以下の感じです。

$ CHATWORK_API_TOKEN=XXXXX cargo run -q [room_id] [投稿内容]

補足

エラーハンドリング

文字列

文字列まわり戸惑うことが最初多いです。 以下の記事などが面白かったです。

非同期リクエスト

reqwestですが、同期リクエストにみえますね。非同期リクエストをする部分はまだunstableです。

まとめ

簡単にできると思ったことが、意外と簡単ではありませんでした。

Rustの特徴的な機能に、所有権と借用というものがあります。 これらはソースコードを眺めるだけでは伝わらないのではないかと思います。 実際に書いてみて気づくことがたくさんありました。

言語の機能や使い方を見るだけでわかった気になることは多いですが、新しい発見があるので、ぜひRustを挑戦してみてはどうでしょうか。