Promote api command to a pkg/cmd/api package
This commit is contained in:
parent
fa3e25bb4d
commit
90fa193eaf
5 changed files with 173 additions and 115 deletions
109
api/client.go
109
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
107
pkg/cmd/api/http.go
Normal 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
34
pkg/cmd/api/legacy.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue