Promote api command to a pkg/cmd/api package

This commit is contained in:
Mislav Marohnić 2020-05-14 15:26:42 +02:00
parent fa3e25bb4d
commit 90fa193eaf
5 changed files with 173 additions and 115 deletions

View file

@ -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

View file

@ -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

View file

@ -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 <endpoint>",
@ -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
}

107
pkg/cmd/api/http.go Normal file
View file

@ -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()
}

34
pkg/cmd/api/legacy.go Normal file
View file

@ -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)
}