Skip to content

Commit

Permalink
Merge JSON responses from gh api
Browse files Browse the repository at this point in the history
Fixes cli#1268
  • Loading branch information
heaths committed Dec 12, 2022
1 parent c21408f commit 1a7f803
Show file tree
Hide file tree
Showing 6 changed files with 425 additions and 135 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -22,6 +22,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.0.6
github.com/imdario/mergo v0.3.13
github.com/joho/godotenv v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.13
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Expand Up @@ -168,6 +168,8 @@ github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/gojq v0.12.8 h1:Zxcwq8w4IeR8JJYEtoG2MWJZUv0RGY6QqJcO1cqV8+A=
Expand Down Expand Up @@ -534,6 +536,7 @@ gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
155 changes: 94 additions & 61 deletions pkg/cmd/api/api.go
Expand Up @@ -304,23 +304,62 @@ func apiRun(opts *ApiOptions) error {
return err
}

var resp *http.Response
var responseBody io.ReadSeeker
var pages interface{}
var serverError string

isJSONRegexp := regexp.MustCompile(`[/+]json(;|$)`)
isJSON := false
hasNextPage := true

for hasNextPage {
resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders)
resp, err = httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders)
if err != nil {
return err
}

endCursor, err := processResponse(resp, opts, bodyWriter, headersWriter, &tmpl)
if resp.StatusCode == 204 {
return nil
}

b, err := io.ReadAll(resp.Body)
resp.Body.Close()

if err != nil {
return err
}

responseBody = bytes.NewReader(b)

isJSON = isJSONRegexp.MatchString(resp.Header.Get("Content-Type"))
if isJSON && (opts.RequestPath == "graphql" || resp.StatusCode >= 400) {
serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
if err != nil {
return err
}
}

err = processServerError(serverError, resp, opts)
if err != nil {
// Print the server error message before exiting
_ = printResponse(responseBody, opts, isJSON, serverError, bodyWriter, &tmpl)
return err
}

if !opts.Paginate {
break
}

if isJSON {
err = mergeJSON(&pages, responseBody)
if err != nil {
return err
}
}

if isGraphQL {
endCursor := findEndCursor(responseBody)
hasNextPage = endCursor != ""
if hasNextPage {
params["endCursor"] = endCursor
Expand All @@ -329,43 +368,55 @@ func apiRun(opts *ApiOptions) error {
requestPath, hasNextPage = findNextPage(resp)
requestBody = nil // prevent repeating GET parameters
}
}

if hasNextPage && opts.ShowResponseHeaders {
fmt.Fprint(opts.IO.Out, "\n")
if opts.Paginate && isJSON {
var buf []byte
buf, err = json.Marshal(pages)
if err != nil {
return err
}
}

return tmpl.Flush()
}
responseBody = bytes.NewReader(buf)
}

