coder/cli/cliui/output.go

174 lines
4.4 KiB
Go

package cliui
import (
"context"
"encoding/json"
"reflect"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/clibase"
)
type OutputFormat interface {
ID() string
AttachOptions(opts *clibase.OptionSet)
Format(ctx context.Context, data any) (string, error)
}
type OutputFormatter struct {
formats []OutputFormat
formatID string
}
// NewOutputFormatter creates a new OutputFormatter with the given formats. The
// first format is the default format. At least two formats must be provided.
func NewOutputFormatter(formats ...OutputFormat) *OutputFormatter {
if len(formats) < 2 {
panic("at least two output formats must be provided")
}
formatIDs := make(map[string]struct{}, len(formats))
for _, format := range formats {
if format.ID() == "" {
panic("output format ID must not be empty")
}
if _, ok := formatIDs[format.ID()]; ok {
panic("duplicate format ID: " + format.ID())
}
formatIDs[format.ID()] = struct{}{}
}
return &OutputFormatter{
formats: formats,
formatID: formats[0].ID(),
}
}
// AttachOptions attaches the --output flag to the given command, and any
// additional flags required by the output formatters.
func (f *OutputFormatter) AttachOptions(opts *clibase.OptionSet) {
for _, format := range f.formats {
format.AttachOptions(opts)
}
formatNames := make([]string, 0, len(f.formats))
for _, format := range f.formats {
formatNames = append(formatNames, format.ID())
}
*opts = append(*opts,
clibase.Option{
Flag: "output",
FlagShorthand: "o",
Default: f.formats[0].ID(),
Value: clibase.StringOf(&f.formatID),
Description: "Output format. Available formats: " + strings.Join(formatNames, ", ") + ".",
},
)
}
// Format formats the given data using the format specified by the --output
// flag. If the flag is not set, the default format is used.
func (f *OutputFormatter) Format(ctx context.Context, data any) (string, error) {
for _, format := range f.formats {
if format.ID() == f.formatID {
return format.Format(ctx, data)
}
}
return "", xerrors.Errorf("unknown output format %q", f.formatID)
}
type tableFormat struct {
defaultColumns []string
allColumns []string
sort string
columns []string
}
var _ OutputFormat = &tableFormat{}
// TableFormat creates a table formatter for the given output type. The output
// type should be specified as an empty slice of the desired type.
//
// E.g.: TableFormat([]MyType{}, []string{"foo", "bar"})
//
// defaultColumns is optional and specifies the default columns to display. If
// not specified, all columns are displayed by default.
func TableFormat(out any, defaultColumns []string) OutputFormat {
v := reflect.Indirect(reflect.ValueOf(out))
if v.Kind() != reflect.Slice {
panic("DisplayTable called with a non-slice type")
}
// Get the list of table column headers.
headers, defaultSort, err := typeToTableHeaders(v.Type().Elem())
if err != nil {
panic("parse table headers: " + err.Error())
}
tf := &tableFormat{
defaultColumns: headers,
allColumns: headers,
sort: defaultSort,
}
if len(defaultColumns) > 0 {
tf.defaultColumns = defaultColumns
}
return tf
}
// ID implements OutputFormat.
func (*tableFormat) ID() string {
return "table"
}
// AttachOptions implements OutputFormat.
func (f *tableFormat) AttachOptions(opts *clibase.OptionSet) {
*opts = append(*opts,
clibase.Option{
Flag: "column",
FlagShorthand: "c",
Default: strings.Join(f.defaultColumns, ","),
Value: clibase.StringArrayOf(&f.columns),
Description: "Columns to display in table output. Available columns: " + strings.Join(f.allColumns, ", ") + ".",
},
)
}
// Format implements OutputFormat.
func (f *tableFormat) Format(_ context.Context, data any) (string, error) {
return DisplayTable(data, f.sort, f.columns)
}
type jsonFormat struct{}
var _ OutputFormat = jsonFormat{}
// JSONFormat creates a JSON formatter.
func JSONFormat() OutputFormat {
return jsonFormat{}
}
// ID implements OutputFormat.
func (jsonFormat) ID() string {
return "json"
}
// AttachOptions implements OutputFormat.
func (jsonFormat) AttachOptions(_ *clibase.OptionSet) {}
// Format implements OutputFormat.
func (jsonFormat) Format(_ context.Context, data any) (string, error) {
outBytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", xerrors.Errorf("marshal output to JSON: %w", err)
}
return string(outBytes), nil
}