@@ -22,6 +22,13 @@ func Table() table.Writer {
22
22
return tableWriter
23
23
}
24
24
25
+ // This type can be supplied as part of a slice to DisplayTable
26
+ // or to a `TableFormat` `Format` call to render a separator.
27
+ // Leading separators are not supported and trailing separators
28
+ // are ignored by the table formatter.
29
+ // e.g. `[]any{someRow, TableSeparator, someRow}`
30
+ type TableSeparator struct {}
31
+
25
32
// filterTableColumns returns configurations to hide columns
26
33
// that are not provided in the array. If the array is empty,
27
34
// no filtering will occur!
@@ -47,8 +54,12 @@ func filterTableColumns(header table.Row, columns []string) []table.ColumnConfig
47
54
return columnConfigs
48
55
}
49
56
50
- // DisplayTable renders a table as a string. The input argument must be a slice
51
- // of structs. At least one field in the struct must have a `table:""` tag
57
+ // DisplayTable renders a table as a string. The input argument can be:
58
+ // - a struct slice.
59
+ // - an interface slice, where the first element is a struct,
60
+ // and all other elements are of the same type, or a TableSeparator.
61
+ //
62
+ // At least one field in the struct must have a `table:""` tag
52
63
// containing the name of the column in the outputted table.
53
64
//
54
65
// If `sort` is not specified, the field with the `table:"$NAME,default_sort"`
@@ -66,11 +77,20 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
66
77
v := reflect .Indirect (reflect .ValueOf (out ))
67
78
68
79
if v .Kind () != reflect .Slice {
69
- return "" , xerrors .Errorf ("DisplayTable called with a non-slice type" )
80
+ return "" , xerrors .New ("DisplayTable called with a non-slice type" )
81
+ }
82
+ var tableType reflect.Type
83
+ if v .Type ().Elem ().Kind () == reflect .Interface {
84
+ if v .Len () == 0 {
85
+ return "" , xerrors .New ("DisplayTable called with empty interface slice" )
86
+ }
87
+ tableType = reflect .Indirect (reflect .ValueOf (v .Index (0 ).Interface ())).Type ()
88
+ } else {
89
+ tableType = v .Type ().Elem ()
70
90
}
71
91
72
92
// Get the list of table column headers.
73
- headersRaw , defaultSort , err := typeToTableHeaders (v . Type (). Elem () , true )
93
+ headersRaw , defaultSort , err := typeToTableHeaders (tableType , true )
74
94
if err != nil {
75
95
return "" , xerrors .Errorf ("get table headers recursively for type %q: %w" , v .Type ().Elem ().String (), err )
76
96
}
@@ -82,9 +102,8 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
82
102
}
83
103
headers := make (table.Row , len (headersRaw ))
84
104
for i , header := range headersRaw {
85
- headers [i ] = header
105
+ headers [i ] = strings . ReplaceAll ( header , "_" , " " )
86
106
}
87
-
88
107
// Verify that the given sort column and filter columns are valid.
89
108
if sort != "" || len (filterColumns ) != 0 {
90
109
headersMap := make (map [string ]string , len (headersRaw ))
@@ -130,6 +149,11 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
130
149
return "" , xerrors .Errorf ("specified sort column %q not found in table headers, available columns are %q" , sort , strings .Join (headersRaw , `", "` ))
131
150
}
132
151
}
152
+ return renderTable (out , sort , headers , filterColumns )
153
+ }
154
+
155
+ func renderTable (out any , sort string , headers table.Row , filterColumns []string ) (string , error ) {
156
+ v := reflect .Indirect (reflect .ValueOf (out ))
133
157
134
158
// Setup the table formatter.
135
159
tw := Table ()
@@ -143,15 +167,22 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
143
167
144
168
// Write each struct to the table.
145
169
for i := 0 ; i < v .Len (); i ++ {
170
+ cur := v .Index (i ).Interface ()
171
+ _ , ok := cur .(TableSeparator )
172
+ if ok {
173
+ tw .AppendSeparator ()
174
+ continue
175
+ }
146
176
// Format the row as a slice.
147
- rowMap , err := valueToTableMap (v .Index (i ))
177
+ // ValueToTableMap does what `reflect.Indirect` does
178
+ rowMap , err := valueToTableMap (reflect .ValueOf (cur ))
148
179
if err != nil {
149
180
return "" , xerrors .Errorf ("get table row map %v: %w" , i , err )
150
181
}
151
182
152
183
rowSlice := make ([]any , len (headers ))
153
- for i , h := range headersRaw {
154
- v , ok := rowMap [h ]
184
+ for i , h := range headers {
185
+ v , ok := rowMap [h .( string ) ]
155
186
if ! ok {
156
187
v = nil
157
188
}
@@ -188,25 +219,28 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
188
219
// returned. If the table tag is malformed, an error is returned.
189
220
//
190
221
// The returned name is transformed from "snake_case" to "normal text".
191
- func parseTableStructTag (field reflect.StructField ) (name string , defaultSort , recursive bool , skipParentName bool , err error ) {
222
+ func parseTableStructTag (field reflect.StructField ) (name string , defaultSort , noSortOpt , recursive , skipParentName bool , err error ) {
192
223
tags , err := structtag .Parse (string (field .Tag ))
193
224
if err != nil {
194
- return "" , false , false , false , xerrors .Errorf ("parse struct field tag %q: %w" , string (field .Tag ), err )
225
+ return "" , false , false , false , false , xerrors .Errorf ("parse struct field tag %q: %w" , string (field .Tag ), err )
195
226
}
196
227
197
228
tag , err := tags .Get ("table" )
198
229
if err != nil || tag .Name == "-" {
199
230
// tags.Get only returns an error if the tag is not found.
200
- return "" , false , false , false , nil
231
+ return "" , false , false , false , false , nil
201
232
}
202
233
203
234
defaultSortOpt := false
235
+ noSortOpt = false
204
236
recursiveOpt := false
205
237
skipParentNameOpt := false
206
238
for _ , opt := range tag .Options {
207
239
switch opt {
208
240
case "default_sort" :
209
241
defaultSortOpt = true
242
+ case "nosort" :
243
+ noSortOpt = true
210
244
case "recursive" :
211
245
recursiveOpt = true
212
246
case "recursive_inline" :
@@ -216,11 +250,11 @@ func parseTableStructTag(field reflect.StructField) (name string, defaultSort, r
216
250
recursiveOpt = true
217
251
skipParentNameOpt = true
218
252
default :
219
- return "" , false , false , false , xerrors .Errorf ("unknown option %q in struct field tag" , opt )
253
+ return "" , false , false , false , false , xerrors .Errorf ("unknown option %q in struct field tag" , opt )
220
254
}
221
255
}
222
256
223
- return strings .ReplaceAll (tag .Name , "_" , " " ), defaultSortOpt , recursiveOpt , skipParentNameOpt , nil
257
+ return strings .ReplaceAll (tag .Name , "_" , " " ), defaultSortOpt , noSortOpt , recursiveOpt , skipParentNameOpt , nil
224
258
}
225
259
226
260
func isStructOrStructPointer (t reflect.Type ) bool {
@@ -244,12 +278,16 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
244
278
245
279
headers := []string {}
246
280
defaultSortName := ""
281
+ noSortOpt := false
247
282
for i := 0 ; i < t .NumField (); i ++ {
248
283
field := t .Field (i )
249
- name , defaultSort , recursive , skip , err := parseTableStructTag (field )
284
+ name , defaultSort , noSort , recursive , skip , err := parseTableStructTag (field )
250
285
if err != nil {
251
286
return nil , "" , xerrors .Errorf ("parse struct tags for field %q in type %q: %w" , field .Name , t .String (), err )
252
287
}
288
+ if requireDefault && noSort {
289
+ noSortOpt = true
290
+ }
253
291
254
292
if name == "" && (recursive && skip ) {
255
293
return nil , "" , xerrors .Errorf ("a name is required for the field %q. " +
@@ -292,8 +330,8 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
292
330
headers = append (headers , name )
293
331
}
294
332
295
- if defaultSortName == "" && requireDefault {
296
- return nil , "" , xerrors .Errorf ("no field marked as default_sort in type %q" , t .String ())
333
+ if defaultSortName == "" && requireDefault && ! noSortOpt {
334
+ return nil , "" , xerrors .Errorf ("no field marked as default_sort or nosort in type %q" , t .String ())
297
335
}
298
336
299
337
return headers , defaultSortName , nil
@@ -320,7 +358,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
320
358
for i := 0 ; i < val .NumField (); i ++ {
321
359
field := val .Type ().Field (i )
322
360
fieldVal := val .Field (i )
323
- name , _ , recursive , skip , err := parseTableStructTag (field )
361
+ name , _ , _ , recursive , skip , err := parseTableStructTag (field )
324
362
if err != nil {
325
363
return nil , xerrors .Errorf ("parse struct tags for field %q in type %T: %w" , field .Name , val , err )
326
364
}
0 commit comments