HTTPieのような使い勝手でちょっとだけカスタマイズできるコマンドラインAPIクライアントを作った

最近、仕事のほうでAndroid Management APIってのをさわってて、お試しでAPIを叩くときの開発体験があんまり良くないなーと感じ、自分用にクライアントライブラリを書いてみた。

何が不満?

Android Management APIは、このとおり、本当に素直なREST APIだ。

developers.google.com

  • ベースURLは共通
  • 認証はサービスアカウントで認証トークンさえもらってきたら、あとはそれをAuthorizationヘッダに付けるだけ
  • device, enterprise, などのCRUDが本当にリソースフル
    • GET /enterprises/{enterpriseId}/devices/{deviceId} のようにIDパラメータを指定してリソースにアクセスできるし、更新はGETの代わりにPATCHを使うだけ、削除はDELETEを使うだけ。

GUIであれば、PostmanとかInsomniaのような便利なツールもあるが、コマンドラインで作業するときにはcurlやHTTPieに頼らざるを得ない。

そうなると、

  • ベースURLは毎度入力しないといけない
  • 認証トークンは3600秒でexpireするため、毎時間、トークン取得APIをわざわざ叩いて、わざわざAuthorizationヘッダにセットしないといけない
    • なんかの割り込みタスクで一時間が経つと、401が返る。あぁつらい
    • シェルのコマンドヒストリーから再実行すると基本的にトークン期限切れて401。あああああつらい
  • /enterprises/xxxxxxxxx/devices/yyyyyyyy のxxxxxxxの部分を変えてAPIを叩くときに、カーソルを移動させるのが面倒くさい

などなど、結構ストレス。とくに401エラー、お前は本当にストレスだ。

どうなったら嬉しい?

エンジニアたるもの、不便なものを「不便だなー」とストレス感じたままなのは良くない。便利にしよう。どんな物が欲しいかを定義してみる。

基本的にはHTTPieの使い勝手で、

パイプ入力でボディを送れる。

jo enterpriseDisplayName=ほげ | http PATCH /enterprises/xxxxxx

クエリパラメータやヘッダは、コマンドラインパラメータで指定可能。ただし! "Authorization: Bearer $TOKEN" みたいな " をいちいちつけなくて済むようにしたい

http GET /enterprises/xxxxxx pageSize=5 Authorization: Bearer xxxxxx

あとは、最大の不満ポイントの401はどう解決するか。

AndroidのOkHTTPなどはどうやっていたかを思い出すと、AuthenticatorとかInterceptorとかを使ってネットワークリクエストの前処理としてAuthorizationヘッダを付けたり後処理として401の際のトークンリフレッシュ&リトライをしたりしていた。 Authenticator - OkHttp - OkHttp

汎用的なAPIクライアントとしてコマンドラインオプションであれこれするよりは、専用のCLIコマンドをサクッと作れるような形にして、認証ロジックやログ出力などはHTTP Interceptorのようなものをプログラムで書いて埋め込めると良さそう。

イメージ的には、このくらいの記述で、

async function authIfNeeded(call, request) {
  const accessToken = await getCachedAccessToken() || await fetchAccessToken()
  request.headers['Authorization'] = `Bearer ${accessToken.token}`

  let response = await call(request)

  if (response.status == 401) {
    const accessToken = await refreshAccessToken(accessToken)
    request.headers['Authorization'] = `Bearer ${accessToken.token}`
    response = await call(request)
  }

  return response
}

const app = createClient('androidmanagement',
  baseURL: 'https://androidmanagement.googleapis.com/v1',
  interceptors: [
    authIfNeeded,
    logging,
  ]
)

app.handle(process.argv)

androidmanagement GET /enterprises/xxxxxx/devices/yyyyyy とかそういうのが叩けるイメージ。

実装言語は何がいいか?

なんとなく、自分以外の人にも使ってもらおうとすれば、npm installとか pip installみたいな操作自体をあまり要求したくはない。

シングルバイナリで渡せるのがベスト、妥協するにしても、npx androidmanagement GET /enterprises/.... のようにインストールしないでさっと使えるものが望ましい。

ということで、候補としてはTypeScriptとDartをあげた。(というか両方作ったww)

JSON APIを最も雑に扱えるのがJavaScriptシリアライズ/デシリアライズがほとんどいらないで、オブジェクトプロパティアクセスができる)なのだけど、フレームワークを型/型ヒントのない言語で実装するのは流石に気が引けたので、あまり好きじゃないけどTypeScript。

あとは、シングルバイナリにコンパイルできるけどGoほどきつくないDart(個人の好みが大きいw)。

まずはTypeScriptで作ってみた

github.com

github.com

Android Management API以外にも対応できるよう、coreとAndroid Management APIクライアントは分けた。

感想としては、非常にいまいち。

  • TypeScriptがnpmとの親和性悪そう
    • 一度JSにバラしてライブラリ公開したものを、再度TypeScriptから読み込む、みたいなのが若干モヤッとする
    • npxでGitHubリポジトリを指定するには、TypeScriptじゃだめでJavaScriptGitHub上にGit管理下に置く必要がある
  • GET with bodyができない
    • Android Management APIでは困らないが、いずれElasticsearchのクライアント作りたいなと思ったときに困る

コードを書いているときのTypeScriptはそんなに不満はなかったが、いざnpmに出すときに、TypeScriptをそのままアップできないのが非常にモヤッとした。自分がTypeScriptそんなに好きじゃないという要因は大いにありそうだが。

Dartでも書いてみた

Dartはばっちり型のある言語なので、JSのコードをそのまま持っていくと、データクラスだらけになるため、ほんの少しだけオブジェクト指向っぽくしている部分はある。でもほとんどJSのコードそのまま。

github.com

Dartは大好きなのだが、実際書いてみると、そこまで生産性の高い言語じゃないなーーという気分にもなった。Kotlinのようなdata classはやっぱりほしい。

ネーミングが...

TypeScriptのほうは @zatsu/androidmanagement, Dartのほうは rakuda

ぜんぜん一貫性もなく、名前を聞いてもあまり印象には残らない。うーむ、、自分ネーミングセンスなさすぎ。。。orz

まとめ

自分のためのプロダクト開発で、TypeScriptとDartを少しだけ学んだ。以上。

(ぜんぜんまとまっていないw)