-
Notifications
You must be signed in to change notification settings - Fork 152
/
marshal.go
466 lines (391 loc) · 12.5 KB
/
marshal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
package human
import (
"bytes"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/fatih/color"
"github.com/scaleway/scaleway-cli/v2/internal/gofields"
"github.com/scaleway/scaleway-cli/v2/internal/tabwriter"
"github.com/scaleway/scaleway-cli/v2/internal/terminal"
"github.com/scaleway/scaleway-sdk-go/logger"
"github.com/scaleway/scaleway-sdk-go/strcase"
)
// Padding between column
const colPadding = 2
// Marshaler allow custom display for some type when printed using HumanPrinter
type Marshaler interface {
MarshalHuman() (string, error)
}
func Marshal(data interface{}, opt *MarshalOpt) (string, error) {
if opt == nil {
opt = &MarshalOpt{}
}
if opt.Title != "" {
subOpt := *opt
subOpt.Title = ""
body, err := Marshal(data, &subOpt)
return terminal.Style(opt.Title+":", color.Bold) + "\n" + body, err
}
rValue := reflect.ValueOf(data)
if !rValue.IsValid() {
return defaultMarshalerFunc(nil, opt)
}
rType := rValue.Type()
// safely get the marshalerFunc
marshalerFunc, _ := getMarshalerFunc(rType)
isNil := isInterfaceNil(data)
isSlice := rType.Kind() == reflect.Slice
switch {
// If data is nil and is not a slice ( In case of a slice we want to print header row and not a simple dash )
case isNil && !isSlice:
return defaultMarshalerFunc(nil, opt)
// If data has a registered MarshalerFunc call it
case marshalerFunc != nil:
return marshalerFunc(rValue.Interface(), opt)
// Handle special well known interface
case rType.Implements(reflect.TypeOf((*Marshaler)(nil)).Elem()):
return rValue.Interface().(Marshaler).MarshalHuman()
// Handle errors
case rType.Implements(reflect.TypeOf((*error)(nil)).Elem()):
return terminal.Style(Capitalize(rValue.Interface().(error).Error()), color.FgRed), nil
// Handle stringers
case rType.Implements(reflect.TypeOf((*fmt.Stringer)(nil)).Elem()):
return rValue.Interface().(fmt.Stringer).String(), nil
// If data is a pointer dereference an call Marshal again
case rType.Kind() == reflect.Ptr:
return Marshal(rValue.Elem().Interface(), opt)
// If data is a slice uses marshalSlice
case isSlice:
return marshalSlice(rValue, opt)
// If data is a struct uses marshalStruct
case rType.Kind() == reflect.Struct:
return marshalStruct(rValue, opt)
// by default we use defaultMarshalerFunc
default:
return defaultMarshalerFunc(rValue.Interface(), opt)
}
}
func marshalStruct(value reflect.Value, opt *MarshalOpt) (string, error) {
// subOpts that will be passed down to sub marshaler (field, sub struct, slice item ...)
subOpts := &MarshalOpt{}
// Marshal sections
sectionsStrs := []string(nil)
sectionFieldNames := map[string]bool{}
for _, section := range opt.Sections {
sectionStr, err := marshalSection(section, value, subOpts)
if err != nil {
return "", err
}
sectionFieldNames[section.FieldName] = true
if sectionStr != "" {
sectionsStrs = append(sectionsStrs, sectionStr)
}
}
var marshal func(reflect.Value, []string) ([][]string, error)
marshal = func(value reflect.Value, keys []string) ([][]string, error) {
if _, isSection := sectionFieldNames[strings.Join(keys, ".")]; isSection {
return nil, nil
}
rType := value.Type()
// safely get the marshaler func
marshalerFunc, _ := getMarshalerFunc(rType)
switch {
// If data is nil
case isInterfaceNil(value.Interface()):
return nil, nil
// If data has a registered MarshalerFunc call it.
case marshalerFunc != nil:
str, err := marshalerFunc(value.Interface(), subOpts)
return [][]string{{strings.Join(keys, "."), str}}, err
// If data is a stringers
case rType.Implements(reflect.TypeOf((*fmt.Stringer)(nil)).Elem()):
return [][]string{{strings.Join(keys, "."), value.Interface().(fmt.Stringer).String()}}, nil
case rType.Kind() == reflect.Ptr:
// If type is a pointer we Marshal pointer.Elem()
return marshal(value.Elem(), keys)
case rType.Kind() == reflect.Slice:
// If type is a slice:
// We loop through all items and marshal them with key = key.0, key.1, ....
data := [][]string(nil)
for i := 0; i < value.Len(); i++ {
subData, err := marshal(value.Index(i), append(keys, strconv.Itoa(i)))
if err != nil {
return nil, err
}
data = append(data, subData...)
}
return data, nil
case rType.Kind() == reflect.Map:
// If type is a map:
// We loop through all items and marshal them with key = key.0, key.1, ....
data := [][]string(nil)
// Get all map keys and sort them. We assume keys are string
mapKeys := value.MapKeys()
sort.Slice(mapKeys, func(i, j int) bool {
return mapKeys[i].String() < mapKeys[j].String()
})
for _, mapKey := range mapKeys {
mapValue := value.MapIndex(mapKey)
subData, err := marshal(mapValue, append(keys, mapKey.String()))
if err != nil {
return nil, err
}
data = append(data, subData...)
}
return data, nil
case rType.Kind() == reflect.Struct:
// If type is a struct
// We loop through all struct field
data := [][]string(nil)
for _, fieldIndex := range getStructFieldsIndex(value.Type()) {
subData, err := marshal(value.FieldByIndex(fieldIndex), append(keys, value.Type().FieldByIndex(fieldIndex).Name))
if err != nil {
return nil, err
}
data = append(data, subData...)
}
return data, nil
case rType.Kind() == reflect.Interface:
// If type is interface{}
// marshal the underlying type
return marshal(value.Elem(), keys)
default:
str, err := defaultMarshalerFunc(value.Interface(), subOpts)
if err != nil {
return nil, err
}
return [][]string{{strings.Join(keys, "."), str}}, nil
}
}
data, err := marshal(value, nil)
if err != nil {
return "", err
}
buffer := bytes.Buffer{}
w := tabwriter.NewWriter(&buffer, 5, 1, colPadding, ' ', tabwriter.ANSIGraphicsRendition)
for _, line := range data {
fmt.Fprintln(w, strings.Join(line, "\t"))
}
if len(sectionsStrs) > 0 {
fmt.Fprintln(w, "\n"+strings.Join(sectionsStrs, "\n\n"))
}
w.Flush()
return strings.TrimSpace(buffer.String()), nil
}
// getStructFieldsIndex will return a list of fieldIndex ([]int) sorted by their position in the Go struct.
// This function will handle anonymous field and make sure that if a field is overwritten only the highest is returned.
// You can use reflect GetFieldByIndex([]int) to get the correct field.
func getStructFieldsIndex(v reflect.Type) [][]int {
// Using a map we make sure only the field with the highest order is returned for a given Name
found := map[string][]int{}
var recFunc func(v reflect.Type, parent []int)
recFunc = func(v reflect.Type, parent []int) {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
// If a field is anonymous we start recursive call
if field.Anonymous {
recFunc(v.Field(i).Type, append(parent, i))
} else {
// else we add the field in the found map
found[field.Name] = append(parent, i)
}
}
}
recFunc(v, []int(nil))
result := [][]int(nil)
for _, value := range found {
result = append(result, value)
}
sort.Slice(result, func(i, j int) bool {
n := 0
for n < len(result[i]) && n < len(result[j]) {
if result[i][n] != result[j][n] {
return result[i][n] < result[j][n]
}
n++
}
// if equal, less should be false
return false
})
return result
}
func marshalSlice(slice reflect.Value, opt *MarshalOpt) (string, error) {
// Resole itemType and get rid of all pointer level if needed.
itemType := slice.Type().Elem()
for itemType.Kind() == reflect.Ptr {
itemType = itemType.Elem()
}
// If itemType is not a struct (e.g []string) we just stringify it
if itemType.Kind() != reflect.Struct {
return fmt.Sprint(slice.Interface()), nil
}
// If there is no Field in opt we generated default one using reflect
if len(opt.Fields) == 0 {
opt.Fields = getDefaultFieldsOpt(itemType)
}
subOpts := &MarshalOpt{TableCell: true}
// Validate that all field exist
for _, f := range opt.Fields {
_, err := gofields.GetType(itemType, f.FieldName)
if err != nil {
return "", &UnknownFieldError{
FieldName: f.FieldName,
ValidFields: gofields.ListFields(itemType),
}
}
}
// We create a in memory grid of the content we want to print
grid := make([][]string, 0, slice.Len()+1)
// Generate header row
headerRow := []string(nil)
for _, fieldSpec := range opt.Fields {
headerRow = append(headerRow, fieldSpec.getLabel())
}
grid = append(grid, headerRow)
// For each item in the slice
for i := 0; i < slice.Len(); i++ {
item := slice.Index(i)
row := []string(nil)
for _, fieldSpec := range opt.Fields {
v, err := gofields.GetValue(item.Interface(), fieldSpec.FieldName)
if err != nil {
logger.Debugf("invalid getFieldValue(): '%v' might not be exported", fieldSpec.FieldName)
row = append(row, "")
continue
}
fieldValue := reflect.ValueOf(v)
str := ""
switch {
// Handle inline slice.
case fieldValue.Type().Kind() == reflect.Slice:
str, err = marshalInlineSlice(fieldValue)
default:
str, err = Marshal(fieldValue.Interface(), subOpts)
}
if err != nil {
return "", err
}
row = append(row, str)
}
grid = append(grid, row)
}
return formatGrid(grid, !opt.DisableShrinking)
}
// marshalInlineSlice transforms nested scalar slices in an inline string representation
// and other types of slices in a count representation.
func marshalInlineSlice(slice reflect.Value) (string, error) {
if slice.IsNil() {
return defaultMarshalerFunc(nil, nil)
}
itemType := slice.Type().Elem()
for itemType.Kind() == reflect.Ptr {
itemType = itemType.Elem()
}
// safely get the marshalerFunc
marshalerFunc, _ := getMarshalerFunc(slice.Type())
switch {
// If marshaler func is available.
// As we cannot set MarshalOpt of a nested slice opt will always be nil here.
case marshalerFunc != nil:
return marshalerFunc(slice.Interface(), nil)
// If it is a slice of scalar values.
case itemType.Kind() != reflect.Slice &&
itemType.Kind() != reflect.Map &&
itemType.Kind() != reflect.Struct:
return fmt.Sprint(slice), nil
// Use slice count by default.
default:
return strconv.Itoa(slice.Len()), nil
}
}
// marshalSection transforms a field from a struct into a section.
func marshalSection(section *MarshalSection, value reflect.Value, opt *MarshalOpt) (string, error) {
subOpt := *opt
title := section.Title
if title == "" {
title = strings.ReplaceAll(strcase.ToBashArg(section.FieldName), "-", " ")
title = cases.Title(language.English).String(strings.ReplaceAll(title, ".", " - "))
}
subOpt.Title = title
field, err := gofields.GetValue(value.Interface(), section.FieldName)
if err != nil {
if section.HideIfEmpty {
if _, ok := err.(*gofields.NilValueError); ok {
return "", nil
}
}
return "", err
}
if section.HideIfEmpty && reflect.ValueOf(field).IsZero() {
return "", nil
}
return Marshal(field, &subOpt)
}
func formatGrid(grid [][]string, shrinkColumns bool) (string, error) {
buffer := bytes.Buffer{}
maxCols := computeMaxCols(grid)
w := tabwriter.NewWriter(&buffer, 5, 1, colPadding, ' ', tabwriter.ANSIGraphicsRendition)
for _, line := range grid {
if shrinkColumns {
line = line[:maxCols]
}
fmt.Fprintln(w, strings.Join(line, "\t"))
}
w.Flush()
return strings.TrimSpace(buffer.String()), nil
}
// computeMaxCols calculates how many row we can fit in terminal width.
func computeMaxCols(grid [][]string) int {
maxCols := len(grid[0])
width := terminal.GetWidth()
// If we are not writing to Stdout or through a tty Stdout, returns max length
if !terminal.IsTerm() || width == 0 {
return maxCols
}
colMaxSize := make([]int, len(grid[0]))
for i := 0; i < len(grid); i++ {
lineSize := 0
for j := 0; j < maxCols; j++ {
size := len(grid[i][j]) + colPadding
if size >= colMaxSize[j] {
colMaxSize[j] = size
}
lineSize += colMaxSize[j]
if lineSize > width {
maxCols = j
}
}
}
return maxCols
}
// Generate default []*MarshalFieldOpt using reflect
// It will detect item type of a slice an keep all root level field that are marshalable
func getDefaultFieldsOpt(t reflect.Type) []*MarshalFieldOpt {
results := []*MarshalFieldOpt(nil)
// Loop through all struct field
for fieldIdx := 0; fieldIdx < t.NumField(); fieldIdx++ {
field := t.Field(fieldIdx)
fieldType := field.Type
if field.Anonymous {
results = append(results, getDefaultFieldsOpt(fieldType)...)
continue
}
if isMarshalable(fieldType) {
spec := &MarshalFieldOpt{
FieldName: field.Name,
}
results = append(results, spec)
} else {
logger.Debugf("fieldType '%v' is not marshallable", fieldType)
}
}
return results
}