最近、仕事のほうでAndroid Management APIってのをさわってて、お試しでAPIを叩くときの開発体験があんまり良くないなーと感じ、自分用にクライアントライブラリを書いてみた。
何が不満?
Android Management APIは、このとおり、本当に素直なREST APIだ。
- ベース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で作ってみた
Android Management API以外にも対応できるよう、coreとAndroid Management APIクライアントは分けた。
感想としては、非常にいまいち。
- TypeScriptがnpmとの親和性悪そう
- 一度JSにバラしてライブラリ公開したものを、再度TypeScriptから読み込む、みたいなのが若干モヤッとする
- npxでGitHubリポジトリを指定するには、TypeScriptじゃだめでJavaScriptをGitHub上にGit管理下に置く必要がある
- GET with bodyができない
コードを書いているときのTypeScriptはそんなに不満はなかったが、いざnpmに出すときに、TypeScriptをそのままアップできないのが非常にモヤッとした。自分がTypeScriptそんなに好きじゃないという要因は大いにありそうだが。
npmがそもそもTypeScriptではなくJavaScriptのモジュールライブラリであるところが、TypeScriptの非常に痒いところ
— Yusuke Iwaki (@yi01imagination) 2022年2月16日
TypeScriptはjsxとか書いてるときには「ああ便利だな」って感じるけど、ライブラリ開発しようとしたら途端にめんどくさいな。
— Yusuke Iwaki (@yi01imagination) 2022年2月23日
とりあえずJavaScriptとの互換とか無視してTypeScriptで書けたらええねん、みたいなノリでdenoみたいなのが現れるのが必然的な気はする。
Dartでも書いてみた
Dartはばっちり型のある言語なので、JSのコードをそのまま持っていくと、データクラスだらけになるため、ほんの少しだけオブジェクト指向っぽくしている部分はある。でもほとんどJSのコードそのまま。
Dartは大好きなのだが、実際書いてみると、そこまで生産性の高い言語じゃないなーーという気分にもなった。Kotlinのようなdata classはやっぱりほしい。
なんかTypeScriptだと2時間くらいで実装できたところがDartだと8時間くらいかかってしまった気がする。
— Yusuke Iwaki (@yi01imagination) 2022年2月26日
もちろん調べながらやったからというのはあるとはいえ、意外と開発効率はそんなに高くないDartなのかもしれない
ネーミングが...
TypeScriptのほうは @zatsu/androidmanagement
, Dartのほうは rakuda
ぜんぜん一貫性もなく、名前を聞いてもあまり印象には残らない。うーむ、、自分ネーミングセンスなさすぎ。。。orz
ネーミングセンスないな自分...https://t.co/yYTF7B8uEZ
— Yusuke Iwaki (@yi01imagination) 2022年2月27日
まとめ
自分のためのプロダクト開発で、TypeScriptとDartを少しだけ学んだ。以上。
(ぜんぜんまとまっていないw)