Have Exporter.Write automatically call ExportData on given data structure

This commit is contained in:
Mislav Marohnić 2021-05-11 18:25:20 +02:00
parent 026b07d1cf
commit 5f0301c990
10 changed files with 97 additions and 61 deletions

View file

@ -6,6 +6,7 @@ import (
)
func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
v := reflect.ValueOf(issue).Elem()
data := map[string]interface{}{}
for _, f := range fields {
@ -25,7 +26,6 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
case "projectCards":
data[f] = issue.ProjectCards.Nodes
default:
v := reflect.ValueOf(issue).Elem()
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
@ -35,6 +35,7 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
}
func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
v := reflect.ValueOf(pr).Elem()
data := map[string]interface{}{}
for _, f := range fields {
@ -75,7 +76,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
}
data[f] = &requests
default:
v := reflect.ValueOf(pr).Elem()
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
@ -84,22 +84,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
return &data
}
func ExportIssues(issues []Issue, fields []string) *[]interface{} {
data := make([]interface{}, len(issues))
for i := range issues {
data[i] = issues[i].ExportData(fields)
}
return &data
}
func ExportPRs(prs []PullRequest, fields []string) *[]interface{} {
data := make([]interface{}, len(prs))
for i := range prs {
data[i] = prs[i].ExportData(fields)
}
return &data
}
func fieldByName(v reflect.Value, field string) reflect.Value {
return v.FieldByNameFunc(func(s string) bool {
return strings.EqualFold(field, s)

View file

@ -90,31 +90,6 @@ func TestIssue_ExportData(t *testing.T) {
}
}
func TestExportIssues(t *testing.T) {
issues := []Issue{
{Milestone: Milestone{Title: "hi"}},
{},
}
exported := ExportIssues(issues, []string{"milestone"})
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
enc.SetIndent("", "\t")
require.NoError(t, enc.Encode(exported))
assert.Equal(t, heredoc.Doc(`
[
{
"milestone": {
"title": "hi"
}
},
{
"milestone": null
}
]
`), buf.String())
}
func TestPullRequest_ExportData(t *testing.T) {
tests := []struct {
name string

View file

@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
defer opts.IO.StopPager()
if opts.Exporter != nil {
data := api.ExportIssues(listResult.Issues, opts.Exporter.Fields())
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
return opts.Exporter.Write(opts.IO.Out, listResult.Issues, opts.IO.ColorEnabled())
}
if isTerminal {

View file

@ -96,11 +96,11 @@ func statusRun(opts *StatusOptions) error {
if opts.Exporter != nil {
data := map[string]interface{}{
"createdBy": api.ExportIssues(issuePayload.Authored.Issues, opts.Exporter.Fields()),
"assigned": api.ExportIssues(issuePayload.Assigned.Issues, opts.Exporter.Fields()),
"mentioned": api.ExportIssues(issuePayload.Mentioned.Issues, opts.Exporter.Fields()),
"createdBy": issuePayload.Authored.Issues,
"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.Out, data, opts.IO.ColorEnabled())
}
out := opts.IO.Out

View file

@ -116,8 +116,7 @@ func viewRun(opts *ViewOptions) error {
defer opts.IO.StopPager()
if opts.Exporter != nil {
exportIssue := issue.ExportData(opts.Exporter.Fields())
return opts.Exporter.Write(opts.IO.Out, exportIssue, opts.IO.ColorEnabled())
return opts.Exporter.Write(opts.IO.Out, issue, opts.IO.ColorEnabled())
}
if opts.IO.IsStdoutTTY() {

View file

@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
defer opts.IO.StopPager()
if opts.Exporter != nil {
data := api.ExportPRs(listResult.PullRequests, opts.Exporter.Fields())
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
return opts.Exporter.Write(opts.IO.Out, listResult.PullRequests, opts.IO.ColorEnabled())
}
if opts.IO.IsStdoutTTY() {

View file

@ -113,13 +113,13 @@ func statusRun(opts *StatusOptions) error {
if opts.Exporter != nil {
data := map[string]interface{}{
"currentBranch": nil,
"createdBy": api.ExportPRs(prPayload.ViewerCreated.PullRequests, opts.Exporter.Fields()),
"needsReview": api.ExportPRs(prPayload.ReviewRequested.PullRequests, opts.Exporter.Fields()),
"createdBy": prPayload.ViewerCreated.PullRequests,
"needsReview": prPayload.ReviewRequested.PullRequests,
}
if prPayload.CurrentPR != nil {
data["currentBranch"] = prPayload.CurrentPR.ExportData(opts.Exporter.Fields())
data["currentBranch"] = prPayload.CurrentPR
}
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
}
out := opts.IO.Out

View file

@ -117,8 +117,7 @@ func viewRun(opts *ViewOptions) error {
defer opts.IO.StopPager()
if opts.Exporter != nil {
exportPR := pr.ExportData(opts.Exporter.Fields())
return opts.Exporter.Write(opts.IO.Out, exportPR, opts.IO.ColorEnabled())
return opts.Exporter.Write(opts.IO.Out, pr, opts.IO.ColorEnabled())
}
if connectedToTerminal {

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"reflect"
"sort"
"strings"
@ -102,11 +103,14 @@ func (e *exportFormat) Fields() []string {
return e.fields
}
// Write serializes data into JSON output written to w. If the object passed as data implements exportable,
// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain
// raw data for serialization.
func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
buf := bytes.Buffer{}
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(data); err != nil {
if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil {
return err
}
@ -121,3 +125,44 @@ func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) e
_, err := io.Copy(w, &buf)
return err
}
func (e *exportFormat) exportData(v reflect.Value) interface{} {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
if !v.IsNil() {
return e.exportData(v.Elem())
}
case reflect.Slice:
a := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
a[i] = e.exportData(v.Index(i))
}
return a
case reflect.Map:
t := reflect.MapOf(v.Type().Key(), emptyInterfaceType)
m := reflect.MakeMapWithSize(t, v.Len())
iter := v.MapRange()
for iter.Next() {
ve := reflect.ValueOf(e.exportData(iter.Value()))
m.SetMapIndex(iter.Key(), ve)
}
return m.Interface()
case reflect.Struct:
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
ve := v.Addr().Interface().(exportable)
return ve.ExportData(e.fields)
} else if v.Type().Implements(exportableType) {
ve := v.Interface().(exportable)
return ve.ExportData(e.fields)
}
}
return v.Interface()
}
type exportable interface {
ExportData([]string) *map[string]interface{}
}
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
var sliceOfEmptyInterface []interface{}
var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem()

View file

@ -2,6 +2,7 @@ package cmdutil
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
@ -137,6 +138,29 @@ func Test_exportFormat_Write(t *testing.T) {
wantW: "{\"name\":\"hubot\"}\n",
wantErr: false,
},
{
name: "call ExportData",
exporter: exportFormat{fields: []string{"field1", "field2"}},
args: args{
data: &exportableItem{"item1"},
colorEnabled: false,
},
wantW: "{\"field1\":\"item1:field1\",\"field2\":\"item1:field2\"}\n",
wantErr: false,
},
{
name: "recursively call ExportData",
exporter: exportFormat{fields: []string{"f1", "f2"}},
args: args{
data: map[string]interface{}{
"s1": []exportableItem{{"i1"}, {"i2"}},
"s2": []exportableItem{{"i3"}},
},
colorEnabled: false,
},
wantW: "{\"s1\":[{\"f1\":\"i1:f1\",\"f2\":\"i1:f2\"},{\"f1\":\"i2:f1\",\"f2\":\"i2:f2\"}],\"s2\":[{\"f1\":\"i3:f1\",\"f2\":\"i3:f2\"}]}\n",
wantErr: false,
},
{
name: "with jq filter",
exporter: exportFormat{filter: ".name"},
@ -166,8 +190,20 @@ func Test_exportFormat_Write(t *testing.T) {
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("exportFormat.Write() = %v, want %v", gotW, tt.wantW)
t.Errorf("exportFormat.Write() = %q, want %q", gotW, tt.wantW)
}
})
}
}
type exportableItem struct {
Name string
}
func (e *exportableItem) ExportData(fields []string) *map[string]interface{} {
m := map[string]interface{}{}
for _, f := range fields {
m[f] = fmt.Sprintf("%s:%s", e.Name, f)
}
return &m
}