Skip to content

Commit 5f0301c

Browse files
committed
Have Exporter.Write automatically call ExportData on given data structure
1 parent 026b07d commit 5f0301c

File tree

10 files changed

+97
-61
lines changed

10 files changed

+97
-61
lines changed

api/export_pr.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
)
77

88
func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
9+
v := reflect.ValueOf(issue).Elem()
910
data := map[string]interface{}{}
1011

1112
for _, f := range fields {
@@ -25,7 +26,6 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
2526
case "projectCards":
2627
data[f] = issue.ProjectCards.Nodes
2728
default:
28-
v := reflect.ValueOf(issue).Elem()
2929
sf := fieldByName(v, f)
3030
data[f] = sf.Interface()
3131
}
@@ -35,6 +35,7 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
3535
}
3636

3737
func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
38+
v := reflect.ValueOf(pr).Elem()
3839
data := map[string]interface{}{}
3940

4041
for _, f := range fields {
@@ -75,7 +76,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
7576
}
7677
data[f] = &requests
7778
default:
78-
v := reflect.ValueOf(pr).Elem()
7979
sf := fieldByName(v, f)
8080
data[f] = sf.Interface()
8181
}
@@ -84,22 +84,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
8484
return &data
8585
}
8686

87-
func ExportIssues(issues []Issue, fields []string) *[]interface{} {
88-
data := make([]interface{}, len(issues))
89-
for i := range issues {
90-
data[i] = issues[i].ExportData(fields)
91-
}
92-
return &data
93-
}
94-
95-
func ExportPRs(prs []PullRequest, fields []string) *[]interface{} {
96-
data := make([]interface{}, len(prs))
97-
for i := range prs {
98-
data[i] = prs[i].ExportData(fields)
99-
}
100-
return &data
101-
}
102-
10387
func fieldByName(v reflect.Value, field string) reflect.Value {
10488
return v.FieldByNameFunc(func(s string) bool {
10589
return strings.EqualFold(field, s)

api/export_pr_test.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,6 @@ func TestIssue_ExportData(t *testing.T) {
9090
}
9191
}
9292

93-
func TestExportIssues(t *testing.T) {
94-
issues := []Issue{
95-
{Milestone: Milestone{Title: "hi"}},
96-
{},
97-
}
98-
exported := ExportIssues(issues, []string{"milestone"})
99-
100-
buf := bytes.Buffer{}
101-
enc := json.NewEncoder(&buf)
102-
enc.SetIndent("", "\t")
103-
require.NoError(t, enc.Encode(exported))
104-
assert.Equal(t, heredoc.Doc(`
105-
[
106-
{
107-
"milestone": {
108-
"title": "hi"
109-
}
110-
},
111-
{
112-
"milestone": null
113-
}
114-
]
115-
`), buf.String())
116-
}
117-
11893
func TestPullRequest_ExportData(t *testing.T) {
11994
tests := []struct {
12095
name string

pkg/cmd/issue/list/list.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
155155
defer opts.IO.StopPager()
156156

157157
if opts.Exporter != nil {
158-
data := api.ExportIssues(listResult.Issues, opts.Exporter.Fields())
159-
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
158+
return opts.Exporter.Write(opts.IO.Out, listResult.Issues, opts.IO.ColorEnabled())
160159
}
161160

162161
if isTerminal {

pkg/cmd/issue/status/status.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ func statusRun(opts *StatusOptions) error {
9696

9797
if opts.Exporter != nil {
9898
data := map[string]interface{}{
99-
"createdBy": api.ExportIssues(issuePayload.Authored.Issues, opts.Exporter.Fields()),
100-
"assigned": api.ExportIssues(issuePayload.Assigned.Issues, opts.Exporter.Fields()),
101-
"mentioned": api.ExportIssues(issuePayload.Mentioned.Issues, opts.Exporter.Fields()),
99+
"createdBy": issuePayload.Authored.Issues,
100+
"assigned": issuePayload.Assigned.Issues,
101+
"mentioned": issuePayload.Mentioned.Issues,
102102
}
103-
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
103+
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
104104
}
105105

106106
out := opts.IO.Out

pkg/cmd/issue/view/view.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ func viewRun(opts *ViewOptions) error {
116116
defer opts.IO.StopPager()
117117

118118
if opts.Exporter != nil {
119-
exportIssue := issue.ExportData(opts.Exporter.Fields())
120-
return opts.Exporter.Write(opts.IO.Out, exportIssue, opts.IO.ColorEnabled())
119+
return opts.Exporter.Write(opts.IO.Out, issue, opts.IO.ColorEnabled())
121120
}
122121

123122
if opts.IO.IsStdoutTTY() {

pkg/cmd/pr/list/list.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
155155
defer opts.IO.StopPager()
156156

157157
if opts.Exporter != nil {
158-
data := api.ExportPRs(listResult.PullRequests, opts.Exporter.Fields())
159-
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
158+
return opts.Exporter.Write(opts.IO.Out, listResult.PullRequests, opts.IO.ColorEnabled())
160159
}
161160

162161
if opts.IO.IsStdoutTTY() {

pkg/cmd/pr/status/status.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,13 @@ func statusRun(opts *StatusOptions) error {
113113
if opts.Exporter != nil {
114114
data := map[string]interface{}{
115115
"currentBranch": nil,
116-
"createdBy": api.ExportPRs(prPayload.ViewerCreated.PullRequests, opts.Exporter.Fields()),
117-
"needsReview": api.ExportPRs(prPayload.ReviewRequested.PullRequests, opts.Exporter.Fields()),
116+
"createdBy": prPayload.ViewerCreated.PullRequests,
117+
"needsReview": prPayload.ReviewRequested.PullRequests,
118118
}
119119
if prPayload.CurrentPR != nil {
120-
data["currentBranch"] = prPayload.CurrentPR.ExportData(opts.Exporter.Fields())
120+
data["currentBranch"] = prPayload.CurrentPR
121121
}
122-
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
122+
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
123123
}
124124

125125
out := opts.IO.Out

pkg/cmd/pr/view/view.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ func viewRun(opts *ViewOptions) error {
117117
defer opts.IO.StopPager()
118118

119119
if opts.Exporter != nil {
120-
exportPR := pr.ExportData(opts.Exporter.Fields())
121-
return opts.Exporter.Write(opts.IO.Out, exportPR, opts.IO.ColorEnabled())
120+
return opts.Exporter.Write(opts.IO.Out, pr, opts.IO.ColorEnabled())
122121
}
123122

124123
if connectedToTerminal {

pkg/cmdutil/json_flags.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"reflect"
910
"sort"
1011
"strings"
1112

@@ -102,11 +103,14 @@ func (e *exportFormat) Fields() []string {
102103
return e.fields
103104
}
104105

106+
// Write serializes data into JSON output written to w. If the object passed as data implements exportable,
107+
// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain
108+
// raw data for serialization.
105109
func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
106110
buf := bytes.Buffer{}
107111
encoder := json.NewEncoder(&buf)
108112
encoder.SetEscapeHTML(false)
109-
if err := encoder.Encode(data); err != nil {
113+
if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil {
110114
return err
111115
}
112116

@@ -121,3 +125,44 @@ func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) e
121125
_, err := io.Copy(w, &buf)
122126
return err
123127
}
128+
129+
func (e *exportFormat) exportData(v reflect.Value) interface{} {
130+
switch v.Kind() {
131+
case reflect.Ptr, reflect.Interface:
132+
if !v.IsNil() {
133+
return e.exportData(v.Elem())
134+
}
135+
case reflect.Slice:
136+
a := make([]interface{}, v.Len())
137+
for i := 0; i < v.Len(); i++ {
138+
a[i] = e.exportData(v.Index(i))
139+
}
140+
return a
141+
case reflect.Map:
142+
t := reflect.MapOf(v.Type().Key(), emptyInterfaceType)
143+
m := reflect.MakeMapWithSize(t, v.Len())
144+
iter := v.MapRange()
145+
for iter.Next() {
146+
ve := reflect.ValueOf(e.exportData(iter.Value()))
147+
m.SetMapIndex(iter.Key(), ve)
148+
}
149+
return m.Interface()
150+
case reflect.Struct:
151+
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
152+
ve := v.Addr().Interface().(exportable)
153+
return ve.ExportData(e.fields)
154+
} else if v.Type().Implements(exportableType) {
155+
ve := v.Interface().(exportable)
156+
return ve.ExportData(e.fields)
157+
}
158+
}
159+
return v.Interface()
160+
}
161+
162+
type exportable interface {
163+
ExportData([]string) *map[string]interface{}
164+
}
165+
166+
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
167+
var sliceOfEmptyInterface []interface{}
168+
var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem()

pkg/cmdutil/json_flags_test.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmdutil
22

33
import (
44
"bytes"
5+
"fmt"
56
"io/ioutil"
67
"testing"
78

@@ -137,6 +138,29 @@ func Test_exportFormat_Write(t *testing.T) {
137138
wantW: "{\"name\":\"hubot\"}\n",
138139
wantErr: false,
139140
},
141+
{
142+
name: "call ExportData",
143+
exporter: exportFormat{fields: []string{"field1", "field2"}},
144+
args: args{
145+
data: &exportableItem{"item1"},
146+
colorEnabled: false,
147+
},
148+
wantW: "{\"field1\":\"item1:field1\",\"field2\":\"item1:field2\"}\n",
149+
wantErr: false,
150+
},
151+
{
152+
name: "recursively call ExportData",
153+
exporter: exportFormat{fields: []string{"f1", "f2"}},
154+
args: args{
155+
data: map[string]interface{}{
156+
"s1": []exportableItem{{"i1"}, {"i2"}},
157+
"s2": []exportableItem{{"i3"}},
158+
},
159+
colorEnabled: false,
160+
},
161+
wantW: "{\"s1\":[{\"f1\":\"i1:f1\",\"f2\":\"i1:f2\"},{\"f1\":\"i2:f1\",\"f2\":\"i2:f2\"}],\"s2\":[{\"f1\":\"i3:f1\",\"f2\":\"i3:f2\"}]}\n",
162+
wantErr: false,
163+
},
140164
{
141165
name: "with jq filter",
142166
exporter: exportFormat{filter: ".name"},
@@ -166,8 +190,20 @@ func Test_exportFormat_Write(t *testing.T) {
166190
return
167191
}
168192
if gotW := w.String(); gotW != tt.wantW {
169-
t.Errorf("exportFormat.Write() = %v, want %v", gotW, tt.wantW)
193+
t.Errorf("exportFormat.Write() = %q, want %q", gotW, tt.wantW)
170194
}
171195
})
172196
}
173197
}
198+
199+
type exportableItem struct {
200+
Name string
201+
}
202+
203+
func (e *exportableItem) ExportData(fields []string) *map[string]interface{} {
204+
m := map[string]interface{}{}
205+
for _, f := range fields {
206+
m[f] = fmt.Sprintf("%s:%s", e.Name, f)
207+
}
208+
return &m
209+
}

0 commit comments

Comments
 (0)