五番煎じぐらいです.

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.exsdeps を読み込んで,Star をする.

ちなみに,他の thank-you-stars と違って,mix task として生えるので他の thank-you-stars と共存可能です!

作る

振る舞い

全部の thank-you-stars を読んだわけではないけど,だいたいこんな感じだと思う.

  1. .thank-you-stars.json ファイルから GitHub の APIトークンを取得
  2. 依存パッケージを記述してるファイルからパッケージ名を取得
  3. パッケージ名から対応する GitHub リポジトリの名前を取得(オーナーも要る)
  4. 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
         [link | _] -> OK.success 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 を切り出すには,標準パッケージ URIURI.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 にインストールしたい(hexphx_new みたいに).

しかし,これは依存パッケージがあるとうまく動作しないみたいだ. なので,純粋な elixir で書く必要がある. JSON パーサーと REST API を叩ければ良いので,なんとかなるかな...

おしまい

テストとかも無駄にしっっっかり書いたので時間かかった(笑)