Add documentation and tests for api --filter
This commit is contained in:
parent
9f4eb55b66
commit
03baeb2645
3 changed files with 156 additions and 31 deletions
|
|
@ -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
61
pkg/cmd/api/filter.go
Normal 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
|
||||
}
|
||||
85
pkg/cmd/api/filter_test.go
Normal file
85
pkg/cmd/api/filter_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue