From 90fa193eafdc52b99cdd514f8177c6cf459da62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 14 May 2020 15:26:42 +0200 Subject: [PATCH] Promote `api` command to a `pkg/cmd/api` package --- api/client.go | 109 +++----------------------------- command/root.go | 3 + {command => pkg/cmd/api}/api.go | 35 ++++++---- pkg/cmd/api/http.go | 107 +++++++++++++++++++++++++++++++ pkg/cmd/api/legacy.go | 34 ++++++++++ 5 files changed, 173 insertions(+), 115 deletions(-) rename {command => pkg/cmd/api}/api.go (87%) create mode 100644 pkg/cmd/api/http.go create mode 100644 pkg/cmd/api/legacy.go diff --git a/api/client.go b/api/client.go index 0e620365f..79883015e 100644 --- a/api/client.go +++ b/api/client.go @@ -7,7 +7,6 @@ import ( "io" "io/ioutil" "net/http" - "net/url" "regexp" "strings" @@ -17,14 +16,18 @@ import ( // ClientOption represents an argument to NewClient type ClientOption = func(http.RoundTripper) http.RoundTripper -// NewClient initializes a Client -func NewClient(opts ...ClientOption) *Client { +// NewHTTPClient initializes an http.Client +func NewHTTPClient(opts ...ClientOption) *http.Client { tr := http.DefaultTransport for _, opt := range opts { tr = opt(tr) } - http := &http.Client{Transport: tr} - client := &Client{http: http} + return &http.Client{Transport: tr} +} + +// NewClient initializes a Client +func NewClient(opts ...ClientOption) *Client { + client := &Client{http: NewHTTPClient(opts...)} return client } @@ -208,102 +211,6 @@ func (c Client) REST(method string, p string, body io.Reader, data interface{}) return nil } -// DirectRequest is a low-level interface to making generic API requests -func (c Client) DirectRequest(method string, p string, params interface{}, headers []string) (*http.Response, error) { - url := "https://api.github.com/" + p - var body io.Reader - var bodyIsJSON bool - isGraphQL := p == "graphql" - - switch pp := params.(type) { - case map[string]interface{}: - if strings.EqualFold(method, "GET") { - url = addQuery(url, pp) - } else { - if isGraphQL { - pp = groupGraphQLVariables(pp) - } - b, err := json.Marshal(pp) - if err != nil { - return nil, fmt.Errorf("error serializing parameters: %w", err) - } - body = bytes.NewBuffer(b) - bodyIsJSON = true - } - case io.Reader: - body = pp - default: - return nil, fmt.Errorf("unrecognized parameters type: %v", params) - } - - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - - if bodyIsJSON { - req.Header.Set("Content-Type", "application/json; charset=utf-8") - } - for _, h := range headers { - idx := strings.IndexRune(h, ':') - if idx == -1 { - return nil, fmt.Errorf("header %q requires a value separated by ':'", h) - } - req.Header.Set(h[0:idx], strings.TrimSpace(h[idx+1:])) - } - - return c.http.Do(req) -} - -func groupGraphQLVariables(params map[string]interface{}) map[string]interface{} { - topLevel := make(map[string]interface{}) - variables := make(map[string]interface{}) - - for key, val := range params { - switch key { - case "query": - topLevel[key] = val - default: - variables[key] = val - } - } - - if len(variables) > 0 { - topLevel["variables"] = variables - } - return topLevel -} - -func addQuery(path string, params map[string]interface{}) string { - if len(params) == 0 { - return path - } - - query := url.Values{} - for key, value := range params { - switch v := value.(type) { - case string: - query.Add(key, v) - case []byte: - query.Add(key, string(v)) - case nil: - query.Add(key, "") - case int: - query.Add(key, fmt.Sprintf("%d", v)) - case bool: - query.Add(key, fmt.Sprintf("%v", v)) - default: - panic(fmt.Sprintf("unknown type %v", v)) - } - } - - sep := "?" - if strings.ContainsRune(path, '?') { - sep = "&" - } - return path + sep + query.Encode() -} - func handleResponse(resp *http.Response, data interface{}) error { success := resp.StatusCode >= 200 && resp.StatusCode < 300 diff --git a/command/root.go b/command/root.go index 6233be957..85a6d722b 100644 --- a/command/root.go +++ b/command/root.go @@ -12,6 +12,7 @@ import ( "github.com/cli/cli/context" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" + apiCmd "github.com/cli/cli/pkg/cmd/api" "github.com/cli/cli/utils" "github.com/spf13/cobra" @@ -61,6 +62,8 @@ func init() { } return &FlagError{Err: err} }) + + RootCmd.AddCommand(apiCmd.NewCmdApi()) } // FlagError is the kind of error raised in flag processing diff --git a/command/api.go b/pkg/cmd/api/api.go similarity index 87% rename from command/api.go rename to pkg/cmd/api/api.go index d27dacd0d..2fa1b12f5 100644 --- a/command/api.go +++ b/pkg/cmd/api/api.go @@ -1,21 +1,18 @@ -package command +package api import ( "fmt" "io" "io/ioutil" + "net/http" "os" "strconv" "strings" - "github.com/cli/cli/api" + "github.com/cli/cli/context" "github.com/spf13/cobra" ) -func init() { - RootCmd.AddCommand(makeApiCommand()) -} - type ApiOptions struct { RequestMethod string RequestMethodPassed bool @@ -24,9 +21,11 @@ type ApiOptions struct { RawFields []string RequestHeaders []string ShowResponseHeaders bool + + HttpClient func() (*http.Client, error) } -func makeApiCommand() *cobra.Command { +func NewCmdApi() *cobra.Command { opts := ApiOptions{} cmd := &cobra.Command{ Use: "api ", @@ -55,13 +54,16 @@ on the format of the value: opts.RequestPath = args[0] opts.RequestMethodPassed = c.Flags().Changed("method") - ctx := contextForCommand(c) - client, err := apiClientForContext(ctx) - if err != nil { - return err + opts.HttpClient = func() (*http.Client, error) { + ctx := context.New() + token, err := ctx.AuthLogin() + if err != nil { + return nil, err + } + return apiClientFromContext(token), nil } - return apiRun(&opts, client) + return apiRun(&opts) }, } @@ -73,7 +75,7 @@ on the format of the value: return cmd } -func apiRun(opts *ApiOptions, client *api.Client) error { +func apiRun(opts *ApiOptions) error { params := make(map[string]interface{}) for _, f := range opts.RawFields { key, value, err := parseField(f) @@ -99,7 +101,12 @@ func apiRun(opts *ApiOptions, client *api.Client) error { method = "POST" } - resp, err := client.DirectRequest(method, opts.RequestPath, params, opts.RequestHeaders) + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + resp, err := httpRequest(httpClient, method, opts.RequestPath, params, opts.RequestHeaders) if err != nil { return err } diff --git a/pkg/cmd/api/http.go b/pkg/cmd/api/http.go new file mode 100644 index 000000000..112f6ab1b --- /dev/null +++ b/pkg/cmd/api/http.go @@ -0,0 +1,107 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func httpRequest(client *http.Client, method string, p string, params interface{}, headers []string) (*http.Response, error) { + // TODO: GHE support + url := "https://api.github.com/" + p + var body io.Reader + var bodyIsJSON bool + isGraphQL := p == "graphql" + + switch pp := params.(type) { + case map[string]interface{}: + if strings.EqualFold(method, "GET") { + url = addQuery(url, pp) + } else { + if isGraphQL { + pp = groupGraphQLVariables(pp) + } + b, err := json.Marshal(pp) + if err != nil { + return nil, fmt.Errorf("error serializing parameters: %w", err) + } + body = bytes.NewBuffer(b) + bodyIsJSON = true + } + case io.Reader: + body = pp + default: + return nil, fmt.Errorf("unrecognized parameters type: %v", params) + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + if bodyIsJSON { + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + for _, h := range headers { + idx := strings.IndexRune(h, ':') + if idx == -1 { + return nil, fmt.Errorf("header %q requires a value separated by ':'", h) + } + req.Header.Set(h[0:idx], strings.TrimSpace(h[idx+1:])) + } + + return client.Do(req) +} + +func groupGraphQLVariables(params map[string]interface{}) map[string]interface{} { + topLevel := make(map[string]interface{}) + variables := make(map[string]interface{}) + + for key, val := range params { + switch key { + case "query": + topLevel[key] = val + default: + variables[key] = val + } + } + + if len(variables) > 0 { + topLevel["variables"] = variables + } + return topLevel +} + +func addQuery(path string, params map[string]interface{}) string { + if len(params) == 0 { + return path + } + + query := url.Values{} + for key, value := range params { + switch v := value.(type) { + case string: + query.Add(key, v) + case []byte: + query.Add(key, string(v)) + case nil: + query.Add(key, "") + case int: + query.Add(key, fmt.Sprintf("%d", v)) + case bool: + query.Add(key, fmt.Sprintf("%v", v)) + default: + panic(fmt.Sprintf("unknown type %v", v)) + } + } + + sep := "?" + if strings.ContainsRune(path, '?') { + sep = "&" + } + return path + sep + query.Encode() +} diff --git a/pkg/cmd/api/legacy.go b/pkg/cmd/api/legacy.go new file mode 100644 index 000000000..5c8429727 --- /dev/null +++ b/pkg/cmd/api/legacy.go @@ -0,0 +1,34 @@ +package api + +import ( + "fmt" + "net/http" + "os" + "strings" + + "github.com/cli/cli/api" + "github.com/cli/cli/utils" +) + +// TODO +func apiClientFromContext(token string) *http.Client { + var opts []api.ClientOption + if verbose := os.Getenv("DEBUG"); verbose != "" { + opts = append(opts, apiVerboseLog()) + } + opts = append(opts, + api.AddHeader("Authorization", fmt.Sprintf("token %s", token)), + // FIXME + // api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", command.Version)), + // antiope-preview: Checks + api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"), + ) + return api.NewHTTPClient(opts...) +} + +// TODO +func apiVerboseLog() api.ClientOption { + logTraffic := strings.Contains(os.Getenv("DEBUG"), "api") + colorize := utils.IsTerminal(os.Stderr) + return api.VerboseLog(utils.NewColorable(os.Stderr), logTraffic, colorize) +}