Help us understand the problem. What is going on with this article?

RustでAPIを叩くCLIツールを作る

More than 3 years have passed since last update.

はじめに

これはRustその2 Advent Calendar 2017の18日目の記事です。

こんにちわ、@Khigashiguchiです。
今回は、業務上使用しているドキュメント共有ツールのAPIを叩くCLIツールを作りましたので、そのやり方をご紹介いたします。

今回作ったもの

勤務先でDocBaseというドキュメント共有サービスを使っています。APIドキュメントを元に、自分のローカルPCにあるmarkdownファイルをターミナル内で即座にアップするツールを作りました。
Khigashiguchi/docbase-cliに今回のコードを公開しています。

目標は、下記の利用方法を持つCLIツールです。

$ docbase-cli -h

DocBase API Command Line Interface Application

USAGE:
    docbase-cli
    docbase-cli post <post-file-path>... <post-title>...
    docbase-cli (-h | --help)
    docbase-cli --version

Options:
    -h, --help      Show this screen.
    --version       Show version.

開発目次

  • コマンドラインオプション解析
  • Markdownファイルの読み込み
  • リクエスト内のjsonを作成
  • HTTPSリクエスト

コマンドラインオプション解析

CLIを作る上でまずコマンドライン入力を解析してプログラムに渡す必要があります。今回は、docoptというcrateを利用しました。

  • Cargo.toml
[dependencies]
docopt = "0.8"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.8"
futures = "0.1"
hyper = "0.11"
tokio-core = "0.1"
hyper-tls = "0.1.2"
dotenv = "0.10.1"

[dependencies.jsonway]
git = "https://github.com/rustless/jsonway"

今回は、使用するcratesを最初に全部読み込みます。

  • main.rs
#[macro_use]
extern crate serde_derive;
extern crate docopt;

const USAGE: &'static str = "
DocBase API Command Line Interface Application

USAGE:
    docbase-cli
    docbase-cli post <post-file-path>... <post-title>...
    docbase-cli (-h | --help)
    docbase-cli --version

Options:
    -h, --help      Show this screen.
    --version       Show version.
";

#[derive(Debug, Deserialize)]
pub struct Args {
    cmd_post: bool,
    arg_post_file_path: Vec<String>,
    arg_post_title: Vec<String>,
}


fn main() {
    let args: Args = Docopt::new(USAGE)
    .and_then(|d| d.deserialize())
    .unwrap_or_else(|e| e.exit());

    println!("{:?}", args);
}

上記の実装で、--helpで利用方法を出力することができるようになります。

$ docbase-cli -h

DocBase API Command Line Interface Application

USAGE:
    docbase-cli
    docbase-cli post <post-file-path>... <post-title>...
    docbase-cli (-h | --help)
    docbase-cli --version

Options:
    -h, --help      Show this screen.
    --version       Show version.

今回は、加えて $ docbase-cli post <post-file-path>... <post-title>... で渡されたオプションを元に処理をしたいので実装を追加します。今回は、オプションごとの処理をDocbase Traitに実装しました。

  • main.rs
#[macro_use]
extern crate serde_derive;
extern crate docopt;

+ mod docbase;

+ use docopt::Docopt;
+ use docbase::Docbase;

...

fn main() {
    let args: Args = Docopt::new(USAGE)
    .and_then(|d| d.deserialize())
    .unwrap_or_else(|e| e.exit());

    + let mut docbase = Docbase::new();
    + docbase.run(args);
}

オプションごとの処理をDocbase.rsに定義していきます。

  • docbase.rs
use super::Args;

pub struct Docbase {
}

impl Docbase {
    pub fn new() -> Docbase {
        Docbase {}
    }
    pub fn run(&mut self, args: Args) {
        if args.cmd_post {
            self.execute_post(args.arg_post_file_path, args.arg_post_title);
        } else {
            println!("{:?}", args);
        }
    }
    fn execute_post(&self, post_file_path: Vec<String>, post_title: Vec<String>) {
    }
}

実装方法としては、Docbase::runにコマンドオプションであるargsを渡します。cmd_post(docbase-cli post)であれば、execute_postメソッドを呼び出すという方式です。
execute_postの中では、下記の処理が行われる必要があります。

  • コマンドオプションのarg_post_file_pathを元にローカルのファイルをStringに読み込む。
  • リクエスト内のjsonを作成
  • HTTPSリクエスト
  • jsonレスポンスの解析

Markdownファイルの読み込み

実際にDocbaseに投げるローカルにあるMarkdownファイルの読み込みを行います。

  • docbase.rs
