Skip to content

Commit b329919

Browse files
committed
api command: support raw body passed in via --input <file>
This is to support file uploads or to pass in JSON bodies constructed elsewhere.
1 parent 217998a commit b329919

File tree

2 files changed

+125
-2
lines changed

2 files changed

+125
-2
lines changed

pkg/cmd/api/api.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type ApiOptions struct {
2020
RequestMethod string
2121
RequestMethodPassed bool
2222
RequestPath string
23+
RequestInputFile string
2324
MagicFields []string
2425
RawFields []string
2526
RequestHeaders []string
@@ -55,6 +56,10 @@ on the format of the value:
5556
appropriate JSON types;
5657
- if the value starts with "@", the rest of the value is interpreted as a
5758
filename to read the value from. Pass "-" to read from standard input.
59+
60+
Raw request body may be passed from the outside via a file specified by '--input'.
61+
Pass "-" to read from standard input. In this mode, parameters specified via
62+
'--field' flags are serialized into URL query parameters.
5863
`,
5964
Args: cobra.ExactArgs(1),
6065
RunE: func(c *cobra.Command, args []string) error {
@@ -73,6 +78,7 @@ on the format of the value:
7378
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter")
7479
cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add an additional HTTP request header")
7580
cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
81+
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The file to use as body for the HTTP request")
7682
return cmd
7783
}
7884

@@ -83,16 +89,33 @@ func apiRun(opts *ApiOptions) error {
8389
}
8490

8591
method := opts.RequestMethod
86-
if len(params) > 0 && !opts.RequestMethodPassed {
92+
requestPath := opts.RequestPath
93+
requestHeaders := opts.RequestHeaders
94+
var requestBody interface{} = params
95+
96+
if !opts.RequestMethodPassed && (len(params) > 0 || opts.RequestInputFile != "") {
8797
method = "POST"
8898
}
8999

100+
if opts.RequestInputFile != "" {
101+
file, size, err := openUserFile(opts.RequestInputFile, opts.IO.In)
102+
if err != nil {
103+
return err
104+
}
105+
defer file.Close()
106+
requestPath = addQuery(requestPath, params)
107+
requestBody = file
108+
if size > 0 {
109+
requestHeaders = append(requestHeaders, fmt.Sprintf("Content-Length: %d", size))
110+
}
111+
}
112+
90113
httpClient, err := opts.HttpClient()
91114
if err != nil {
92115
return err
93116
}
94117

95-
resp, err := httpRequest(httpClient, method, opts.RequestPath, params, opts.RequestHeaders)
118+
resp, err := httpRequest(httpClient, method, requestPath, requestBody, requestHeaders)
96119
if err != nil {
97120
return err
98121
}
@@ -188,3 +211,21 @@ func readUserFile(fn string, stdin io.ReadCloser) ([]byte, error) {
188211
defer r.Close()
189212
return ioutil.ReadAll(r)
190213
}
214+
215+
func openUserFile(fn string, stdin io.ReadCloser) (io.ReadCloser, int64, error) {
216+
if fn == "-" {
217+
return stdin, 0, nil
218+
}
219+
220+
r, err := os.Open(fn)
221+
if err != nil {
222+
return r, 0, err
223+
}
224+
225+
s, err := os.Stat(fn)
226+
if err != nil {
227+
return r, 0, err
228+
}
229+
230+
return r, s.Size(), nil
231+
}

pkg/cmd/api/api_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func Test_NewCmdApi(t *testing.T) {
3131
RequestMethod: "GET",
3232
RequestMethodPassed: false,
3333
RequestPath: "graphql",
34+
RequestInputFile: "",
3435
RawFields: []string(nil),
3536
MagicFields: []string(nil),
3637
RequestHeaders: []string(nil),
@@ -45,6 +46,7 @@ func Test_NewCmdApi(t *testing.T) {
4546
RequestMethod: "DELETE",
4647
RequestMethodPassed: true,
4748
RequestPath: "repos/octocat/Spoon-Knife",
49+
RequestInputFile: "",
4850
RawFields: []string(nil),
4951
MagicFields: []string(nil),
5052
RequestHeaders: []string(nil),
@@ -59,6 +61,7 @@ func Test_NewCmdApi(t *testing.T) {
5961
RequestMethod: "GET",
6062
RequestMethodPassed: false,
6163
RequestPath: "graphql",
64+
RequestInputFile: "",
6265
RawFields: []string{"query=QUERY"},
6366
MagicFields: []string{"body=@file.txt"},
6467
RequestHeaders: []string(nil),
@@ -73,13 +76,29 @@ func Test_NewCmdApi(t *testing.T) {
7376
RequestMethod: "GET",
7477
RequestMethodPassed: false,
7578
RequestPath: "user",
79+
RequestInputFile: "",
7680
RawFields: []string(nil),
7781
MagicFields: []string(nil),
7882
RequestHeaders: []string{"accept: text/plain"},
7983
ShowResponseHeaders: true,
8084
},
8185
wantsErr: false,
8286
},
87+
{
88+
name: "with request body from file",
89+
cli: "user --input myfile",
90+
wants: ApiOptions{
91+
RequestMethod: "GET",
92+
RequestMethodPassed: false,
93+
RequestPath: "user",
94+
RequestInputFile: "myfile",
95+
RawFields: []string(nil),
96+
MagicFields: []string(nil),
97+
RequestHeaders: []string(nil),
98+
ShowResponseHeaders: false,
99+
},
100+
wantsErr: false,
101+
},
83102
{
84103
name: "no arguments",
85104
cli: "",
@@ -92,6 +111,7 @@ func Test_NewCmdApi(t *testing.T) {
92111
assert.Equal(t, tt.wants.RequestMethod, o.RequestMethod)
93112
assert.Equal(t, tt.wants.RequestMethodPassed, o.RequestMethodPassed)
94113
assert.Equal(t, tt.wants.RequestPath, o.RequestPath)
114+
assert.Equal(t, tt.wants.RequestInputFile, o.RequestInputFile)
95115
assert.Equal(t, tt.wants.RawFields, o.RawFields)
96116
assert.Equal(t, tt.wants.MagicFields, o.MagicFields)
97117
assert.Equal(t, tt.wants.RequestHeaders, o.RequestHeaders)
@@ -199,6 +219,44 @@ func Test_apiRun(t *testing.T) {
199219
}
200220
}
201221

222+
func Test_apiRun_inputFile(t *testing.T) {
223+
io, stdin, _, _ := iostreams.Test()
224+
resp := &http.Response{StatusCode: 204}
225+
226+
options := ApiOptions{
227+
RequestPath: "hello",
228+
RequestInputFile: "-",
229+
RawFields: []string{"a=b", "c=d"},
230+
231+
IO: io,
232+
HttpClient: func() (*http.Client, error) {
233+
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
234+
resp.Request = req
235+
return resp, nil
236+
}
237+
return &http.Client{Transport: tr}, nil
238+
},
239+
}
240+
241+
fmt.Fprintln(stdin, "I WORK OUT")
242+
243+
err := apiRun(&options)
244+
if err != nil {
245+
t.Errorf("got error %v", err)
246+
}
247+
248+
assert.Equal(t, "POST", resp.Request.Method)
249+
assert.Equal(t, "/hello?a=b&c=d", resp.Request.URL.RequestURI())
250+
assert.Equal(t, "", resp.Request.Header.Get("Content-Length"))
251+
assert.Equal(t, "", resp.Request.Header.Get("Content-Type"))
252+
253+
bb, err := ioutil.ReadAll(resp.Request.Body)
254+
if err != nil {
255+
t.Errorf("got error %v", err)
256+
}
257+
assert.Equal(t, "I WORK OUT\n", string(bb))
258+
}
259+
202260
func Test_parseFields(t *testing.T) {
203261
io, stdin, _, _ := iostreams.Test()
204262
fmt.Fprint(stdin, "pasted contents")
@@ -305,3 +363,27 @@ func Test_magicFieldValue(t *testing.T) {
305363
})
306364
}
307365
}
366+
367+
func Test_openUserFile(t *testing.T) {
368+
f, err := ioutil.TempFile("", "gh-test")
369+
if err != nil {
370+
t.Fatal(err)
371+
}
372+
fmt.Fprint(f, "file contents")
373+
f.Close()
374+
t.Cleanup(func() { os.Remove(f.Name()) })
375+
376+
file, length, err := openUserFile(f.Name(), nil)
377+
if err != nil {
378+
t.Fatal(err)
379+
}
380+
defer file.Close()
381+
382+
fb, err := ioutil.ReadAll(file)
383+
if err != nil {
384+
t.Fatal(err)
385+
}
386+
387+
assert.Equal(t, int64(13), length)
388+
assert.Equal(t, "file contents", string(fb))
389+
}

0 commit comments

Comments
 (0)