詩と創作・思索のひろば

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

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
//   }
// }