詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

GraphQLのレスポンスJSONに対応するstructからクエリを生成できるgo-graphql-query

このあいだ GitHub が公開していた GraphQL API が便利そうだったので使おうと思ったのだけど、求めたライブラリがなかったので作った次第です。

ここで GraphQL についての説明はしませんが、結果の JSON とクエリが同じ形を持っているのが便利で美しいですね。ということは API の結果の JSON を受け取る struct から GraphQL のクエリが生成できるのが自然でしょう。そういうことをやってくれるシンプルなライブラリです。

GitHub - motemen/go-graphql-query

軽い気持ちで書きはじめたところ GraphQL に予想外の表現力があることがわかったのでけっこう無理をしているところもあります。一般的にどんな使い方がなされるのかわかってないので、フィードバックもお待ちしてます。

API

API は今のところ一つだけで、 Build(v interface{}) ([]byte, error) に結果を受け取りたい構造体を渡してやると GraphQL のクエリが返ってくる、というものです。

import "github.com/motemen/go-graphql-query"
// ...
var result someStruct
query, err := graphqlquery.Build(&result)
// ... query を使って Graph QL リクエスト
err := json.Unmarshal(respBody, &result)

以下、いろいろな例をあげていきます。

シンプルな例

以下の型をもつ構造体を Build に渡すと、

type simpleExample struct {
    Hero struct {
        Name string
    }
}

以下のようなクエリが得られます。

query {
  hero {
    name
  }
}

簡単ですね。

構造体のフィールドからクエリ中の名前はさしあたり camelCase への変換で実装してありますが、json タグによってつけられた名前があれば、それが優先されます。

ディレクティブとエイリアス

GraphQL にはもっと複雑な機能がありますが、それらはだいたい graphql をキーとする構造体のタグで表現されます。

タグが @ で始まっていれば、そのフィールドに対するディレクティブになります。

struct {
    Hero struct {
        Name    string
        Friends []struct {
            Name string
        } `graphql:"@include(if: true)"`
    }
}

// query {
//   hero {
//     name
//     friends @include(if: true) {
//       name
//     }
//   }
// }

また、タグが alias= で始まっていれば、そのフィールドがエイリアスであることを示します。

struct {
    JediHero struct {
        Name string
    } `graphql:"alias=hero"`
}

// query {
//   jediHero: hero {
//     name
//   }
// }

カンマで区切って併置することも可能。

struct {
    Hero struct {
        Name        string
        HeroFriends []struct {
            Name string
        } `graphql:"@include(if: true),alias=friends"`
    }
}

// query {
//   hero {
//     name
//     heroFriends: friends @include(if: true) {
//       name
//     }
//   }
// }

引数と変数

引数はタグ中の (...) で表現できます。カッコ内のカンマはまあまあいい感じにサポートされます。

struct {
    Human []struct {
        Name   string
        Height int
    } `graphql:"(after: \"1000\", limit: 5)"`
}

// query {
//   human(after: "1000", limit: 5) {
//     name
//     height
//   }
// }

また、引数は struct 内の GraphQLArguments という特別な名前のフィールドに構造体型をあてることでも表現できます。この構造体の中では、タグの中身は引数として与えるパラメタの値(の文字列)として解釈されます。

struct {
    Human []struct {
        GraphQLArguments struct {
            After string `graphql:"\"1000\""`
            Limit int    `graphql:"5"`
        }
        Name   string
        Height int
    }
}

// query {
//   human(after: "1000", limit: 5) {
//     name
//     height
//   }
// }

これがどういうときに役立つかというと、クエリに変数が登場する場合です。引数の値を $ で始めると変数として解釈され、それがそのままクエリ自体の引数として登場します(query(...) の中)。この際、構造体のフィールドの型情報が利用されます。

struct {
    Human []struct {
        GraphQLArguments struct {
            After string `graphql:"$afterCursor"`
            Limit int    `graphql:"$limit"`
        }
        Name   string
        Height int
    }
}

// query($afterCursor: String, $limit: Int) {
//   human(after: $afterCursor, limit: $limit) {
//     name
//     height
//   }
// }

便利ですが、前に出たディレクティブの場合など、変数が構造体タグの中に登場する場合は、手でクエリの引数を指定する必要があります。この場合は構造体トップレベルに GraphQLArguments フィールドを指定してやります。

struct {
    GraphQLArguments struct {
        WithFriends bool `graphql:"$withFriends"`
    }
    Hero struct {
        Name    string
        Friends []struct {
            Name string
        } `graphql:"@include(if: $withFriends)"`
    }
}

// query($withFriends: Boolean) {
//   hero {
//     name
//     friends @include(if: $withFriends) {
//       name
//     }
//   }
// }

引数の型は、組み込みの型であれば適切に変換され、また、名前の付いた型である場合は、その名前が大文字で始まっていればそのままクエリの中に登場します。

インラインフラグメント

インラインフラグメントは、タグを ... で始めることで表現できます。この際、構造体埋め込みも使えます。

