Skip to content

Commit 90fa193

Browse files
committed
Promote api command to a pkg/cmd/api package
1 parent fa3e25b commit 90fa193

File tree

5 files changed

+173
-115
lines changed

5 files changed

+173
-115
lines changed

api/client.go

Lines changed: 8 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"io"
88
"io/ioutil"
99
"net/http"
10-
"net/url"
1110
"regexp"
1211
"strings"
1312

@@ -17,14 +16,18 @@ import (
1716
// ClientOption represents an argument to NewClient
1817
type ClientOption = func(http.RoundTripper) http.RoundTripper
1918

20-
// NewClient initializes a Client
21-
func NewClient(opts ...ClientOption) *Client {
19+
// NewHTTPClient initializes an http.Client
20+
func NewHTTPClient(opts ...ClientOption) *http.Client {
2221
tr := http.DefaultTransport
2322
for _, opt := range opts {
2423
tr = opt(tr)
2524
}
26-
http := &http.Client{Transport: tr}
27-
client := &Client{http: http}
25+
return &http.Client{Transport: tr}
26+
}
27+
28+
// NewClient initializes a Client
29+
func NewClient(opts ...ClientOption) *Client {
30+
client := &Client{http: NewHTTPClient(opts...)}
2831
return client
2932
}
3033

@@ -208,102 +211,6 @@ func (c Client) REST(method string, p string, body io.Reader, data interface{})
208211
return nil
209212
}
210213

211-
// DirectRequest is a low-level interface to making generic API requests
212-
func (c Client) DirectRequest(method string, p string, params interface{}, headers []string) (*http.Response, error) {
213-
url := "https://api.github.com/" + p
214-
var body io.Reader
215-
var bodyIsJSON bool
216-
isGraphQL := p == "graphql"
217-
218-
switch pp := params.(type) {
219-
case map[string]interface{}:
220-
if strings.EqualFold(method, "GET") {
221-
url = addQuery(url, pp)
222-
} else {
223-
if isGraphQL {
224-
pp = groupGraphQLVariables(pp)
225-
}
226-
b, err := json.Marshal(pp)
227-
if err != nil {
228-
return nil, fmt.Errorf("error serializing parameters: %w", err)
229-
}
230-
body = bytes.NewBuffer(b)
231-
bodyIsJSON = true
232-
}
233-
case io.Reader:
234-
body = pp
235-
default:
236-
return nil, fmt.Errorf("unrecognized parameters type: %v", params)
237-
}
238-
239-
req, err := http.NewRequest(method, url, body)
240-
if err != nil {
241-
return nil, err
242-
}
243-
244-
if bodyIsJSON {
245-
req.Header.Set("Content-Type", "application/json; charset=utf-8")
246-
}
247-
for _, h := range headers {
248-
idx := strings.IndexRune(h, ':')
249-
if idx == -1 {
250-
return nil, fmt.Errorf("header %q requires a value separated by ':'", h)
251-
}
252-
req.Header.Set(h[0:idx], strings.TrimSpace(h[idx+1:]))
253-
}
254-
255-
return c.http.Do(req)
256-
}
257-
258-
func groupGraphQLVariables(params map[string]interface{}) map[string]interface{} {
259-
topLevel := make(map[string]interface{})
260-
variables := make(map[string]interface{})
261-
262-
for key, val := range params {
263-
switch key {
264-
case "query":
265-
topLevel[key] = val
266-
default:
267-
variables[key] = val
268-
}
269-
}
270-
271-
if len(variables) > 0 {
272-
topLevel["variables"] = variables
273-
}
274-
return topLevel
275-
}
276-
277-
func addQuery(path string, params map[string]interface{}) string {
278-
if len(params) == 0 {
279-
return path
280-
}
281-
282-
query := url.Values{}
283-
for key, value := range params {
284-
switch v := value.(type) {
285-
case string:
286-
query.Add(key, v)
287-
case []byte:
288-
query.Add(key, string(v))
289-
case nil:
290-
query.Add(key, "")
291-
case int:
292-
query.Add(key, fmt.Sprintf("%d", v))
293-
case bool:
294-
query.Add(key, fmt.Sprintf("%v", v))
295-
default:
296-
panic(fmt.Sprintf("unknown type %v", v))
297-
}
298-
}
299-
300-
sep := "?"
301-
if strings.ContainsRune(path, '?') {
302-
sep = "&"
303-
}
304-
return path + sep + query.Encode()
305-
}
306-
307214
func handleResponse(resp *http.Response, data interface{}) error {
308215
success := resp.StatusCode >= 200 && resp.StatusCode < 300
309216

command/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/cli/cli/context"
1313
"github.com/cli/cli/internal/config"
1414
"github.com/cli/cli/internal/ghrepo"
15+
apiCmd "github.com/cli/cli/pkg/cmd/api"
1516
"github.com/cli/cli/utils"
1617

1718
"github.com/spf13/cobra"
@@ -61,6 +62,8 @@ func init() {
6162
}
6263
return &FlagError{Err: err}
6364
})
65+
66+
RootCmd.AddCommand(apiCmd.NewCmdApi())
6467
}
6568

