このあいだ 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 // } // }