From e8e82da825726cfa002e8782efdd3586bb2e537e Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 13 Feb 2023 18:27:16 +1100 Subject: [PATCH 1/2] Add pagination examples for RESTClient and GQLClient --- example_gh_test.go | 109 +++++++++++++++++++++++++++++++++++++++------ go.mod | 3 -- go.sum | 9 ---- 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/example_gh_test.go b/example_gh_test.go index f4c6af1..c9fc86d 100644 --- a/example_gh_test.go +++ b/example_gh_test.go @@ -1,10 +1,13 @@ package gh_test import ( + "encoding/json" "fmt" "io" "log" + "net/http" "os" + "regexp" "time" gh "github.com/cli/go-gh" @@ -12,7 +15,6 @@ import ( "github.com/cli/go-gh/pkg/tableprinter" "github.com/cli/go-gh/pkg/term" graphql "github.com/cli/shurcooL-graphql" - "github.com/shurcooL/githubv4" ) // Execute 'gh issue list -R cli/cli', and print the output. @@ -70,29 +72,66 @@ func ExampleRESTClient_request() { if err != nil { log.Fatal(err) } - // URL to cli/cli release v2.14.2 checksums.txt assetURL := "repos/cli/cli/releases/assets/71589494" - resp, err := client.Request("GET", assetURL, nil) + response, err := client.Request(http.MethodGet, assetURL, nil) if err != nil { log.Fatal(err) } - defer resp.Body.Close() - + defer response.Body.Close() f, err := os.CreateTemp("", "*_checksums.txt") if err != nil { log.Fatal(err) } defer f.Close() - - _, err = io.Copy(f, resp.Body) + _, err = io.Copy(f, response.Body) if err != nil { log.Fatal(err) } - fmt.Printf("Asset downloaded to %s\n", f.Name()) } +// Get releases from cli/cli repository using REST API with paginated results. +func ExampleRESTClient_pagination() { + var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) + findNextPage := func(response *http.Response) (string, bool) { + for _, m := range linkRE.FindAllStringSubmatch(response.Header.Get("Link"), -1) { + if len(m) > 2 && m[2] == "next" { + return m[1], true + } + } + return "", false + } + client, err := gh.RESTClient(nil) + if err != nil { + log.Fatal(err) + } + requestPath := "repos/cli/cli/releases" + page := 1 + hasNextPage := true + for hasNextPage { + response, err := client.Request(http.MethodGet, requestPath, nil) + if err != nil { + log.Fatal(err) + } + body, err := io.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + if err := response.Body.Close(); err != nil { + log.Fatal(err) + } + data := []struct{ Name string }{} + if err := json.Unmarshal(body, &data); err != nil { + log.Fatal(err) + } + fmt.Printf("Page: %d\n", page) + fmt.Println(data) + requestPath, hasNextPage = findNextPage(response) + page++ + } +} + // Query tags from cli/cli repository using GQL API. func ExampleGQLClient_simple() { client, err := gh.GQLClient(nil) @@ -155,12 +194,12 @@ func ExampleGQLClient_advanced() { } // Add a star to the cli/go-gh repository using the GQL API. -func ExampleGQLClient_Mutate_simple() { +func ExampleGQLClient_mutate_simple() { client, err := gh.GQLClient(nil) if err != nil { log.Fatal(err) } - var m struct { + var mutation struct { AddStar struct { Starrable struct { Repository struct { @@ -172,16 +211,58 @@ func ExampleGQLClient_Mutate_simple() { } } `graphql:"addStar(input: $input)"` } + type AddStarInput struct { + StarrableID string `json:"starrableId"` + } variables := map[string]interface{}{ - "input": githubv4.AddStarInput{ - StarrableID: githubv4.NewID("R_kgDOF_MgQQ"), + "input": AddStarInput{ + StarrableID: "R_kgDOF_MgQQ", }, } - err = client.Mutate("AddStar", &m, variables) + err = client.Mutate("AddStar", &mutation, variables) if err != nil { log.Fatal(err) } - fmt.Println(m.AddStar.Starrable.Repository.StargazerCount) + fmt.Println(mutation.AddStar.Starrable.Repository.StargazerCount) +} + +// Query releases from cli/cli repository using GQL API with paginated results. +func ExampleGQLClient_pagination() { + client, err := gh.GQLClient(nil) + if err != nil { + log.Fatal(err) + } + var query struct { + Repository struct { + Releases struct { + Nodes []struct { + Name string + } + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"releases(first: 30, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + variables := map[string]interface{}{ + "owner": graphql.String("cli"), + "name": graphql.String("cli"), + "endCursor": (*graphql.String)(nil), + } + page := 1 + for { + if err := client.Query("RepositoryReleases", &query, variables); err != nil { + log.Fatal(err) + } + fmt.Printf("Page: %d\n", page) + fmt.Println(query.Repository.Releases.Nodes) + if !query.Repository.Releases.PageInfo.HasNextPage { + break + } + variables["endCursor"] = graphql.String(query.Repository.Releases.PageInfo.EndCursor) + page++ + } } // Get repository for the current directory. diff --git a/go.mod b/go.mod index c87b1d7..22fa4bf 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.12.0 - github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93 github.com/stretchr/testify v1.7.0 github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e golang.org/x/sys v0.4.0 @@ -40,10 +39,8 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect github.com/yuin/goldmark v1.4.4 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.5.0 // indirect - golang.org/x/oauth2 v0.4.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 0a3587c..dc7532e 100644 --- a/go.sum +++ b/go.sum @@ -17,7 +17,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -66,10 +65,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93 h1:JNy04upyaTaAGVlUFAL+60/1nphmJtuTu36tLhbaqXk= -github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= -github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc= -github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -84,8 +79,6 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -106,8 +99,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 27f2360505b73248d72a1d4e6c9cf85003202a35 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Fri, 17 Feb 2023 19:47:06 +1100 Subject: [PATCH 2/2] address pr comments --- example_gh_test.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/example_gh_test.go b/example_gh_test.go index c9fc86d..496ce56 100644 --- a/example_gh_test.go +++ b/example_gh_test.go @@ -108,26 +108,26 @@ func ExampleRESTClient_pagination() { } requestPath := "repos/cli/cli/releases" page := 1 - hasNextPage := true - for hasNextPage { + for { response, err := client.Request(http.MethodGet, requestPath, nil) if err != nil { log.Fatal(err) } - body, err := io.ReadAll(response.Body) + data := []struct{ Name string }{} + decoder := json.NewDecoder(response.Body) + err = decoder.Decode(&data) if err != nil { log.Fatal(err) } if err := response.Body.Close(); err != nil { log.Fatal(err) } - data := []struct{ Name string }{} - if err := json.Unmarshal(body, &data); err != nil { - log.Fatal(err) - } fmt.Printf("Page: %d\n", page) fmt.Println(data) - requestPath, hasNextPage = findNextPage(response) + var hasNextPage bool + if requestPath, hasNextPage = findNextPage(response); !hasNextPage { + break + } page++ } } @@ -211,6 +211,8 @@ func ExampleGQLClient_mutate_simple() { } } `graphql:"addStar(input: $input)"` } + // Note that the shurcooL/githubv4 package has defined input structs generated from the + // GraphQL schema that can be used instead of writing your own. type AddStarInput struct { StarrableID string `json:"starrableId"` }