struct {
    Hero []struct {
        Name        string
        Height      int `graphql:"... on Human"`
        DroidFields `graphql:"... on Droid"`
    }
}

type DroidFields struct {
    PrimaryFunction string
}

// query {
//   hero {
//     name
//     ... on Human {
//       height
//     }
//     ... on Droid {
//       primaryFunction
//     }
//   }
// }

複雑な例

最後に、実際に自分が使っている例です。もともとこれが作りたくてライブラリをこしらえたのでした。

type githubPullRequest struct {
    GraphQLArguments struct {
        IsBase bool `graphql:"$isBase,notnull"`
    }
    Repository *struct {
        GraphQLArguments struct {
            Owner string `graphql:"$owner,notnull"`
            Name  string `graphql:"$repo,notnull"`
        }
        IsPrivate   bool
        PullRequest struct {
            GraphQLArguments struct {
                Number int `graphql:"$number,notnull"`
            }
            Title   string
            Number  int
            Body    string
            URL     string
            BaseRef struct {
                Name string
            }
            HeadRef struct {
                Target struct {
                    Tree struct {
                        Entries []struct {
                            Name string
                            Oid  string
                            Type string
                        }
                    } `graphql:"... on Commit"`
                }
            } `graphql:"@include(if: $isBase)"`
            Commits struct {
                GraphQLArguments struct {
                    First int    `graphql:"100"`
                    After string `graphql:"$commitsAfter"`
                }
                Edges []struct {
                    Node struct {
                        Commit struct {
                            Message string
                        }
                    }
                }
                PageInfo struct {
                    HasNextPage bool
                    EndCursor   string
                }
                TotalCount int
            } `graphql:"@include(if: $isBase)"`
        }
    }
    RateLimit struct {
        Remaining int
    }
}

// query($commitsAfter: String, $isBase: Boolean!, $number: Int!, $owner: String!, $repo: String!) {
//   repository(name: $repo, owner: $owner) {
//     isPrivate
//     pullRequest(number: $number) {
//       title
//       number
//       body
//       url
//       baseRef {
//         name
//       }
//       headRef @include(if: $isBase) {
//         target {
//           ... on Commit {
//             tree {
//               entries {
//                 name
//                 oid
//                 type
//               }
//             }
//           }
//         }
//       }
//       commits(after: $commitsAfter, first: 100) @include(if: $isBase) {
//         edges {
//           node {
//             commit {
//               message
//             }
//           }
//         }
//         pageInfo {
//           hasNextPage
//           endCursor
//         }
//         totalCount
//       }
//     }
//   }
//   rateLimit {
//     remaining
//   }
// }

指定された型からswitch文を生成するgo-switchgenと、そのコーディング動画

GitHub - motemen/go-switchgen

ある日 Go で reflect パッケージを使ったプログラムを書きたい! と思い立ち、となると reflect.Kind で swtich したいわけで、ありうる case 節を全部書きつくすのは大変だ、というわけで switch 文を生成する簡単なプログラムを書きました。パッケージ名と型を指定すると、その型における switch … case の候補を列挙してくれます。

普通の switch の場合:

% go-switchgen reflect.Kind
switch {
case reflect.Array:
case reflect.Bool:
...
case reflect.Uintptr:
case reflect.UnsafePointer:
}

type switch の場合:

% go-switchgen go/ast.Node
switch {
case *ast.ArrayType:
case *ast.AssignStmt:
...
case *ast.UnaryExpr:
case *ast.ValueSpec:
}

まあまあ便利そうですね。

そして今回はそのコーディング風景を録画してみました。

Goライブコーディング: go-switchgen(2倍速) - YouTube

少し前に同僚がコーディング動画を公開しているのを見て、自分でもやってみたいと思っていたところで、ちょうどいいサイズのネタを探していたこともあってのことでした。

30分くらいかかったので、2倍速にしています。あとから自分のやったことを見なおして声を当てるのは意外と難しく、あまり意味のあるコメントはつけられませんでした。利用しているパッケージの背景知識も必要になるので、題材としてはあまりよくなかったのかも。次に何しよう……と思いながらドキュメントを眺めているだけのシーンも多く、見ていて面白いコーディング動画を撮るのは一筋縄ではいかなそうです。

録画してみると自分がどんなことを考えながらコードを書いているかが分かって面白いですね(今回はかなり print デバッグしながらのコーディングでした)。あまり恥ずかしいことはできない、という心理もはたらき、エディタの設定を見直したりしていて、少し快適になりました。

# そして公開したあとに go/importer を使えばよかったことに気づき、こっそり修正しました……。

GoogleカレンダーとSlackステータスをワンクリックで連携できるアプリをGoogle Apps Scriptで書いた

Googleカレンダーで現在進行中のイベントをSlackステータスに反映させるようにしておくと、チームメンバーに、移動中や不在やミーティング中といった状況を自然に共有できるので便利ですね。そのように設定している人も多いと思います。

