Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

AsanaのCSVエクスポートをなんとか擬似的に自動化する

弊チームではプロジェクト管理にAsanaを使っているのだけれど、それとは関係無く、バーンアップチャートを作っていこうというチーム横断の機運があり、社内の有志がGoogle Spreadsheetで動くテンプレートを作ってくれている。AsanaのCSVエクスポート機能を使い、テンプレートの特定のシートにCSVを貼り付けると、自動的にバーンアップチャートが作成される。

がしかしMTGの度にAsanaを開いてエクスポートしてアップロードしてという手間がなんか格好悪いので自動化できないかと考えた。

ところがどっこい、残念ながらAsanaはCSVをエクスポートする機能をAPIとして提供していないようで、JSON APIを駆使してCSV同等の出力をするしかなさそうだった。

作りました

作った。

github.com

  • 言語は慣れてる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)
            }
        }
    }
}

とはいえ素朴に実装してあるだけなので、難しいことはあまりない。

感想

完全自動化できなかったのが残念だったけれど、型パワーで安心しながら実装できた。とはいえテストを書くのをさぼったのは良くなかった。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?