6669
// FlagError is the kind of error raised in flag processing

command/api.go renamed to pkg/cmd/api/api.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
package command
1+
package api
22

33
import (
44
"fmt"
55
"io"
66
"io/ioutil"
7+
"net/http"
78
"os"
89
"strconv"
910
"strings"
1011

11-
"github.com/cli/cli/api"
12+
"github.com/cli/cli/context"
1213
"github.com/spf13/cobra"
1314
)
1415

15-
func init() {
16-
RootCmd.AddCommand(makeApiCommand())
17-
}
18-
1916
type ApiOptions struct {
2017
RequestMethod string
2118
RequestMethodPassed bool
@@ -24,9 +21,11 @@ type ApiOptions struct {
2421
RawFields []string
2522
RequestHeaders []string
2623
ShowResponseHeaders bool
24+
25+
HttpClient func() (*http.Client, error)
2726
}
2827

29-
func makeApiCommand() *cobra.Command {
28+
func NewCmdApi() *cobra.Command {
3029
opts := ApiOptions{}
3130
cmd := &cobra.Command{
3231
Use: "api <endpoint>",
@@ -55,13 +54,16 @@ on the format of the value:
5554
opts.RequestPath = args[0]
5655
opts.RequestMethodPassed = c.Flags().Changed("method")
5756

58-
ctx := contextForCommand(c)
59-
client, err := apiClientForContext(ctx)
60-
if err != nil {
61-
return err
57+
opts.HttpClient = func() (*http.Client, error) {
58+
ctx := context.New()
59+
token, err := ctx.AuthLogin()
60+
if err != nil {
61+
return nil, err
62+
}
63+
return apiClientFromContext(token), nil
6264
}
6365

64-
return apiRun(&opts, client)
66+
return apiRun(&opts)
6567
},
6668
}
6769

@@ -73,7 +75,7 @@ on the format of the value:
7375
return cmd
7476
}
7577

76-
func apiRun(opts *ApiOptions, client *api.Client) error {
78+
func apiRun(opts *ApiOptions) error {
7779
params := make(map[string]interface{})
7880
for _, f := range opts.RawFields {
7981
key, value, err := parseField(f)
@@ -99,7 +101,12 @@ func apiRun(opts *ApiOptions, client *api.Client) error {
99101
method = "POST"
100102
}
101103

102-
resp, err := client.DirectRequest(method, opts.RequestPath, params, opts.RequestHeaders)
104+
httpClient, err := opts.HttpClient()
105+
if err != nil {
106+
return err
107+
}
108+
109+
resp, err := httpRequest(httpClient, method, opts.RequestPath, params, opts.RequestHeaders)
103110
if err != nil {
104111
return err
105112
}

pkg/cmd/api/http.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"strings"
11+
)
12+
13+
func httpRequest(client *http.Client, method string, p string, params interface{}, headers []string) (*http.Response, error) {
14+
// TODO: GHE support
15+
url := "https://api.github.com/" + p
16+
var body io.Reader
17+
var bodyIsJSON bool
18+
isGraphQL := p == "graphql"
19+
20+
switch pp := params.(type) {
21+
case map[string]interface{}:
22+
if strings.EqualFold(method, "GET") {
23+
url = addQuery(url, pp)
24+
} else {
25+
if isGraphQL {
26+
pp = groupGraphQLVariables(pp)
27+
}
28+
b, err := json.Marshal(pp)
29+
if err != nil {
30+
return nil, fmt.Errorf("error serializing parameters: %w", err)
31+
}
32+
body = bytes.NewBuffer(b)
33+
bodyIsJSON = true
34+
}
35+
case io.Reader:
36+
body = pp
37+
default:
38+
return nil, fmt.Errorf("unrecognized parameters type: %v", params)
39+
}
40+
41+
req, err := http.NewRequest(method, url, body)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
if bodyIsJSON {
47+
req.Header.Set("Content-Type", "application/json; charset=utf-8")
48+
}
49+
for _, h := range headers {
50+
idx := strings.IndexRune(h, ':')
51+
if idx == -1 {
52+
return nil, fmt.Errorf("header %q requires a value separated by ':'", h)
53+
}
54+
req.Header.Set(h[0:idx], strings.TrimSpace(h[idx+1:]))
55+
}
56+
57+
return client.Do(req)
58+
}
59+
60+
func groupGraphQLVariables(params map[string]interface{}) map[string]interface{} {
61+
topLevel := make(map[string]interface{})
62+
variables := make(map[string]interface{})
63+
64+
for key, val := range params {
65+
switch key {
66+
case "query":
67+
topLevel[key] = val
68+
default:
69+
variables[key] = val
70+
}
71+
}
72+
73+
if len(variables) > 0 {
74+
topLevel["variables"] = variables
75+
}
76+
return topLevel
77+
}
78+
79+
func addQuery(path string, params map[string]interface{}) string {
80+
if len(params) == 0 {
81+
return path
82+
}
83+
84+
query := url.Values{}
85+
for key, value := range params {
86+
switch v := value.(type) {
87+
case string:
88+
query.Add(key, v)
89+
case []byte:
90+
query.Add(key, string(v))
91+
case nil:
92+
query.Add(key, "")
93+
case int:
94+
query.Add(key, fmt.Sprintf("%d", v))
95+
case bool:
96+
query.Add(key, fmt.Sprintf("%v", v))
97+
default:
98+
panic(fmt.Sprintf("unknown type %v", v))
99+
}
100+
}
101+
102+
sep := "?"
103+
if strings.ContainsRune(path, '?') {
104+
sep = "&"
105+
}
106+
return path + sep + query.Encode()
107+
}

pkg/cmd/api/legacy.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"strings"
8+
9+
"github.com/cli/cli/api"
10+
"github.com/cli/cli/utils"
11+
)
12+
13+
// TODO
14+
func apiClientFromContext(token string) *http.Client {
15+
var opts []api.ClientOption
16+
if verbose := os.Getenv("DEBUG"); verbose != "" {
17+
opts = append(opts, apiVerboseLog())
18+
}
19+
opts = append(opts,
20+
api.AddHeader("Authorization", fmt.Sprintf("token %s", token)),
21+
// FIXME
22+
// api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", command.Version)),
23+
// antiope-preview: Checks
24+
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"),
25+
)
26+
return api.NewHTTPClient(opts...)
27+
}
28+
29+
// TODO
30+
func apiVerboseLog() api.ClientOption {
31+
logTraffic := strings.Contains(os.Getenv("DEBUG"), "api")
32+
colorize := utils.IsTerminal(os.Stderr)
33+
return api.VerboseLog(utils.NewColorable(os.Stderr), logTraffic, colorize)
34+
}

0 commit comments

Comments
 (0)