Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional API functionality #39

Merged
merged 2 commits into from May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 40 additions & 21 deletions gh.go
Expand Up @@ -61,13 +61,15 @@ func RESTClient(opts *api.ClientOptions) (api.RESTClient, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
if optionsNeedResolution(opts) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
}
}
return iapi.NewRESTClient(opts.Host, opts), nil
}
Expand All @@ -80,13 +82,15 @@ func GQLClient(opts *api.ClientOptions) (api.GQLClient, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
if optionsNeedResolution(opts) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
}
}
return iapi.NewGQLClient(opts.Host, opts), nil
}
Expand All @@ -104,13 +108,15 @@ func HTTPClient(opts *api.ClientOptions) (*http.Client, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
if optionsNeedResolution(opts) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
err = resolveOptions(opts, cfg)
if err != nil {
return nil, err
}
}
client := iapi.NewHTTPClient(opts)
return &client, nil
Expand Down Expand Up @@ -151,6 +157,19 @@ func CurrentRepository() (repo.Repository, error) {
return irepo.New(r.Host, r.Owner, r.Repo), nil
}

func optionsNeedResolution(opts *api.ClientOptions) bool {
if opts.Host == "" {
return true
}
if opts.AuthToken == "" {
return true
}
if opts.UnixDomainSocket == "" && opts.Transport == nil {
return true
}
return false
}

func resolveOptions(opts *api.ClientOptions, cfg config.Config) error {
var token string
var err error
Expand Down
104 changes: 103 additions & 1 deletion gh_test.go
Expand Up @@ -2,6 +2,7 @@ package gh

import (
"fmt"
"net/http"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -129,7 +130,7 @@ func TestGQLClientError(t *testing.T) {

res := struct{ Organization struct{ Name string } }{}
err = client.Do("QUERY", nil, &res)
assert.EqualError(t, err, "GQL error: Could not resolve to an Organization with the login of 'cli'.")
assert.EqualError(t, err, "GQL: Could not resolve to an Organization with the login of 'cli'. (organization)")
assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

Expand Down Expand Up @@ -201,6 +202,107 @@ func TestResolveOptions(t *testing.T) {
}
}

func TestOptionsNeedResolution(t *testing.T) {
tests := []struct {
name string
opts *api.ClientOptions
out bool
}{
{
name: "Host, AuthToken, and UnixDomainSocket specified",
opts: &api.ClientOptions{
Host: "test.com",
AuthToken: "token",
UnixDomainSocket: "socket",
},
out: false,
},
{
name: "Host, AuthToken, and Transport specified",
opts: &api.ClientOptions{
Host: "test.com",
AuthToken: "token",
Transport: http.DefaultTransport,
},
out: false,
},
{
name: "Host, and AuthToken specified",
opts: &api.ClientOptions{
Host: "test.com",
AuthToken: "token",
},
out: true,
},
{
name: "Host, and UnixDomainSocket specified",
opts: &api.ClientOptions{
Host: "test.com",
UnixDomainSocket: "socket",
},
out: true,
},
{
name: "Host, and Transport specified",
opts: &api.ClientOptions{
Host: "test.com",
Transport: http.DefaultTransport,
},
out: true,
},
{
name: "AuthToken, and UnixDomainSocket specified",
opts: &api.ClientOptions{
AuthToken: "token",
UnixDomainSocket: "socket",
},
out: true,
},
{
name: "AuthToken, and Transport specified",
opts: &api.ClientOptions{
AuthToken: "token",
Transport: http.DefaultTransport,
},
out: true,
},
{
name: "Host specified",
opts: &api.ClientOptions{
Host: "test.com",
},
out: true,
},
{
name: "AuthToken specified",
opts: &api.ClientOptions{
AuthToken: "token",
},
out: true,
},
{
name: "UnixDomainSocket specified",
opts: &api.ClientOptions{
UnixDomainSocket: "socket",
},
out: true,
},
{
name: "Transport specified",
opts: &api.ClientOptions{
Transport: http.DefaultTransport,
},
out: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, optionsNeedResolution(tt.opts))
})
}
}

func testConfig() config.Config {
var data = `
hosts:
Expand Down
6 changes: 3 additions & 3 deletions internal/api/gql_client.go
Expand Up @@ -55,7 +55,7 @@ func (c gqlClient) Do(query string, variables map[string]interface{}, response i

success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
return handleHTTPError(resp)
return api.HandleHTTPError(resp)
}

if resp.StatusCode == http.StatusNoContent {
Expand All @@ -67,14 +67,14 @@ func (c gqlClient) Do(query string, variables map[string]interface{}, response i
return err
}

gr := &gqlResponse{Data: response}
gr := gqlResponse{Data: response}
err = json.Unmarshal(body, &gr)
if err != nil {
return err
}

if len(gr.Errors) > 0 {
return &api.GQLError{Errors: gr.Errors}
return api.GQLError{Errors: gr.Errors}
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/api/gql_client_test.go
Expand Up @@ -37,7 +37,7 @@ func TestGQLClientDo(t *testing.T) {
JSON(`{"errors":[{"message":"OH NO"},{"message":"this is fine"}]}`)
},
wantErr: true,
wantErrMsg: "GQL error: OH NO\nthis is fine",
wantErrMsg: "GQL: OH NO, this is fine",
},
{
name: "http fail request empty response",
Expand Down
74 changes: 1 addition & 73 deletions internal/api/http.go
@@ -1,9 +1,7 @@
package api

import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
Expand All @@ -29,6 +27,7 @@ const (
)

var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)

var timeZoneNames = map[int]string{
-39600: "Pacific/Niue",
-36000: "Pacific/Honolulu",
Expand Down Expand Up @@ -119,77 +118,6 @@ func NewHTTPClient(opts *api.ClientOptions) http.Client {
return http.Client{Transport: transport, Timeout: opts.Timeout}
}

// TODO: Export function in near future.
func handleHTTPError(resp *http.Response) error {
httpError := api.HTTPError{
StatusCode: resp.StatusCode,
RequestURL: resp.Request.URL,
Headers: resp.Header,
}

if !jsonTypeRE.MatchString(resp.Header.Get(contentType)) {
httpError.Message = resp.Status
return httpError
}

body, err := io.ReadAll(resp.Body)
if err != nil {
httpError.Message = err.Error()
return httpError
}

var parsedBody struct {
Message string `json:"message"`
Errors []json.RawMessage
}
if err := json.Unmarshal(body, &parsedBody); err != nil {
return httpError
}

var messages []string
if parsedBody.Message != "" {
messages = append(messages, parsedBody.Message)
}
for _, raw := range parsedBody.Errors {
switch raw[0] {
case '"':
var errString string
_ = json.Unmarshal(raw, &errString)
messages = append(messages, errString)
httpError.Errors = append(httpError.Errors, api.HTTPErrorItem{Message: errString})
case '{':
var errInfo api.HTTPErrorItem
_ = json.Unmarshal(raw, &errInfo)
msg := errInfo.Message
if errInfo.Code != "" && errInfo.Code != "custom" {
msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code))
}
if msg != "" {
messages = append(messages, msg)
}
httpError.Errors = append(httpError.Errors, errInfo)
}
}
httpError.Message = strings.Join(messages, "\n")

return httpError
}

// Convert common error codes to human readable messages
// See https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors for more details.
func errorCodeToMessage(code string) string {
switch code {
case "missing", "missing_field":
return "is missing"
case "invalid", "unprocessable":
return "is invalid"
case "already_exists":
return "already exists"
default:
return code
}
}

func inspectableMIMEType(t string) bool {
return strings.HasPrefix(t, "text/") || jsonTypeRE.MatchString(t)
}
Expand Down
27 changes: 26 additions & 1 deletion internal/api/rest_client.go
Expand Up @@ -23,6 +23,31 @@ func NewRESTClient(host string, opts *api.ClientOptions) api.RESTClient {
}
}

func (c restClient) Request(method string, path string, body io.Reader) (*http.Response, error) {
url := restURL(c.host, path)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}

resp, err := c.client.Do(req)
if err != nil {
return resp, err
}

success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
err = api.HTTPError{
StatusCode: resp.StatusCode,
RequestURL: resp.Request.URL,
AcceptedOAuthScopes: resp.Header.Get("X-Accepted-Oauth-Scopes"),
OAuthScopes: resp.Header.Get("X-Oauth-Scopes"),
}
}

return resp, err
}

func (c restClient) Do(method string, path string, body io.Reader, response interface{}) error {
url := restURL(c.host, path)
req, err := http.NewRequest(method, url, body)
Expand All @@ -38,7 +63,7 @@ func (c restClient) Do(method string, path string, body io.Reader, response inte

success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
return handleHTTPError(resp)
return api.HandleHTTPError(resp)
}

if resp.StatusCode == http.StatusNoContent {
Expand Down