弊チームではプロジェクト管理にAsanaを使っているのだけれど、それとは関係無く、バーンアップチャートを作っていこうというチーム横断の機運があり、社内の有志がGoogle Spreadsheetで動くテンプレートを作ってくれている。AsanaのCSVエクスポート機能を使い、テンプレートの特定のシートにCSVを貼り付けると、自動的にバーンアップチャートが作成される。
がしかしMTGの度にAsanaを開いてエクスポートしてアップロードしてという手間がなんか格好悪いので自動化できないかと考えた。
ところがどっこい、残念ながらAsanaはCSVをエクスポートする機能をAPIとして提供していないようで、JSON APIを駆使してCSV同等の出力をするしかなさそうだった。
作りました
作った。
- 言語は慣れてるScalaを使った
- 型がおいしい
- HTTPライブラリにはsttpを使った
- Akka HTTPはなんか大変そう
- JSONのパースにはcirceを使った
- 単に慣れてる
- 時刻まわりはnscala-timeを使った
- 定番っぽい
- CSV出力にはscala-csvを使った
- なんか簡単に動きそうだったので
あまり深く考えずに動きそうなパーツをガチャンとくっつけた感じになった。
動作
% ASANA_PAT="*****" ASANA_PROJECT_GID="*****" sbt run
これでout.csv
が吐き出される。今のところはこれだけ。また、いくつかの必須でないフィールド(Assigneeのemail addressなど)は出力をサボっている。
本当は自動的にS3やGoogle DriveにアップロードしてIMPORTXML
を噛ませれば完全自動化できるのだけれど、S3にアップロードしてSpreadsheetから見えるようにするには権限まわりの折衝が必要で、Google Driveへのアップロードはgdrive
コマンドを使わなければいけなそうで結構大変。今のところはまあがんばって動かすしかない。
将来的には権限まわりをいい感じに整理して完全自動化を目指したい。
大変だったところ
APIのクセ
Asana APIでプロジェクトのタスク一覧を取得すると、Sectionまわりの情報は返さないので別途取得しなければならなかった。したがって
- プロジェクトに紐付くセクションを一括取得
- セクションに紐付くタスクを一括取得
- CSVにうまくセクション情報も出力する
という段階を踏まなければならなかった。
カーソル処理
100件ずつ引いて、特定のフィールドがあったらさらに引く・・・という処理は世界中頻出だと思うけれど、そのための抽象化レイヤーを作成した。FetcherIterator
という名前でIterator
を実装している。
class FetcherIterator[A, B, O]( fetch: (O) => Option[A], nextPred: (A) => Boolean, joiner: (Option[A]) => B, initialOffset: O, offsetExtractor: (A) => O ) extends Iterator[B] { var started: Boolean = false var current: Option[A] = None def hasNext: Boolean = !started || current.isDefined && nextPred(current.get) def next(): B = { current = fetch(started match { case true => offsetExtractor(current.get) case false => { started = true initialOffset } }) joiner(current) } }
なんかもっとオシャレになるような気もする。
circe
circeは賢いので、Case Classを自動的にJSONと相互変換できるようになっている。がDateTimeといった部分は自前でEncoder/Decoderを実装する必要があった。
import com.github.nscala_time.time.Imports._ import io.circe.{ Decoder, Encoder, HCursor, Json } object DateTimeDecoder { implicit val decodeDateTime: Decoder[org.joda.time.DateTime] = new Decoder[org.joda.time.DateTime] { final def apply(c: HCursor): Decoder.Result[org.joda.time.DateTime] = { for { str <- c.as[String] } yield { DateTime.parse(str) } } } }
とはいえ素朴に実装してあるだけなので、難しいことはあまりない。
感想
完全自動化できなかったのが残念だったけれど、型パワーで安心しながら実装できた。とはいえテストを書くのをさぼったのは良くなかった。