Elixir ライブラリにスターを送るツール thank-you-stars を作ってみました
五番煎じぐらいです.
- Node : teppeis/thank-you-stars
- Go : mattn/thank-you-stars
- Haskell : y-taka-23/thank-you-stars
- R : ksmzn/ThankYouStars
R があるのには驚いた(笑)
いきさつ
9月末までのアルバイトで Elixir を使い始めて,その集大成として作りました(嘘).
Haskell 版の thank-you-starsのコードを眺めていたら,「Elixir でも作れそうだなぁ」と思ったので作った.
できたモノ
リポジトリは matsubara0507/thank-you-stars .
使い方はホームディレクトリに以下のように記述した .thank-you-stars.json
ファイルを作って
{
"token": "SET_YOUR_TOKEN_HERE"
}
{:thank_you_stars, "~> 0.1.0", only: :dev}
を mix.exs
ファイルの deps
に書き加えて,mix thank_you_stars
を実行するだけ. mix.exs
の deps
を読み込んで,Star をする.
ちなみに,他の thank-you-stars と違って,mix task として生えるので他の thank-you-stars と共存可能です!
作る
振る舞い
全部の thank-you-stars を読んだわけではないけど,だいたいこんな感じだと思う.
.thank-you-stars.json
ファイルから GitHub の APIトークンを取得- 依存パッケージを記述してるファイルからパッケージ名を取得
- パッケージ名から対応する GitHub リポジトリの名前を取得(オーナーも要る)
- Star をする GitHub API を叩いて Star する
OK パッケージ
Elixir は(Go言語のように)エラーハンドリングを {:ok, xxx}
か {:error, yyy}
を返して行う慣習がある(例外も投げれるけど). ファイルの読み込みとか,JSONのパースとか.
ただし,コレのおかげでお得意のパイプ演算子 (|>)
による結合が使えない. モナドが使えれば.... 調べてみたら,このパターン専用のモナドみたいな OK パッケージ というのがあった. なので,積極的にこのパッケージを使っていく.
使い方はパイプ演算子の代わりにバインド演算子 ~>>
を使うだけ.
OK.success(file)
~>> File.read
~>> Poison.decode
~>> Dict.fetch(name)
他にも OK.with
マクロや OK.try
マクロなどがある.
1. GitHub の APIトークンを取得
$HOME/.thank-you-stars.json
ファイルの読み込みは File.read(Path.join [System.user_home, ".thank-you-stars.json"])
でできる.
JSON のデコードは Poison というライブラリを使うのがデファクトスタンダードっぽい. しかし,このまま使うと OK パッケージとうまくかみ合わない(例では使ったけど).
というのも,ドキュメントでは {:error, {:invalid, String.t}}
という型のエラーが返ってくる可能性があると書いてあるが 実際に返ってくるのは {:error, :invalid, 0}
という感じの3つ組のタプル(この問題は既に Issue にあって,コメントによると修正済みで次のバージョンでは直ってるようだ). OK パッケージは3つ組のタプルを処理できない. なのでラッパー関数を定義して使った.
defp poison_decode(str) do
case Poison.decode(str) do
:error, :invalid, _} -> {:error, :invalid}
{-> other
other end
end
あとは連想配列の "token"
キーで API Token を取得するだけ.
2. パッケージ名を取得
他の言語のだと JSON ファイルや Cabal ファイルを読み込んで,デコードしてみたいなことをしているが,Elixir もとい Mix の場合は必要ない.
Mix.Project.config[:deps]
で mix.exs ファイルの dpes
タグの値を取得することが出来るからだ. あとはいい感じにパッケージ名だけをとってくるだけ.
3. GitHub リポジトリの名前を取得
ここが鬼門. 最終的には,Hex (Elixirのパッケージマネージャ)の API を見つけたのでそれを使ったが,普通にググっただけでは見つからなくて,困った. 困った挙句,「各パッケージの Hex の Web ページには書いてあるんだから,スクレイピングすればいいじゃん!」とか血迷ったことを思いついて,それでやってた.
しかし,hexpm リポジトリを眺めてたら,「これ Phoenix じゃん!」となったので,Routing の定義をしているファイルを確認したら,API の定義があった. 外からも叩けたので無事に,無駄に HTML をパースするという重い処理をしなくてすんだ.
API じたいは HTTPoison で叩いて,Posion でデコードしている.
もういっこ問題があって,それは GitHub のリポジトリへの URL が適当に定義されている点だ. "GitHub" : "https://github.com/xxx/yyy"
と書いてあったり,"Github" : "https://github.com/xxx/yyy"
と書いてあったり,"github" : "https://github.com/xxx/yyy"
と書いてあったりする. これは,hexpm の API が単純に,各パッケージの mix.exs に定義した hex の設定を読んでいるだけで,GitHub へのリンクの設定は,フォーマットが無く任意だからだ(:links
の設定値の中に自由にリンクを書くだけ).
なので,どれでもマッチできるような処理を書く必要があった.
def github_url(links) do
"GitHub", "Github", "github"]
[|> Enum.map(&(Map.get(links, &1)))
|> Enum.filter(&(!is_nil(&1)))
|> case do
-> OK.failure nil
[] | _] -> OK.success link
[link end
end
4. Star をする
GitHub API の Elixir Client には Tentacat パッケージを使った. しかし,Star を行う API は実装されてなかった(あとで PR でも送ろうかな...)ので Tentacat.put/3
を直接使った.
def star_github_package(url, client) do
url|> URI.parse()
|> Map.get(:path, "")
|> (&(Tentacat.put "user/starred#{&1}", client)).()
|> case do
204, _} -> OK.success(url)
{-> OK.failure(url)
_ end
end
ちなみに,GitHub の URL https://github.com/xxx/yyy
からオーナーとリポジトリ名 /xxx/yyy
を切り出すには,標準パッケージ URI の URI.parse/1
関数を使った(標準であるの凄い).
Hex に登録
最後に,Hex に登録する. こうすることで, matsubara0507/thank-you-stars
を簡単にローカルへインストールできる.
手順は簡単で,Hex のドキュメントに従うだけ. もし,Hex のサインアップページ でアカウントを作ってしまった場合には,mix hex.user auth
を実行すればよい.
実行
こんな感じ
$ mix thank_you_stars
Starred! https://github.com/antonmi/espec
Starred! https://github.com/elixir-lang/ex_doc
Starred! https://github.com/edgurgel/httpoison
Starred! https://github.com/CrowdHailer/OK
Starred! https://github.com/devinus/poison
Starred! https://github.com/edgurgel/tentacat
できれば
できれば,mix archive.install github matsubara0507/thank-you-stars
でグローバルな Mix にインストールしたい(hex
や phx_new
みたいに).
しかし,これは依存パッケージがあるとうまく動作しないみたいだ. なので,純粋な elixir で書く必要がある. JSON
パーサーと REST API を叩ければ良いので,なんとかなるかな...
おしまい
テストとかも無駄にしっっっかり書いたので時間かかった(笑)