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 }