From e2973453b5cd77df1b246a6147bbed6b47e4ce1c Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Mon, 23 Aug 2021 12:00:25 -0700 Subject: [PATCH] Add helper template functions for rendering tables (#3519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- pkg/cmd/api/api.go | 10 ++- pkg/cmd/api/api_test.go | 105 ++++++++++++++++++++++++- pkg/cmd/issue/list/list.go | 2 +- pkg/cmd/issue/status/status.go | 2 +- pkg/cmd/issue/view/view.go | 2 +- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/pr/status/status.go | 2 +- pkg/cmd/pr/view/view.go | 2 +- pkg/cmd/release/view/view.go | 2 +- pkg/cmd/repo/list/list.go | 2 +- pkg/cmd/repo/view/view.go | 2 +- pkg/cmd/repo/view/view_test.go | 7 +- pkg/cmd/root/help_topic.go | 26 ++++++- pkg/cmdutil/json_flags.go | 10 ++- pkg/cmdutil/json_flags_test.go | 22 +++--- pkg/export/template.go | 91 +++++++++++++++++++--- pkg/export/template_test.go | 135 ++++++++++++++++++++++++++++++--- pkg/iostreams/iostreams.go | 19 ++++- pkg/text/truncate.go | 13 +++- pkg/text/truncate_test.go | 105 +++++++++++++++++++++++++ utils/table_printer.go | 21 ++++- 21 files changed, 517 insertions(+), 65 deletions(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 27e26f0041b..b08c1ec6d8e 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -294,6 +294,8 @@ func apiRun(opts *ApiOptions) error { host = opts.Hostname } + template := export.NewTemplate(opts.IO, opts.Template) + hasNextPage := true for hasNextPage { resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders) @@ -301,7 +303,7 @@ func apiRun(opts *ApiOptions) error { return err } - endCursor, err := processResponse(resp, opts, headersOutputStream) + endCursor, err := processResponse(resp, opts, headersOutputStream, &template) if err != nil { return err } @@ -324,10 +326,10 @@ func apiRun(opts *ApiOptions) error { } } - return nil + return template.End() } -func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream io.Writer) (endCursor string, err error) { +func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream io.Writer, template *export.Template) (endCursor string, err error) { if opts.ShowResponseHeaders { fmt.Fprintln(headersOutputStream, resp.Proto, resp.Status) printHeaders(headersOutputStream, resp.Header, opts.IO.ColorEnabled()) @@ -365,7 +367,7 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream } } else if opts.Template != "" { // TODO: reuse parsed template across pagination invocations - err = export.ExecuteTemplate(opts.IO.Out, responseBody, opts.Template, opts.IO.ColorEnabled()) + err = template.Execute(responseBody) if err != nil { return } diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 0d7290835ef..1a28c671d7f 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -17,6 +17,7 @@ import ( "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/export" "github.com/cli/cli/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" @@ -671,6 +672,101 @@ func Test_apiRun_paginationGraphQL(t *testing.T) { assert.Equal(t, "PAGE1_END", endCursor) } +func Test_apiRun_paginated_template(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + io.SetStdoutTTY(true) + + requestCount := 0 + responses := []*http.Response{ + { + StatusCode: 200, + Header: http.Header{"Content-Type": []string{`application/json`}}, + Body: ioutil.NopCloser(bytes.NewBufferString(`{ + "data": { + "nodes": [ + { + "page": 1, + "caption": "page one" + } + ], + "pageInfo": { + "endCursor": "PAGE1_END", + "hasNextPage": true + } + } + }`)), + }, + { + StatusCode: 200, + Header: http.Header{"Content-Type": []string{`application/json`}}, + Body: ioutil.NopCloser(bytes.NewBufferString(`{ + "data": { + "nodes": [ + { + "page": 20, + "caption": "page twenty" + } + ], + "pageInfo": { + "endCursor": "PAGE20_END", + "hasNextPage": false + } + } + }`)), + }, + } + + options := ApiOptions{ + IO: io, + HttpClient: func() (*http.Client, error) { + var tr roundTripper = func(req *http.Request) (*http.Response, error) { + resp := responses[requestCount] + resp.Request = req + requestCount++ + return resp, nil + } + return &http.Client{Transport: tr}, nil + }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + + RequestMethod: "POST", + RequestPath: "graphql", + Paginate: true, + // test that templates executed per page properly render a table. + Template: `{{range .data.nodes}}{{tablerow .page .caption}}{{end}}`, + } + + err := apiRun(&options) + require.NoError(t, err) + + assert.Equal(t, heredoc.Doc(` + 1 page one + 20 page twenty + `), stdout.String(), "stdout") + assert.Equal(t, "", stderr.String(), "stderr") + + var requestData struct { + Variables map[string]interface{} + } + + bb, err := ioutil.ReadAll(responses[0].Request.Body) + require.NoError(t, err) + err = json.Unmarshal(bb, &requestData) + require.NoError(t, err) + _, hasCursor := requestData.Variables["endCursor"].(string) + assert.Equal(t, false, hasCursor) + + bb, err = ioutil.ReadAll(responses[1].Request.Body) + require.NoError(t, err) + err = json.Unmarshal(bb, &requestData) + require.NoError(t, err) + endCursor, hasCursor := requestData.Variables["endCursor"].(string) + assert.Equal(t, true, hasCursor) + assert.Equal(t, "PAGE1_END", endCursor) +} + func Test_apiRun_inputFile(t *testing.T) { tests := []struct { name string @@ -1167,10 +1263,15 @@ func Test_processResponse_template(t *testing.T) { ]`)), } - _, err := processResponse(&resp, &ApiOptions{ + opts := ApiOptions{ IO: io, Template: `{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " }}){{"\n"}}{{end}}`, - }, ioutil.Discard) + } + template := export.NewTemplate(io, opts.Template) + _, err := processResponse(&resp, &opts, ioutil.Discard, &template) + require.NoError(t, err) + + err = template.End() require.NoError(t, err) assert.Equal(t, heredoc.Doc(` diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index d2e34380b9b..b0efd75b722 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -163,7 +163,7 @@ func listRun(opts *ListOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, listResult.Issues, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, listResult.Issues) } if isTerminal { diff --git a/pkg/cmd/issue/status/status.go b/pkg/cmd/issue/status/status.go index 2a8c97ce87f..d68e2223d9b 100644 --- a/pkg/cmd/issue/status/status.go +++ b/pkg/cmd/issue/status/status.go @@ -100,7 +100,7 @@ func statusRun(opts *StatusOptions) error { "assigned": issuePayload.Assigned.Issues, "mentioned": issuePayload.Mentioned.Issues, } - return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, data) } out := opts.IO.Out diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index 3c312324c81..8821ba0999b 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -112,7 +112,7 @@ func viewRun(opts *ViewOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, issue, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, issue) } if opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 11a365cee0c..b6bce300ef9 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -163,7 +163,7 @@ func listRun(opts *ListOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, listResult.PullRequests, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, listResult.PullRequests) } if opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index d14ae5ec2ac..d8c458981bf 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -119,7 +119,7 @@ func statusRun(opts *StatusOptions) error { if prPayload.CurrentPR != nil { data["currentBranch"] = prPayload.CurrentPR } - return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, data) } out := opts.IO.Out diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index 2c390d412ea..1aa1b589cb6 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -118,7 +118,7 @@ func viewRun(opts *ViewOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, pr, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, pr) } if connectedToTerminal { diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go index a7b796bee62..0a3867aeb5b 100644 --- a/pkg/cmd/release/view/view.go +++ b/pkg/cmd/release/view/view.go @@ -109,7 +109,7 @@ func viewRun(opts *ViewOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, release, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, release) } if opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/repo/list/list.go b/pkg/cmd/repo/list/list.go index bb4ad3d531b..98f171c9af1 100644 --- a/pkg/cmd/repo/list/list.go +++ b/pkg/cmd/repo/list/list.go @@ -141,7 +141,7 @@ func listRun(opts *ListOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, listResult.Repositories, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, listResult.Repositories) } cs := opts.IO.ColorScheme() diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index 7e507c18266..e1d90d1082f 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -138,7 +138,7 @@ func viewRun(opts *ViewOptions) error { defer opts.IO.StopPager() if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO.Out, repo, opts.IO.ColorEnabled()) + return opts.Exporter.Write(opts.IO, repo) } fullName := ghrepo.FullName(toView) diff --git a/pkg/cmd/repo/view/view_test.go b/pkg/cmd/repo/view/view_test.go index b208b1f5a38..66a8b31330d 100644 --- a/pkg/cmd/repo/view/view_test.go +++ b/pkg/cmd/repo/view/view_test.go @@ -3,7 +3,6 @@ package view import ( "bytes" "fmt" - "io" "net/http" "testing" @@ -669,9 +668,9 @@ func (e *testExporter) Fields() []string { return e.fields } -func (e *testExporter) Write(w io.Writer, data interface{}, colorize bool) error { +func (e *testExporter) Write(io *iostreams.IOStreams, data interface{}) error { r := data.(*api.Repository) - fmt.Fprintf(w, "name: %s\n", r.Name) - fmt.Fprintf(w, "defaultBranchRef: %s\n", r.DefaultBranchRef.Name) + fmt.Fprintf(io.Out, "name: %s\n", r.Name) + fmt.Fprintf(io.Out, "defaultBranchRef: %s\n", r.DefaultBranchRef.Name) return nil } diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index d2d9c219a66..ca39b6d2f63 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -95,12 +95,30 @@ var HelpTopics = map[string]map[string]string{ For the syntax of Go templates, see: https://golang.org/pkg/text/template/ The following functions are available in templates: - - %[1]scolor