Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita872330

Browse files
authored
feat: add generic table formatter (coder#3415)
1 parentb1b2d1b commita872330

File tree

9 files changed

+619
-36
lines changed

9 files changed

+619
-36
lines changed

‎cli/cliui/table.go

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package cliui
22

33
import (
4+
"fmt"
5+
"reflect"
46
"strings"
7+
"time"
58

9+
"github.com/fatih/structtag"
610
"github.com/jedib0t/go-pretty/v6/table"
11+
"golang.org/x/xerrors"
712
)
813

914
// Table creates a new table with standardized styles.
@@ -41,3 +46,258 @@ func FilterTableColumns(header table.Row, columns []string) []table.ColumnConfig
4146
}
4247
returncolumnConfigs
4348
}
49+
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
52+
// containing the name of the column in the outputted table.
53+
//
54+
// Nested structs are processed if the field has the `table:"$NAME,recursive"`
55+
// tag and their fields will be named as `$PARENT_NAME $NAME`. If the tag is
56+
// malformed or a field is marked as recursive but does not contain a struct or
57+
// a pointer to a struct, this function will return an error (even with an empty
58+
// input slice).
59+
//
60+
// If sort is empty, the input order will be used. If filterColumns is empty or
61+
// nil, all available columns are included.
62+
funcDisplayTable(outany,sortstring,filterColumns []string) (string,error) {
63+
v:=reflect.Indirect(reflect.ValueOf(out))
64+
65+
ifv.Kind()!=reflect.Slice {
66+
return"",xerrors.Errorf("DisplayTable called with a non-slice type")
67+
}
68+
69+
// Get the list of table column headers.
70+
headersRaw,err:=typeToTableHeaders(v.Type().Elem())
71+
iferr!=nil {
72+
return"",xerrors.Errorf("get table headers recursively for type %q: %w",v.Type().Elem().String(),err)
73+
}
74+
iflen(headersRaw)==0 {
75+
return"",xerrors.New(`no table headers found on the input type, make sure there is at least one "table" struct tag`)
76+
}
77+
headers:=make(table.Row,len(headersRaw))
78+
fori,header:=rangeheadersRaw {
79+
headers[i]=header
80+
}
81+
82+
// Verify that the given sort column and filter columns are valid.
83+
ifsort!=""||len(filterColumns)!=0 {
84+
headersMap:=make(map[string]string,len(headersRaw))
85+
for_,header:=rangeheadersRaw {
86+
headersMap[strings.ToLower(header)]=header
87+
}
88+
89+
ifsort!="" {
90+
sort=strings.ToLower(strings.ReplaceAll(sort,"_"," "))
91+
h,ok:=headersMap[sort]
92+
if!ok {
93+
return"",xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q",sort,strings.Join(headersRaw,`", "`))
94+
}
95+
96+
// Autocorrect
97+
sort=h
98+
}
99+
100+
fori,column:=rangefilterColumns {
101+
column:=strings.ToLower(strings.ReplaceAll(column,"_"," "))
102+
h,ok:=headersMap[column]
103+
if!ok {
104+
return"",xerrors.Errorf("specified filter column %q not found in table headers, available columns are %q",sort,strings.Join(headersRaw,`", "`))
105+
}
106+
107+
// Autocorrect
108+
filterColumns[i]=h
109+
}
110+
}
111+
112+
// Verify that the given sort column is valid.
113+
ifsort!="" {
114+
sort=strings.ReplaceAll(sort,"_"," ")
115+
found:=false
116+
for_,header:=rangeheadersRaw {
117+
ifstrings.EqualFold(sort,header) {
118+
found=true
119+
sort=header
120+
break
121+
}
122+
}
123+
if!found {
124+
return"",xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q",sort,strings.Join(headersRaw,`", "`))
125+
}
126+
}
127+
128+
// Setup the table formatter.
129+
tw:=Table()
130+
tw.AppendHeader(headers)
131+
tw.SetColumnConfigs(FilterTableColumns(headers,filterColumns))
132+
ifsort!="" {
133+
tw.SortBy([]table.SortBy{{
134+
Name:sort,
135+
}})
136+
}
137+
138+
// Write each struct to the table.
139+
fori:=0;i<v.Len();i++ {
140+
// Format the row as a slice.
141+
rowMap,err:=valueToTableMap(v.Index(i))
142+
iferr!=nil {
143+
return"",xerrors.Errorf("get table row map %v: %w",i,err)
144+
}
145+
146+
rowSlice:=make([]any,len(headers))
147+
fori,h:=rangeheadersRaw {
148+
v,ok:=rowMap[h]
149+
if!ok {
150+
v=nil
151+
}
152+
153+
// Special type formatting.
154+
switchval:=v.(type) {
155+
case time.Time:
156+
v=val.Format(time.Stamp)
157+
case*time.Time:
158+
ifval!=nil {
159+
v=val.Format(time.Stamp)
160+
}
161+
}
162+
163+
rowSlice[i]=v
164+
}
165+
166+
tw.AppendRow(table.Row(rowSlice))
167+
}
168+
169+
returntw.Render(),nil
170+
}
171+
172+
// parseTableStructTag returns the name of the field according to the `table`
173+
// struct tag. If the table tag does not exist or is "-", an empty string is
174+
// returned. If the table tag is malformed, an error is returned.
175+
//
176+
// The returned name is transformed from "snake_case" to "normal text".
177+
funcparseTableStructTag(field reflect.StructField) (namestring,recursebool,errerror) {
178+
tags,err:=structtag.Parse(string(field.Tag))
179+
iferr!=nil {
180+
return"",false,xerrors.Errorf("parse struct field tag %q: %w",string(field.Tag),err)
181+
}
182+
183+
tag,err:=tags.Get("table")
184+
iferr!=nil||tag.Name=="-" {
185+
// tags.Get only returns an error if the tag is not found.
186+
return"",false,nil
187+
}
188+
189+
recursive:=false
190+
for_,opt:=rangetag.Options {
191+
ifopt=="recursive" {
192+
recursive=true
193+
continue
194+
}
195+
196+
return"",false,xerrors.Errorf("unknown option %q in struct field tag",opt)
197+
}
198+
199+
returnstrings.ReplaceAll(tag.Name,"_"," "),recursive,nil
200+
}
201+
202+
funcisStructOrStructPointer(t reflect.Type)bool {
203+
returnt.Kind()==reflect.Struct|| (t.Kind()==reflect.Pointer&&t.Elem().Kind()==reflect.Struct)
204+
}
205+
206+
// typeToTableHeaders converts a type to a slice of column names. If the given
207+
// type is invalid (not a struct or a pointer to a struct, has invalid table
208+
// tags, etc.), an error is returned.
209+
functypeToTableHeaders(t reflect.Type) ([]string,error) {
210+
if!isStructOrStructPointer(t) {
211+
returnnil,xerrors.Errorf("typeToTableHeaders called with a non-struct or a non-pointer-to-a-struct type")
212+
}
213+
ift.Kind()==reflect.Pointer {
214+
t=t.Elem()
215+
}
216+
217+
headers:= []string{}
218+
fori:=0;i<t.NumField();i++ {
219+
field:=t.Field(i)
220+
name,recursive,err:=parseTableStructTag(field)
221+
iferr!=nil {
222+
returnnil,xerrors.Errorf("parse struct tags for field %q in type %q: %w",field.Name,t.String(),err)
223+
}
224+
ifname=="" {
225+
continue
226+
}
227+
228+
fieldType:=field.Type
229+
ifrecursive {
230+
if!isStructOrStructPointer(fieldType) {
231+
returnnil,xerrors.Errorf("field %q in type %q is marked as recursive but does not contain a struct or a pointer to a struct",field.Name,t.String())
232+
}
233+
234+
childNames,err:=typeToTableHeaders(fieldType)
235+
iferr!=nil {
236+
returnnil,xerrors.Errorf("get child field header names for field %q in type %q: %w",field.Name,fieldType.String(),err)
237+
}
238+
for_,childName:=rangechildNames {
239+
headers=append(headers,fmt.Sprintf("%s %s",name,childName))
240+
}
241+
continue
242+
}
243+
244+
headers=append(headers,name)
245+
}
246+
247+
returnheaders,nil
248+
}
249+
250+
// valueToTableMap converts a struct to a map of column name to value. If the
251+
// given type is invalid (not a struct or a pointer to a struct, has invalid
252+
// table tags, etc.), an error is returned.
253+
funcvalueToTableMap(val reflect.Value) (map[string]any,error) {
254+
if!isStructOrStructPointer(val.Type()) {
255+
returnnil,xerrors.Errorf("valueToTableMap called with a non-struct or a non-pointer-to-a-struct type")
256+
}
257+
ifval.Kind()==reflect.Pointer {
258+
ifval.IsNil() {
259+
// No data for this struct, so return an empty map. All values will
260+
// be rendered as nil in the resulting table.
261+
returnmap[string]any{},nil
262+
}
263+
264+
val=val.Elem()
265+
}
266+
267+
row:=map[string]any{}
268+
fori:=0;i<val.NumField();i++ {
269+
field:=val.Type().Field(i)
270+
fieldVal:=val.Field(i)
271+
name,recursive,err:=parseTableStructTag(field)
272+
iferr!=nil {
273+
returnnil,xerrors.Errorf("parse struct tags for field %q in type %T: %w",field.Name,val,err)
274+
}
275+
ifname=="" {
276+
continue
277+
}
278+
279+
// Recurse if it's a struct.
280+
fieldType:=field.Type
281+
ifrecursive {
282+
if!isStructOrStructPointer(fieldType) {
283+
returnnil,xerrors.Errorf("field %q in type %q is marked as recursive but does not contain a struct or a pointer to a struct",field.Name,fieldType.String())
284+
}
285+
286+
// valueToTableMap does nothing on pointers so we don't need to
287+
// filter here.
288+
childMap,err:=valueToTableMap(fieldVal)
289+
iferr!=nil {
290+
returnnil,xerrors.Errorf("get child field values for field %q in type %q: %w",field.Name,fieldType.String(),err)
291+
}
292+
forchildName,childValue:=rangechildMap {
293+
row[fmt.Sprintf("%s %s",name,childName)]=childValue
294+
}
295+
continue
296+
}
297+
298+
// Otherwise, we just use the field value.
299+
row[name]=val.Field(i).Interface()
300+
}
301+
302+
returnrow,nil
303+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp