Add documentation and tests for api --filter

This commit is contained in:
Mislav Marohnić 2021-03-03 19:24:38 +01:00
parent 9f4eb55b66
commit 03baeb2645
3 changed files with 156 additions and 31 deletions

View file

@ -24,7 +24,6 @@ import (
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/jsoncolor"
"github.com/itchyny/gojq"
"github.com/spf13/cobra"
)
@ -99,6 +98,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
original query accepts an %[1]s$endCursor: String%[1]s variable and that it fetches the
%[1]spageInfo{ hasNextPage, endCursor }%[1]s set of fields from a collection.
The %[1]s--filter%[1]s option accepts a query in jq syntax and will print only the resulting
values that match the query. This is equivalent to piping the output to %[1]sjq -r%[1]s.
To learn more about the query syntax, see: https://stedolan.github.io/jq/manual/v1.6/
With %[1]s--template%[1]s, the provided Go template is rendered using the JSON data as input.
For the syntax of Go templates, see: https://golang.org/pkg/text/template/
@ -123,6 +126,9 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
# set a custom HTTP header
$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
# print only specific fields from the response
$ gh api repos/:owner/:repo/issues --filter '.[].title'
# use a template for the output
$ gh api repos/:owner/:repo/issues --template \
'{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}'
@ -199,7 +205,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body")
cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format the response using a Go template")
cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter the response using jq syntax")
cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter fields from the response using jq syntax")
cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
return cmd
}
@ -328,38 +334,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
}
if opts.FilterOutput != "" {
var query *gojq.Query
query, err = gojq.Parse(opts.FilterOutput)
// TODO: reuse parsed query across pagination invocations
err = filterJSON(opts.IO.Out, responseBody, opts.FilterOutput)
if err != nil {
return
}
var jsonData []byte
jsonData, err = ioutil.ReadAll(responseBody)
if err != nil {
return
}
var responseData interface{}
err = json.Unmarshal(jsonData, &responseData)
if err != nil {
return
}
iter := query.Run(responseData)
for {
var v interface{}
var ok bool
v, ok = iter.Next()
if !ok {
break
}
err, ok = v.(error)
if ok {
return
}
fmt.Fprintf(opts.IO.Out, "%v\n", v)
}
} else if opts.Template != "" {
// TODO: reuse parsed template across pagination invocations
var t *template.Template

61
pkg/cmd/api/filter.go Normal file
View file

@ -0,0 +1,61 @@
package api
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"github.com/itchyny/gojq"
)
func filterJSON(w io.Writer, input io.Reader, queryStr string) error {
query, err := gojq.Parse(queryStr)
if err != nil {
return err
}
jsonData, err := ioutil.ReadAll(input)
if err != nil {
return err
}
var responseData interface{}
err = json.Unmarshal(jsonData, &responseData)
if err != nil {
return err
}
iter := query.Run(responseData)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, isErr := v.(error); isErr {
return err
}
if text, e := jsonScalarToString(v); e == nil {
_, err := fmt.Fprintln(w, text)
if err != nil {
return err
}
} else {
var jsonFragment []byte
jsonFragment, err = json.Marshal(v)
if err != nil {
return err
}
_, err = w.Write(jsonFragment)
if err != nil {
return err
}
_, err = fmt.Fprint(w, "\n")
if err != nil {
return err
}
}
}
return nil
}

View file

@ -0,0 +1,85 @@
package api
import (
"bytes"
"io"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
)
func Test_filterJSON(t *testing.T) {
type args struct {
json io.Reader
query string
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "simple",
args: args{
json: strings.NewReader(`{"name":"Mona", "arms":8}`),
query: `.name`,
},
wantW: "Mona\n",
},
{
name: "multiple queries",
args: args{
json: strings.NewReader(`{"name":"Mona", "arms":8}`),
query: `.name,.arms`,
},
wantW: "Mona\n8\n",
},
{
name: "object as JSON",
args: args{
json: strings.NewReader(`{"user":{"login":"monalisa"}}`),
query: `.user`,
},
wantW: "{\"login\":\"monalisa\"}\n",
},
{
name: "complex",
args: args{
json: strings.NewReader(heredoc.Doc(`[
{
"title": "First title",
"labels": [{"name":"bug"}, {"name":"help wanted"}]
},
{
"title": "Second but not last",
"labels": []
},
{
"title": "Alas, tis' the end",
"labels": [{}, {"name":"feature"}]
}
]`)),
query: `.[] | [.title,(.labels | map(.name) | join(","))] | @tsv`,
},
wantW: heredoc.Doc(`
First title bug,help wanted
Second but not last
Alas, tis' the end ,feature
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
if err := filterJSON(w, tt.args.json, tt.args.query); (err != nil) != tt.wantErr {
t.Errorf("filterJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("filterJSON() = %q, want %q", gotW, tt.wantW)
}
})
}
}