X Tutup
Skip to content

Commit 301a35e

Browse files
authored
Merge pull request cli#3621 from cli/export-data
Push data serialization concern into Exporter
2 parents f2456f4 + 5f0301c commit 301a35e

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

@@ -117,11 +118,14 @@ func (e *exportFormat) Fields() []string {
117118
return e.fields
118119
}
119120

121+
// Write serializes data into JSON output written to w. If the object passed as data implements exportable,
122+
// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain
123+
// raw data for serialization.
120124
func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
121125
buf := bytes.Buffer{}
122126
encoder := json.NewEncoder(&buf)
123127
encoder.SetEscapeHTML(false)
124-
if err := encoder.Encode(data); err != nil {
128+
if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil {
125129
return err
126130
}
127131

@@ -136,3 +140,44 @@ func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) e
136140
_, err := io.Copy(w, &buf)
137141
return err
138142
}
143+
144+
func (e *exportFormat) exportData(v reflect.Value) interface{} {
145+
switch v.Kind() {
146+
case reflect.Ptr, reflect.Interface:
147+
if !v.IsNil() {
148+
return e.exportData(v.Elem())
149+
}
150+
case reflect.Slice:
151+
a := make([]interface{}, v.Len())
152+
for i := 0; i < v.Len(); i++ {
153+
a[i] = e.exportData(v.Index(i))
154+
}
155+
return a
156+
case reflect.Map:
157+
t := reflect.MapOf(v.Type().Key(), emptyInterfaceType)
158+
m := reflect.MakeMapWithSize(t, v.Len())
159+
iter := v.MapRange()
160+
for iter.Next() {
161+
ve := reflect.ValueOf(e.exportData(iter.Value()))
162+
m.SetMapIndex(iter.Key(), ve)
163+
}
164+
return m.Interface()
165+
case reflect.Struct:
166+
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
167+
ve := v.Addr().Interface().(exportable)
168+
return ve.ExportData(e.fields)
169+
} else if v.Type().Implements(exportableType) {
170+
ve := v.Interface().(exportable)
171+
return ve.ExportData(e.fields)
172+
}
173+
}
174+
return v.Interface()
175+
}
176+
177+
type exportable interface {
178+
ExportData([]string) *map[string]interface{}
179+
}
180+
181+
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
182+
var sliceOfEmptyInterface []interface{}
183+
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)
X Tutup