f:id:motemen:20170515184041p:plain
似たアイコンが並んでいるように見えますが一方はモザイクです

巷では Google Apps Script でこの連携を行うような方法が公開されていて、自分でも書いて使ってました。これは一度動かしてしまえば大変便利なんですが、インストールの方法はけっこう面倒で、非エンジニアをふくめ会社のみんなに薦めるには少しハードルが高い。

そこで、Google Apps Script を用いて、(初回のインストール手順を除いて)ワンクリックで Google カレンダーと Slack ステータスの連携を行えるウェブアプリを作りました。

GitHub - motemen/gas-google-calendar-to-slack-status-farm

動作イメージ

明らかにワンクリックですね(実際には Slack 側でもう一回クリックする必要がありました)

f:id:motemen:20170515184113p:plain
クリック前

f:id:motemen:20170515184107p:plain
クリック後

動作要件

  • G Suite(Google Apps)のメンバー向け。そうでない場合も使えるかもしれません
  • Slack App として動きます
  • 認証した人の Google アカウントの個人カレンダーから、現在+5分後に進行中のイベントを Slack ステータスに反映します
  • イベントのタイトルによってステータスの絵文字が変わります(Slack おすすめ設定に準じます)
    • デフォルトでは :spiral_calendar_pad: (🗓)になります
    • イベント名に「早退」「遅刻」と入っている場合は :bus: (🚌) になります
    • イベント名に「休」と入っている場合は :palm_tree: (🌴)になります
    • イベント名に「:hospital:」など任意の emoji が入っている場合はそれがステータスに反映されます

設置方法

Apps Script の作成

  1. script.google.com などから新規 Goolge Apps Script を作成。
  2. [Resources] → [Libraries…] メニューより表示されるダイアログに、1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF を追加。(apps-script-oauth2を利用しています)
    • f:id:motemen:20170515182620p:plain
  3. gas-google-calendar-to-slack-status-farmリポジトリより、Code.jsをコピペ。

Slack App の作成

一人で使うスクリプトであれば個人用のトークン(最近はレガシートークンとらしい)で済ますところなのだけど、他のメンバーにも使ってもらうため Slack App として導入します。

  1. Slack API: Applications | Slackより新しい App を作成。
  2. 「OAuth & Permissions」に関して、以下の項目を設定。
  3. Permission Scopes に users.profile:write を追加。
  4. Redirect URLs には、先ほどのスクリプトの OAuth コールバック先として https://script.google.com/macros/d/{スクリプトID}/usercallback を指定。
    • スクリプトIDはスクリプトの [File] → [Project properties…] メニュー → [Script ID] 欄で確認できます
  5. 「App Credentials」の「Client ID」「Client Secret」を、それぞれスクリプトの SLACK_CLIENT_ID SLACK_CLIENT_SECRET として設定。

Apps Script のデプロイ

スクリプトの [Publish] → [Deploy as web app…] メニューより、ウェブアプリとしてデプロイ。

  • 「Execute the app as」は「User accessing the web app」に
  • 「Who has access to the app」は G Suite のドメインのメンバーに公開、などとするとよさそう。

ダイアログに表示されている URL にアクセスすると、ウェブ UI が表示されているのが確認できると思います。「Slack連携する」ボタンをクリックすると1分おきに実行されるトリガが作成されます。カレンダーにイベントを作成するなどして確かめてみてください。

付記

  • スクリプトは Google の OAuth も自動的に利用しますが、その際スクリプト名に「Google」という文字列が含まれていると OAuth アプリ名として利用されない気がしました。
  • スクリプト中の SLACK_LOG_WEBHOOK_URL に Slack の Incoming Webhook の URL を設定すると、ちょっとしたログがチャンネルに流れてきます。

技術的な内容

仕組み

最初にこれを作ろうと思ったとき頭によぎったのは Google Apps Script でやれば(Google 側のユーザ認証やカレンダー情報の取得など)簡単だが、Slack 側のユーザ認証情報や時間ベーストリガの実行権限の管理がうまくおこなえるのか? ということでした。apps-script-oauth2 ライブラリは PropertiesService.getUserProperties() というユーザごとのストレージに OAuth2 トークンを保存できるのですが、1分おきに実行されるトリガがスクリプト作成者の権限で実行されてしまうと、ユーザ別の認証情報にアクセスできなくなってしまうわけです。

結論から言うとこの心配はしなくてよくて、ScriptApp.newTrigger() を使うとそれを実行したユーザのトリガが作られるため、うまいことボタンをクリックしたユーザの権限でステータスの更新のための処理が実行できるのでした。その代わり、スクリプトの所有者からトリガを一覧したり操作したりできません。ここが困りポイントで、現状ではログから設定したユーザを見つけて、声をかけていくしかない……。

TypeScript 型定義ファイル

今回もGoogle Apps Script の TypeScript 型定義ファイルを作った - 詩と創作・思索のひろばで作った型定義ファイルを使ったけど、いくつかミスが見つかったのでとりあえず as any でしのぎつつ Pull Request も送っています