Skip to content

Commit 3f6d0bf

Browse files
committed
Switch to :owner/:repo syntax for placeholders
1 parent acf0046 commit 3f6d0bf

File tree

4 files changed

+95
-98
lines changed

4 files changed

+95
-98
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.13
44

55
require (
66
github.com/AlecAivazis/survey/v2 v2.0.7
7+
github.com/MakeNowJust/heredoc v1.0.0
78
github.com/briandowns/spinner v1.11.1
89
github.com/charmbracelet/glamour v0.1.1-0.20200320173916-301d3bcf3058
910
github.com/dlclark/regexp2 v1.2.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z
33
github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=
44
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
55
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
6+
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
7+
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
68
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
79
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
810
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=

pkg/cmd/api/api.go

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"github.com/MakeNowJust/heredoc"
1617
"github.com/cli/cli/internal/ghrepo"
1718
"github.com/cli/cli/pkg/cmdutil"
1819
"github.com/cli/cli/pkg/iostreams"
@@ -48,27 +49,45 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
4849
Short: "Make an authenticated GitHub API request",
4950
Long: `Makes an authenticated HTTP request to the GitHub API and prints the response.
5051
51-
The <endpoint> argument should either be a path of a GitHub API v3 endpoint, or
52+
The endpoint argument should either be a path of a GitHub API v3 endpoint, or
5253
"graphql" to access the GitHub API v4.
5354
55+
Placeholder values ":owner" and ":repo" in the endpoint argument will get replaced
56+
with values from the repository of the current directory.
57+
5458
The default HTTP request method is "GET" normally and "POST" if any parameters
5559
were added. Override the method with '--method'.
5660
57-
Pass one or more '--raw-field' values in "<key>=<value>" format to add
61+
Pass one or more '--raw-field' values in "key=value" format to add
5862
JSON-encoded string parameters to the POST body.
5963
6064
The '--field' flag behaves like '--raw-field' with magic type conversion based
6165
on the format of the value:
6266
6367
- literal values "true", "false", "null", and integer numbers get converted to
6468
appropriate JSON types;
69+
- placeholder values ":owner" and ":repo" get populated with values from the
70+
repository of the current directory;
6571
- if the value starts with "@", the rest of the value is interpreted as a
6672
filename to read the value from. Pass "-" to read from standard input.
6773
6874
Raw request body may be passed from the outside via a file specified by '--input'.
6975
Pass "-" to read from standard input. In this mode, parameters specified via
7076
'--field' flags are serialized into URL query parameters.
7177
`,
78+
Example: heredoc.Doc(`
79+
$ gh api repos/:owner/:repo/releases
80+
81+
$ gh api graphql -F owner=':owner' -F name=':repo' -f query='
82+
query($name: String!, $owner: String!) {
83+
repository(owner: $owner, name: $name) {
84+
releases(last: 3) {
85+
nodes { tagName }
86+
}
87+
}
88+
}
89+
'
90+
`),
7291
Args: cobra.ExactArgs(1),
7392
RunE: func(c *cobra.Command, args []string) error {
7493
opts.RequestPath = args[0]
@@ -96,9 +115,9 @@ func apiRun(opts *ApiOptions) error {
96115
return err
97116
}
98117

99-
requestPath, params, err := fillPlaceholders(opts, params)
118+
requestPath, err := fillPlaceholders(opts.RequestPath, opts)
100119
if err != nil {
101-
return fmt.Errorf("unable to expand `{...}` placeholders in query: %w", err)
120+
return fmt.Errorf("unable to expand placeholder in path: %w", err)
102121
}
103122
method := opts.RequestMethod
104123
requestHeaders := opts.RequestHeaders
@@ -176,35 +195,31 @@ func apiRun(opts *ApiOptions) error {
176195
return nil
177196
}
178197

179-
// fillPlaceholders replaces `{owner}` and `{repo}` placeholders with values from the current repository
180-
func fillPlaceholders(opts *ApiOptions, params map[string]interface{}) (string, map[string]interface{}, error) {
181-
query := opts.RequestPath
182-
isGraphQL := opts.RequestPath == "graphql"
183-
184-
if isGraphQL {
185-
if q, ok := params["query"].(string); ok {
186-
query = q
187-
}
188-
}
198+
var placeholderRE = regexp.MustCompile(`\:(owner|repo)\b`)
189199

190-
if !strings.Contains(query, "{owner}") && !strings.Contains(query, "{repo}") {
191-
return opts.RequestPath, params, nil
200+
// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository
201+
func fillPlaceholders(value string, opts *ApiOptions) (string, error) {
202+
if !placeholderRE.MatchString(value) {
203+
return value, nil
192204
}
193205

194206
baseRepo, err := opts.BaseRepo()
195207
if err != nil {
196-
return opts.RequestPath, params, err
208+
return value, err
197209
}
198210

199-
query = strings.ReplaceAll(query, "{owner}", baseRepo.RepoOwner())
200-
query = strings.ReplaceAll(query, "{repo}", baseRepo.RepoName())
201-
202-
if isGraphQL {
203-
params["query"] = query
204-
return opts.RequestPath, params, nil
205-
}
211+
value = placeholderRE.ReplaceAllStringFunc(value, func(m string) string {
212+
switch m {
213+
case ":owner":
214+
return baseRepo.RepoOwner()
215+
case ":repo":
216+
return baseRepo.RepoName()
217+
default:
218+
panic(fmt.Sprintf("invalid placeholder: %q", m))
219+
}
220+
})
206221

207-
return query, params, nil
222+
return value, nil
208223
}
209224

210225
func printHeaders(w io.Writer, headers http.Header, colorize bool) {
@@ -241,7 +256,7 @@ func parseFields(opts *ApiOptions) (map[string]interface{}, error) {
241256
if err != nil {
242257
return params, err
243258
}
244-
value, err := magicFieldValue(strValue, opts.IO.In)
259+
value, err := magicFieldValue(strValue, opts)
245260
if err != nil {
246261
return params, fmt.Errorf("error parsing %q value: %w", key, err)
247262
}
@@ -258,9 +273,9 @@ func parseField(f string) (string, string, error) {
258273
return f[0:idx], f[idx+1:], nil
259274
}
260275

261-
func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) {
276+
func magicFieldValue(v string, opts *ApiOptions) (interface{}, error) {
262277
if strings.HasPrefix(v, "@") {
263-
return readUserFile(v[1:], stdin)
278+
return readUserFile(v[1:], opts.IO.In)
264279
}
265280

266281
if n, err := strconv.Atoi(v); err == nil {
@@ -275,7 +290,7 @@ func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) {
275290
case "null":
276291
return nil, nil
277292
default:
278-
return v, nil
293+
return fillPlaceholders(v, opts)
279294
}
280295
}
281296

pkg/cmd/api/api_test.go

Lines changed: 48 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package api
33
import (
44
"bytes"
55
"fmt"
6-
"io"
76
"io/ioutil"
87
"net/http"
98
"os"
10-
"reflect"
119
"testing"
1210

1311
"github.com/cli/cli/internal/ghrepo"
@@ -368,9 +366,11 @@ func Test_magicFieldValue(t *testing.T) {
368366
f.Close()
369367
t.Cleanup(func() { os.Remove(f.Name()) })
370368

369+
io, _, _, _ := iostreams.Test()
370+
371371
type args struct {
372-
v string
373-
stdin io.ReadCloser
372+
v string
373+
opts *ApiOptions
374374
}
375375
tests := []struct {
376376
name string
@@ -403,21 +403,41 @@ func Test_magicFieldValue(t *testing.T) {
403403
wantErr: false,
404404
},
405405
{
406-
name: "file",
407-
args: args{v: "@" + f.Name()},
406+
name: "placeholder",
407+
args: args{
408+
v: ":owner",
409+
opts: &ApiOptions{
410+
IO: io,
411+
BaseRepo: func() (ghrepo.Interface, error) {
412+
return ghrepo.New("hubot", "robot-uprising"), nil
413+
},
414+
},
415+
},
416+
want: "hubot",
417+
wantErr: false,
418+
},
419+
{
420+
name: "file",
421+
args: args{
422+
v: "@" + f.Name(),
423+
opts: &ApiOptions{IO: io},
424+
},
408425
want: []byte("file contents"),
409426
wantErr: false,
410427
},
411428
{
412-
name: "file error",
413-
args: args{v: "@"},
429+
name: "file error",
430+
args: args{
431+
v: "@",
432+
opts: &ApiOptions{IO: io},
433+
},
414434
want: nil,
415435
wantErr: true,
416436
},
417437
}
418438
for _, tt := range tests {
419439
t.Run(tt.name, func(t *testing.T) {
420-
got, err := magicFieldValue(tt.args.v, tt.args.stdin)
440+
got, err := magicFieldValue(tt.args.v, tt.args.opts)
421441
if (err != nil) != tt.wantErr {
422442
t.Errorf("magicFieldValue() error = %v, wantErr %v", err, tt.wantErr)
423443
return
@@ -456,101 +476,60 @@ func Test_openUserFile(t *testing.T) {
456476

457477
func Test_fillPlaceholders(t *testing.T) {
458478
type args struct {
459-
opts *ApiOptions
460-
params map[string]interface{}
479+
value string
480+
opts *ApiOptions
461481
}
462482
tests := []struct {
463-
name string
464-
args args
465-
wantPath string
466-
wantParams map[string]interface{}
467-
wantErr bool
483+
name string
484+
args args
485+
want string
486+
wantErr bool
468487
}{
469488
{
470489
name: "no changes",
471490
args: args{
491+
value: "repos/owner/repo/releases",
472492
opts: &ApiOptions{
473-
RequestPath: "repos/owner/repo/releases",
474-
BaseRepo: nil,
475-
},
476-
params: map[string]interface{}{
477-
"query": "{owner}/{repo}",
493+
BaseRepo: nil,
478494
},
479495
},
480-
wantPath: "repos/owner/repo/releases",
481-
wantParams: map[string]interface{}{
482-
"query": "{owner}/{repo}",
483-
},
484-
wantErr: false,
485-
},
486-
{
487-
name: "REST path substitute",
488-
args: args{
489-
opts: &ApiOptions{
490-
RequestPath: "repos/{owner}/{repo}/releases",
491-
BaseRepo: func() (ghrepo.Interface, error) {
492-
return ghrepo.New("hubot", "robot-uprising"), nil
493-
},
494-
},
495-
params: map[string]interface{}{
496-
"query": "{owner}/{repo}",
497-
},
498-
},
499-
wantPath: "repos/hubot/robot-uprising/releases",
500-
wantParams: map[string]interface{}{
501-
"query": "{owner}/{repo}",
502-
},
496+
want: "repos/owner/repo/releases",
503497
wantErr: false,
504498
},
505499
{
506-
name: "GraphQL query substitute",
500+
name: "has substitutes",
507501
args: args{
502+
value: "repos/:owner/:repo/releases",
508503
opts: &ApiOptions{
509-
RequestPath: "graphql",
510504
BaseRepo: func() (ghrepo.Interface, error) {
511505
return ghrepo.New("hubot", "robot-uprising"), nil
512506
},
513507
},
514-
params: map[string]interface{}{
515-
"query": "{owner}/{repo}/pulls/{owner}",
516-
},
517-
},
518-
wantPath: "graphql",
519-
wantParams: map[string]interface{}{
520-
"query": "hubot/robot-uprising/pulls/hubot",
521508
},
509+
want: "repos/hubot/robot-uprising/releases",
522510
wantErr: false,
523511
},
524512
{
525-
name: "GraphQL no query",
513+
name: "no greedy substitutes",
526514
args: args{
515+
value: ":ownership/:repository",
527516
opts: &ApiOptions{
528-
RequestPath: "graphql",
529-
BaseRepo: nil,
530-
},
531-
params: map[string]interface{}{
532-
"foo": "{owner}/{repo}",
517+
BaseRepo: nil,
533518
},
534519
},
535-
wantPath: "graphql",
536-
wantParams: map[string]interface{}{
537-
"foo": "{owner}/{repo}",
538-
},
520+
want: ":ownership/:repository",
539521
wantErr: false,
540522
},
541523
}
542524
for _, tt := range tests {
543525
t.Run(tt.name, func(t *testing.T) {
544-
got, got1, err := fillPlaceholders(tt.args.opts, tt.args.params)
526+
got, err := fillPlaceholders(tt.args.value, tt.args.opts)
545527
if (err != nil) != tt.wantErr {
546528
t.Errorf("fillPlaceholders() error = %v, wantErr %v", err, tt.wantErr)
547529
return
548530
}
549-
if got != tt.wantPath {
550-
t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.wantPath)
551-
}
552-
if !reflect.DeepEqual(got1, tt.wantParams) {
553-
t.Errorf("fillPlaceholders() got1 = %v, want %v", got1, tt.wantParams)
531+
if got != tt.want {
532+
t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.want)
554533
}
555534
})
556535
}

0 commit comments

Comments
 (0)