func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersWriter io.Writer, template *template.Template) (endCursor string, err error) {
if opts.ShowResponseHeaders {
fmt.Fprintln(headersWriter, resp.Proto, resp.Status)
printHeaders(headersWriter, resp.Header, opts.IO.ColorEnabled())
fmt.Fprint(headersWriter, "\r\n")
}

if resp.StatusCode == 204 {
return
err = printResponse(responseBody, opts, isJSON, serverError, bodyWriter, &tmpl)
if err != nil {
return err
}
var responseBody io.Reader = resp.Body
defer resp.Body.Close()

isJSON, _ := regexp.MatchString(`[/+]json(;|$)`, resp.Header.Get("Content-Type"))
return tmpl.Flush()
}

var serverError string
if isJSON && (opts.RequestPath == "graphql" || resp.StatusCode >= 400) {
responseBody, serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
if err != nil {
return
func processServerError(serverError string, resp *http.Response, opts *ApiOptions) error {
if serverError == "" && resp.StatusCode > 299 {
serverError = fmt.Sprintf("HTTP %d", resp.StatusCode)
}

if serverError != "" {
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", serverError)
if msg := api.ScopesSuggestion(resp); msg != "" {
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", msg)
}
if u := factory.SSOURL(); u != "" {
fmt.Fprintf(opts.IO.ErrOut, "Authorize in your web browser: %s\n", u)
}
return cmdutil.SilentError
}

var bodyCopy *bytes.Buffer
isGraphQLPaginate := isJSON && resp.StatusCode == 200 && opts.Paginate && opts.RequestPath == "graphql"
if isGraphQLPaginate {
bodyCopy = &bytes.Buffer{}
responseBody = io.TeeReader(responseBody, bodyCopy)
return nil
}

func printResponse(responseBody io.ReadSeeker, opts *ApiOptions, isJSON bool, serverError string, bodyWriter io.Writer, template *template.Template) (err error) {
_, err = responseBody.Seek(0, io.SeekStart)
if err != nil {
return
}

if opts.FilterOutput != "" && serverError == "" {
Expand All @@ -384,29 +435,6 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
} else {
_, err = io.Copy(bodyWriter, responseBody)
}
if err != nil {
return
}

if serverError == "" && resp.StatusCode > 299 {
serverError = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
if serverError != "" {
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", serverError)
if msg := api.ScopesSuggestion(resp); msg != "" {
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", msg)
}
if u := factory.SSOURL(); u != "" {
fmt.Fprintf(opts.IO.ErrOut, "Authorize in your web browser: %s\n", u)
}
err = cmdutil.SilentError
return
}

if isGraphQLPaginate {
endCursor = findEndCursor(bodyCopy)
}

return
}

Expand Down Expand Up @@ -537,46 +565,51 @@ func openUserFile(fn string, stdin io.ReadCloser) (io.ReadCloser, int64, error)
return r, s.Size(), nil
}

func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error) {
bodyCopy := &bytes.Buffer{}
b, err := io.ReadAll(io.TeeReader(r, bodyCopy))
func parseErrorResponse(responseBody io.ReadSeeker, statusCode int) (string, error) {
_, err := responseBody.Seek(0, io.SeekStart)
if err != nil {
return r, "", err
return "", err
}

var parsedBody struct {
Message string
Errors json.RawMessage
}

b, err := io.ReadAll(responseBody)
if err != nil {
return "", err
}

err = json.Unmarshal(b, &parsedBody)
if err != nil {
return bodyCopy, "", err
return "", err
}

if len(parsedBody.Errors) > 0 && parsedBody.Errors[0] == '"' {
var stringError string
if err := json.Unmarshal(parsedBody.Errors, &stringError); err != nil {
return bodyCopy, "", err
return "", err
}
if stringError != "" {
if parsedBody.Message != "" {
return bodyCopy, fmt.Sprintf("%s (%s)", stringError, parsedBody.Message), nil
return fmt.Sprintf("%s (%s)", stringError, parsedBody.Message), nil
}
return bodyCopy, stringError, nil
return stringError, nil
}
}

if parsedBody.Message != "" {
return bodyCopy, fmt.Sprintf("%s (HTTP %d)", parsedBody.Message, statusCode), nil
return fmt.Sprintf("%s (HTTP %d)", parsedBody.Message, statusCode), nil
}

if len(parsedBody.Errors) == 0 || parsedBody.Errors[0] != '[' {
return bodyCopy, "", nil
return "", nil
}

var errorObjects []json.RawMessage
if err := json.Unmarshal(parsedBody.Errors, &errorObjects); err != nil {
return bodyCopy, "", err
return "", err
}

var objectError struct {
Expand All @@ -590,24 +623,24 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
if rawErr[0] == '{' {
err := json.Unmarshal(rawErr, &objectError)
if err != nil {
return bodyCopy, "", err
return "", err
}
errors = append(errors, objectError.Message)
} else if rawErr[0] == '"' {
var stringError string
err := json.Unmarshal(rawErr, &stringError)
if err != nil {
return bodyCopy, "", err
return "", err
}
errors = append(errors, stringError)
}
}

if len(errors) > 0 {
return bodyCopy, strings.Join(errors, "\n"), nil
return strings.Join(errors, "\n"), nil
}

return bodyCopy, "", nil
return "", nil
}

func previewNamesToMIMETypes(names []string) string {
Expand Down

0 comments on commit 1a7f803

Please sign in to comment.