feat: add cli/format package
This commit is contained in:
parent
e40a8515cf
commit
4a8add1d3d
|
@ -0,0 +1,38 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
const Format format.Format = "json"
|
||||
|
||||
func init() {
|
||||
format.Register(Format, NewWriter())
|
||||
}
|
||||
|
||||
type Writer struct{}
|
||||
|
||||
// Format implements format.Writer.
|
||||
func (*Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
||||
encoder := json.NewEncoder(writer)
|
||||
|
||||
if hints.OutputMode == format.OutputModeWide {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWriter() *Writer {
|
||||
return &Writer{}
|
||||
}
|
||||
|
||||
var _ format.Writer = &Writer{}
|
|
@ -0,0 +1,49 @@
|
|||
package format
|
||||
|
||||
type PropHintName string
|
||||
|
||||
type PropHintFunc func() (PropHintName, any)
|
||||
|
||||
type Prop struct {
|
||||
name string
|
||||
label string
|
||||
hints map[PropHintName]any
|
||||
}
|
||||
|
||||
func (p *Prop) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Prop) Label() string {
|
||||
return p.label
|
||||
}
|
||||
|
||||
func NewProp(name, label string, funcs ...PropHintFunc) Prop {
|
||||
hints := make(map[PropHintName]any)
|
||||
for _, fn := range funcs {
|
||||
name, value := fn()
|
||||
hints[name] = value
|
||||
}
|
||||
|
||||
return Prop{name, label, hints}
|
||||
}
|
||||
|
||||
func WithPropHint(name PropHintName, value any) PropHintFunc {
|
||||
return func() (PropHintName, any) {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
|
||||
func PropHint[T any](p Prop, name PropHintName, defaultValue T) T {
|
||||
rawValue, exists := p.hints[name]
|
||||
if !exists {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
value, ok := rawValue.(T)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Format string
|
||||
|
||||
type Registry map[Format]Writer
|
||||
|
||||
var defaultRegistry = Registry{}
|
||||
|
||||
var ErrUnknownFormat = errors.New("unknown format")
|
||||
|
||||
func Write(format Format, writer io.Writer, hints Hints, data ...any) error {
|
||||
formatWriter, exists := defaultRegistry[format]
|
||||
if !exists {
|
||||
return errors.WithStack(ErrUnknownFormat)
|
||||
}
|
||||
|
||||
if hints.OutputMode == "" {
|
||||
hints.OutputMode = OutputModeCompact
|
||||
}
|
||||
|
||||
if err := formatWriter.Write(writer, hints, data...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Available() []Format {
|
||||
formats := make([]Format, 0, len(defaultRegistry))
|
||||
|
||||
for f := range defaultRegistry {
|
||||
formats = append(formats, f)
|
||||
}
|
||||
|
||||
return formats
|
||||
}
|
||||
|
||||
func Register(format Format, writer Writer) {
|
||||
defaultRegistry[format] = writer
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package table
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
const (
|
||||
hintCompactModeMaxColumnWidth format.PropHintName = "compactModeMaxColumnWidth"
|
||||
)
|
||||
|
||||
func WithCompactModeMaxColumnWidth(max int) format.PropHintFunc {
|
||||
return format.WithPropHint(hintCompactModeMaxColumnWidth, max)
|
||||
}
|
||||
|
||||
func getProps(d any) []format.Prop {
|
||||
props := make([]format.Prop, 0)
|
||||
|
||||
v := reflect.Indirect(reflect.ValueOf(d))
|
||||
typeOf := v.Type()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
name := typeOf.Field(i).Name
|
||||
props = append(props, format.NewProp(name, name))
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
func getFieldValue(obj any, name string) string {
|
||||
v := reflect.Indirect(reflect.ValueOf(obj))
|
||||
|
||||
fieldValue := v.FieldByName(name)
|
||||
|
||||
if !fieldValue.IsValid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Map:
|
||||
fallthrough
|
||||
case reflect.Struct:
|
||||
fallthrough
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Interface:
|
||||
json, err := json.Marshal(fieldValue.Interface())
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return string(json)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%v", fieldValue)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package table
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
const Format format.Format = "table"
|
||||
|
||||
const DefaultCompactModeMaxColumnWidth = 30
|
||||
|
||||
func init() {
|
||||
format.Register(Format, NewWriter(DefaultCompactModeMaxColumnWidth))
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
compactModeMaxColumnWidth int
|
||||
}
|
||||
|
||||
// Write implements format.Writer.
|
||||
func (w *Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
||||
t := table.NewWriter()
|
||||
|
||||
t.SetOutputMirror(writer)
|
||||
|
||||
var props []format.Prop
|
||||
|
||||
if hints.Props != nil {
|
||||
props = hints.Props
|
||||
} else {
|
||||
if len(data) > 0 {
|
||||
props = getProps(data[0])
|
||||
} else {
|
||||
props = make([]format.Prop, 0)
|
||||
}
|
||||
}
|
||||
|
||||
labels := table.Row{}
|
||||
|
||||
for _, p := range props {
|
||||
labels = append(labels, p.Label())
|
||||
}
|
||||
|
||||
t.AppendHeader(labels)
|
||||
|
||||
isCompactMode := hints.OutputMode == format.OutputModeCompact
|
||||
|
||||
for _, d := range data {
|
||||
row := table.Row{}
|
||||
|
||||
for _, p := range props {
|
||||
value := getFieldValue(d, p.Name())
|
||||
|
||||
compactModeMaxColumnWidth := format.PropHint(p,
|
||||
hintCompactModeMaxColumnWidth,
|
||||
w.compactModeMaxColumnWidth,
|
||||
)
|
||||
|
||||
if isCompactMode && len(value) > compactModeMaxColumnWidth {
|
||||
value = value[:compactModeMaxColumnWidth] + "..."
|
||||
}
|
||||
|
||||
row = append(row, value)
|
||||
}
|
||||
|
||||
t.AppendRow(row)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWriter(compactModeMaxColumnWidth int) *Writer {
|
||||
return &Writer{compactModeMaxColumnWidth}
|
||||
}
|
||||
|
||||
var _ format.Writer = &Writer{}
|
|
@ -0,0 +1,86 @@
|
|||
package table
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
type dummyItem struct {
|
||||
MyString string
|
||||
MyInt int
|
||||
MySub subItem
|
||||
}
|
||||
|
||||
type subItem struct {
|
||||
MyBool bool
|
||||
}
|
||||
|
||||
var dummyItems = []any{
|
||||
dummyItem{
|
||||
MyString: "Foo",
|
||||
MyInt: 1,
|
||||
MySub: subItem{
|
||||
MyBool: false,
|
||||
},
|
||||
},
|
||||
dummyItem{
|
||||
MyString: "Bar",
|
||||
MyInt: 0,
|
||||
MySub: subItem{
|
||||
MyBool: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestWriterNoHints(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
||||
|
||||
if err := writer.Write(&buf, format.Hints{}, dummyItems...); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
expected := `+----------+-------+------------------+
|
||||
| MYSTRING | MYINT | MYSUB |
|
||||
+----------+-------+------------------+
|
||||
| Foo | 1 | {"MyBool":false} |
|
||||
| Bar | 0 | {"MyBool":true} |
|
||||
+----------+-------+------------------+`
|
||||
|
||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterWithPropHints(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
||||
|
||||
hints := format.Hints{
|
||||
Props: []format.Prop{
|
||||
format.NewProp("MyString", "MyString"),
|
||||
format.NewProp("MyInt", "MyInt"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := writer.Write(&buf, hints, dummyItems...); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
expected := `+----------+-------+
|
||||
| MYSTRING | MYINT |
|
||||
+----------+-------+
|
||||
| Foo | 1 |
|
||||
| Bar | 0 |
|
||||
+----------+-------+`
|
||||
|
||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package format
|
||||
|
||||
import "io"
|
||||
|
||||
type OutputMode string
|
||||
|
||||
const (
|
||||
OutputModeWide OutputMode = "wide"
|
||||
OutputModeCompact OutputMode = "compact"
|
||||
)
|
||||
|
||||
type Hints struct {
|
||||
Props []Prop
|
||||
OutputMode OutputMode
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(writer io.Writer, hints Hints, data ...any) error
|
||||
}
|
1
go.mod
1
go.mod
|
@ -28,6 +28,7 @@ require (
|
|||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -102,6 +102,8 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
|
|||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
|
@ -125,6 +127,7 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
|
@ -139,6 +142,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
|
@ -157,6 +161,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
|
@ -231,6 +236,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
|
|
Loading…
Reference in New Issue