Overview
こんにちは pon です。これはRust Advent Calendar 2020 の記事です。
初心者がHTTPクライアントCLIをRustで書いて、実務で利用したので、実装方法を紹介します(ほとんどライブラリの紹介になる気がするが...)。Rustで何か作ってみたい人の足がかりになると思います。
作ったやつ
社内のAPIを叩くので実際のコードは公開できませんが、どんな感じのツールかを共有します。テキストからキーワード一覧を取得して、そのキーワードごとに検索エンジンが何件返すかを調べる簡単なツールです。
これを作るのに使ったライブラリを紹介します。これらを使うとRustでも簡単にHTTPクライアントCLIを作成できます。
HTTPクライアント: surf
surfはHTTPクライアントライブラリです。async/await に対応した HTTP クライアントです。非同期処理ランタイムとしてはasync_std を利用しています。
非同期処理ランタイムごとに素晴らしいまとめがあるこちらの記事がおすすめです。 2019 年の非同期 Rust の動向調査
例えばGETリクエストをしたい場合は下記のように書きます。(これは日本語でsurfを紹介してくれている@helloyuki_さんの記事の引用です。)
extern crate surf;
#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut res = surf::get("https://httpbin.org/get").await?;
dbg!(res.body_string().await?);
Ok(())
}
これだけでhttpbinに対してGETリクエストが放てます。今回はレスポンスのJSONを構造体にparse処理して結果を集計したかったのでrecv_json()
を利用します。構造体には Serialize/Deserialize traitを実装するためのマクロを書いておきます。
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
#[derive(Serialize, Deserialize)]
struct Res {
total: u32
}
そうするとrecv_json()
でレスポンスが構造体で受けれるようになります。今回は検索ヒット数だけが欲しのでクエリに対してヒット数をprintしてみます。
let Res {total} = surf::get("https://example.com").recv_json().await?;
println!("{},{}", total, s);
検索クエリをセットしたい場合はset_query
を使います。Query構造体を用意してSerialize/Deserialize
traitを実装させてそれを利用するだけです。
#[derive(Serialize, Deserialize)]
struct Query {
query: String
}
let query = Query { query: "コロナ" };
let Res {total} = surf::get("https://example.com")
.set_query(&query)?
.recv_json().await?;
println!("{},{}", total, s);
また、今回はBasic認証のかかったテスト用APIへのコールだったのでHTTP Headerをつける必要があります。Headerは.set_header()
です。なおBaisc認証ではBase64エンコードした文字列を渡すのでbase64::encode
を利用します。
extern crate base64;
use base64::encode;
// ...
let b = format!("{}:{}", u, p);
let e = format!("Basic {}", encode(b)); //Base64 エンコード
let query = Query { query: s.to_string() };
let Res {total} = surf::get("https://example.com")
.set_header("Authorization", &e)
.set_query(&query)?
.recv_json().await?;
println!("{},{}", total, s);
これで目的のBasic認証込みの、APIクライアントができました。これをCLIとして利用できるようにします。
CLIライブラリ: clap
clapはRustのためのコマンドラインパーサーです。
いろんな書き方ができますが僕は下記のBuilder patternを使った設定記述が好きです。 clap using Builder Pattern
例えば今回の用途で言うとこんな感じになります。
extern crate clap;
use clap::{App, Arg};
// ...
let app = App::new("rust-http-cli")
.version("0.1.0")
.author("po3rin")
.about("get search his")
.arg(Arg::new("user")
.about("basic auth user")
.short('u')
.long("user")
.takes_value(true) // 値をとるフラグかどうか
)
.arg(Arg::new("pass")
.about("basic auth pass")
.short('p')
.long("pass")
.takes_value(true)
)
.arg(Arg::new("file")
.about("input file")
.short('f')
.long("file")
.takes_value(true)
.required(true) // 必須
);
// flagで渡された値の取得
let matches = app.get_matches();
let u = matches.value_of("user").unwrap();
let p = matches.value_of("pass").unwrap();
let f = matches.value_of("file").unwrap();
こんな感じに記述してビルドしておくと-h
でヘルプが出ます。
./target/debug/rust-http-cli -h
rust-http-cli 0.1.0
po3rin
get search his
USAGE:
askdhit [OPTIONS] --file <file>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-f, --file <file> input csv file
-p, --pass <pass> basic auth pass
-u, --user <user> basic auth user
少しの記述でサクッとCLIの形が完成しました。
今回の僕の用途だとクエリがリストアップされたテキストファイルをフラグで受けてパースしてsurfを使ったリクエストに利用しています。、また、Baisc認証用にフラグで受け取った情報をこの後にエンコードしていきます。
まとめ
surf + clap でRust初心者でもサクッとHTTPクラアントCLIが作れました。何かRustで小さなツールを実装してみたいと思っている方の参考になればと思います。