+ use std::error::Error;
+ use std::fs::File;
+ use std::io::prelude::*;
+ use std::path::Path;
use super::Args;

    fn execute_post(&self, post_file_path: Vec<String>, post_title: Vec<String>) {
        // argsからファイルパスを取得する
        + let post_file = &post_file_path[0];
        + let path = Path::new(post_file);
        + let display = path.display();

        // ファイルを開く
        + let mut file = match File::open(&path) {
            Err(why) => panic!("Couldn't open {}: {}", display, Error::description(&why)),
            Ok(file) => file,
        };

        // ファイル内のテキストを読み込む
        + let mut s = String::new();
        + let body = match file.read_to_string(&mut s) {
            Err(why) => panic!("Couldn't read {}: {}", display, Error::description(&why)),
            Ok(_) => s
    }

リクエスト内のjsonを作成

jsonを生成するために、jsonway crateを利用します。

  • docbase.rs
+ extern crate jsonway;

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use super::Args;

    fn execute_post(&self, post_file_path: Vec<String>, post_title: Vec<String>) {
        // argsからタイトルを取得する
        + let title = &post_title[0];

        // argsからファイルパスを取得する
        let post_file = &post_file_path[0];
        let path = Path::new(post_file);
        let display = path.display();

        // ファイルを開く
        let mut file = match File::open(&path) {
            Err(why) => panic!("Couldn't open {}: {}", display, Error::description(&why)),
            Ok(file) => file,
        };

        // ファイル内のテキストを読み込む
        let mut s = String::new();
        let body = match file.read_to_string(&mut s) {
            Err(why) => panic!("Couldn't read {}: {}", display, Error::description(&why)),
            Ok(_) => s

        // jsonを構築する
        + let json = jsonway::object(|json| {
            json.set("title", title.to_string());
            json.set("body", body.to_string());
            json.set("draft", "true".to_string());
        }).unwrap().to_string();
    }

HTTPSリクエスト

HTTPSリクエストを生成・送信するためにhyper crateを利用します。

  • docbase.rs
+ extern crate hyper;
+ extern crate hyper_tls;
+ extern crate tokio_core;
+ extern crate futures;
extern crate jsonway;

+ use std::env;
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
+ use self::hyper::Client;
+ use self::hyper::{Method, Request};
+ use self::hyper::header::{ContentType};
+ use self::hyper_tls::HttpsConnector;
+ use self::tokio_core::reactor::Core;
+ use self::dotenv::dotenv;
+ use self::futures::{Future, Stream};
+ use self::serde_json::Value;
use super::Args;

    fn execute_post(&self, post_file_path: Vec<String>, post_title: Vec<String>) {
        // argsからタイトルを取得する
        let title = &post_title[0];

        // argsからファイルパスを取得する
        let post_file = &post_file_path[0];
        let path = Path::new(post_file);
        let display = path.display();

        // ファイルを開く
        let mut file = match File::open(&path) {
            Err(why) => panic!("Couldn't open {}: {}", display, Error::description(&why)),
            Ok(file) => file,
        };

        // ファイル内のテキストを読み込む
        let mut s = String::new();
        let body = match file.read_to_string(&mut s) {
            Err(why) => panic!("Couldn't read {}: {}", display, Error::description(&why)),
            Ok(_) => s

        // jsonを構築する
        let json = jsonway::object(|json| {
            json.set("title", title.to_string());
            json.set("body", body.to_string());
            json.set("draft", "true".to_string());
        }).unwrap().to_string();

        // URIを生成する
        + let docbase_domain = "<your-team-name>";
        + let docbase_base_uri = "https://api.docbase.io/teams/";
        + let docbase_uri = format!("{}{}{}", docbase_base_uri, docbase_domain, "/posts");

        // 環境変数からトークンを取得する
        + let docbase_token = env::var("DOCBASE_TOKEN").unwrap();

        // HTTPSクライアントインスタンスを生成
        + let mut core = Core::new().unwrap();
        + let handle = core.handle();
        + let client = Client::configure()
            .connector(HttpsConnector::new(1, &handle).unwrap())
            .build(&handle);

        // リクエストを生成
        + let uri = docbase_uri.parse().unwrap();
        + let mut req = Request::new(Method::Post, uri);
        // リクエストヘッダーを付与
        + req.headers_mut().set(ContentType::json());
        + req.headers_mut().set_raw("X-Api-Version", "1");
        + req.headers_mut().set_raw("X-DocBaseToken", docbase_token);
        // リクエストボディを付与
        + req.set_body(json);

        + let post = client.request(req).and_then(|res| {
            println!("POST: {}", res.status());
            res.body().concat2().and_then(move |body| {
                let v: Value = serde_json::from_slice(&body).unwrap();
                println!("Success! The url posted is {}.", v["url"].to_string());
                Ok(())
            })
        });
        + core.run(post).unwrap();
    }

上記の実装より、下記の結果を得ることができました。

$ docbase-cli post hello.md "This is the first post from command"

POST: 201 Created
Success! The url posted is "<url>".

最後に

様々、cratesを利用することで簡単にCLIツールを作ることができました。
CLIを作る上で参考になれば幸いです。

参考

binc
Eコマースプラットフォーム「BASE」、オンライン決済サービス「PAY.JP」、購入者向けID型決済サービス「PAY ID」の3つのサービスを運営しています。
https://binc.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away