GitHub
Rust

hyper + serde_jsonでGithub APiを利用した際のメモ

More than 1 year has passed since last update.

Rusk/Task Manager in Rust の作成log.
Rust とTask から命名

Motivation

Googleカレンダーで主にスケジュール・タスク管理をしてたのだけど、
色々と不満点があったので小さいCLIツールを作ることにした。

  • Web上でGoogle ToDoリストを作成するまでの導線が長い
  • ToDoにタグ付けしたい & Tag毎に確認したい
  • ターミナルからも見たい

Task管理する上で欲しい項目/機能をリストアップ。

項目

  • task/name
  • description
  • tag(s)
  • deadline

機能

  • CRUD
  • Notification
  • ターミナル/Web(スマホ)両方から確認したい

で、上記を一番楽に実装しようと考えた結果、GitHubのレポジトリを立てToDoをIssueとして管理することにした。

データ構造としてのタスクの項目が多いので、ToDoの作成は主にWebから。CLIから作成する場合はinteractiveな感じに。

Approach

env

Rust: stable 1.21.0

Cargo.toml
futures = "0.1.17"
hyper = "0.11.6"
hyper-tls = "0.1.2"
prettytable-rs = "0.6.7"
serde = "1.0.18"
serde_derive = "1.0.18"
serde_json = "1.0.5"
structopt = "0.1.3"
structopt-derive = "0.1.3"
term = "0.4.6"
tokio-core = "0.1.10"

Rustのhyperを使ってhttpクライアントを作成。
hyper単体だとTLS対応していないので、hyper-tlsをコネクタに利用した。

CLIにはstructoptを用いてCRUD操作をenumとstructでいい感じに。
出力の整形はprettytable-rsを使用。OSS様様。

GitHubのAPIコールはここから発行できるpersonal access tokenをヘッダに付与するだけで、curlでも簡単に呼べる。

レポジトリのissue一覧を叩くと以下のようにissueに対してユーザ情報、ラベルリストが入れ子構造になって返される。

curl -H "Authorization: token $GITHUB_API_TOKEN" https://api.github.com/repos/koka831/todo/issues

[
  {
    "url": "https://api.github.com/repos/...",
    ..
    "id": 271094185,
    "number": 2,
    "title": "hoge",
    "user": {
      "login": "koka831",
      ...
    },
    "labels": [
      {
        "id": ...
        "name": "ToDo",
        "color": "3778ba",
      }
    ],
    "state": "open",
    ...
  }, ...
]

同じことをhyperからもやってみると、403/Forbiddenが返される。
referenceを読み直すとUserAgentが必須とのこと。

All API requests MUST include a valid User-Agent header. Requests with no User-Agent header will be rejected.

hyperで独自ヘッダを付与する際には、hyperのマクロが便利。
Authorizationのheader生成関数を作成して、TLSコネクタベースのHttpClientに渡す。
Clientサイドなのでシングルスレッドで。

use hyper::{Client, Method, Request};
use hyper::header::UserAgent;
use hyper_tls::HttpsConnector;
use tokio_core::reactor::Core;

// ...
header! { ( Authorization, "Authorization" ) => [String] }
let auth_header = format!("token {}", token);

let mut req = Request::new(Method::Get, url);
req.headers_mut().set(UserAgent::new("todo"));
req.headers_mut().set(Authorization(auth_header));

// setup client
let handle = core.handle();
let client = Client::configure()
  // creates connector with 1 thread
  .connector(HttpsConnector::new(1, &handle).unwrap())
  .build(&handle);

取得したIssueリストはdeserializeしてstructに落としこむ。

とりあえずの構造体を作ってserde-jsonとすり合わせようと思っていたのだけど、
構造体のkeyをjsonのkeyと合わせておくだけでそのままシリアライズしてくれた。
今回はv3のREST APIを使ったけど、struct/enumのSerializerとGraphQLも相性がいいと思う。

#[derive(Debug, Serialize, Deserialize)]
struct Issue {
  id: u32,
  number: u8,
  title: String,
  labels: Vec<Label>,
  state: String,
  body: String
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Label {
    id: u32,
    pub name: String,
    pub color: String,
    default: bool,
}

// ...
// send request
let res = client.request(req)
  .and_then(|res| { res.body().concat2()
  .and_then(move |body| {
    let v: Vec<Issue> = serde_json::from_slice(&body).unwrap();
    Ok(v)
  })
});

core.run(res).unwrap()

ToDo

  • APIコールが1~2secかかるのでその間スピナーアイコン表示したい
  • rusk + fzf + vimでタスクをシームレスに編集できるように
    • CLI側のメモは後日fzf+vimインターフェースと合わせて

misc

https://koka831.github.io/memo/2017/11/07/github-api.html の転載