Ctrl K
ComunidadGuías…

cli/cliui/table.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cliui

import (
	"fmt"
	"reflect"
	"strings"
	"time"

	"github.com/fatih/structtag"
	"github.com/jedib0t/go-pretty/v6/table"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/codersdk"
)

// Table creates a new table with standardized styles.
func Table() table.Writer {
	tableWriter := table.NewWriter()
	tableWriter.Style().Box.PaddingLeft = ""
	tableWriter.Style().Box.PaddingRight = "  "
	tableWriter.Style().Options.DrawBorder = false
	tableWriter.Style().Options.SeparateHeader = false
	tableWriter.Style().Options.SeparateColumns = false
	return tableWriter
}

// This type can be supplied as part of a slice to DisplayTable
// or to a `TableFormat` `Format` call to render a separator.
// Leading separators are not supported and trailing separators
// are ignored by the table formatter.
// e.g. `[]any{someRow, TableSeparator, someRow}`
type TableSeparator struct{}

// filterHeaders filters the headers to only include the columns
// that are provided in the array. If the array is empty, all
// headers are included.
func filterHeaders(header table.Row, columns []string) table.Row {
	if len(columns) == 0 {
		return header
	}

	filteredHeaders := make(table.Row, len(columns))
	for i, column := range columns {
		column = strings.ReplaceAll(column, "_", " ")

		for _, headerTextRaw := range header {
			headerText, _ := headerTextRaw.(string)
			if strings.EqualFold(column, headerText) {
				filteredHeaders[i] = headerText
				break
			}
		}
	}
	return filteredHeaders
}

// createColumnConfigs returns configuration to hide columns
// that are not provided in the array. If the array is empty,
// no filtering will occur!
func createColumnConfigs(header table.Row, columns []string) []table.ColumnConfig {
	if len(columns) == 0 {
		return nil
	}
	columnConfigs := make([]table.ColumnConfig, 0)
	for _, headerTextRaw := range header {
		headerText, _ := headerTextRaw.(string)
		hidden := true
		for _, column := range columns {
			if strings.EqualFold(strings.ReplaceAll(column, "_", " "), headerText) {
				hidden = false
				break
			}
		}
		columnConfigs = append(columnConfigs, table.ColumnConfig{
			Name:   headerText,
			Hidden: hidden,
		})
	}
	return columnConfigs
}

// DisplayTable renders a table as a string. The input argument can be:
//   - a struct slice.
//   - an interface slice, where the first element is a struct,
//     and all other elements are of the same type, or a TableSeparator.
//
// At least one field in the struct must have a `table:""` tag
// containing the name of the column in the outputted table.
//
// If `sort` is not specified, the field with the `table:"$NAME,default_sort"`
// tag will be used to sort. An error will be returned if no field has this tag.
//
// Nested structs are processed if the field has the `table:"$NAME,recursive"`
// tag and their fields will be named as `$PARENT_NAME $NAME`. If the tag is
// malformed or a field is marked as recursive but does not contain a struct or
// a pointer to a struct, this function will return an error (even with an empty
// input slice).
//
// If sort is empty, the input order will be used. If filterColumns is empty or
// nil, all available columns are included.
func DisplayTable(out any, sort string, filterColumns []string) (string, error) {
	v := reflect.Indirect(reflect.ValueOf(out))

	if v.Kind() != reflect.Slice {
		return "", xerrors.New("DisplayTable called with a non-slice type")
	}
	var tableType reflect.Type
	if v.Type().Elem().Kind() == reflect.Interface {
		if v.Len() == 0 {
			return "", xerrors.New("DisplayTable called with empty interface slice")
		}
		tableType = reflect.Indirect(reflect.ValueOf(v.Index(0).Interface())).Type()
	} else {
		tableType = v.Type().Elem()
	}

	// Get the list of table column headers.
	headersRaw, defaultSort, err := typeToTableHeaders(tableType, true)
	if err != nil {
		return "", xerrors.Errorf("get table headers recursively for type %q: %w", v.Type().Elem().String(), err)
	}
	if len(headersRaw) == 0 {
		return "", xerrors.New(`no table headers found on the input type, make sure there is at least one "table" struct tag`)
	}
	if sort == "" {
		sort = defaultSort
	}
	headers := make(table.Row, len(headersRaw))
	for i, header := range headersRaw {
		headers[i] = strings.ReplaceAll(header, "_", " ")
	}
	// Verify that the given sort column and filter columns are valid.
	if sort != "" || len(filterColumns) != 0 {
		headersMap := make(map[string]string, len(headersRaw))
		for _, header := range headersRaw {
			headersMap[strings.ToLower(header)] = header
		}

		if sort != "" {
			sort = strings.ToLower(strings.ReplaceAll(sort, "_", " "))
			h, ok := headersMap[sort]
			if !ok {
				return "", xerrors.Errorf(`specified sort column %q not found in table headers, available columns are "%v"`, sort, strings.Join(headersRaw, `", "`))
			}

			// Autocorrect
			sort = h
		}

		for i, column := range filterColumns {
			column := strings.ToLower(strings.ReplaceAll(column, "_", " "))
			h, ok := headersMap[column]
			if !ok {
				return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, column, strings.Join(headersRaw, `", "`))
			}

			// Autocorrect
			filterColumns[i] = h
		}
	}

	// Verify that the given sort column is valid.
	if sort != "" {
		sort = strings.ReplaceAll(sort, "_", " ")
		found := false
		for _, header := range headersRaw {
			if strings.EqualFold(sort, header) {
				found = true
				sort = header
				break
			}
		}
		if !found {
			return "", xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q", sort, strings.Join(headersRaw, `", "`))
		}
	}
	return renderTable(out, sort, headers, filterColumns)
}

func renderTable(out any, sort string, headers table.Row, filterColumns []string) (string, error) {
	v := reflect.Indirect(reflect.ValueOf(out))

	// Return empty string for empty data. Callers should check for this
	// and provide an appropriate message to the user.
	if v.Kind() == reflect.Slice && v.Len() == 0 {
		return "", nil
	}

	headers = filterHeaders(headers, filterColumns)
	columnConfigs := createColumnConfigs(headers, filterColumns)

	// Setup the table formatter.
	tw := Table()
	tw.AppendHeader(headers)
	tw.SetColumnConfigs(columnConfigs)
	if sort != "" {
		tw.SortBy([]table.SortBy{{
			Name: sort,
		}})
	}

	// Write each struct to the table.
	for i := 0; i < v.Len(); i++ {
		cur := v.Index(i).Interface()
		_, ok := cur.(TableSeparator)
		if ok {
			tw.AppendSeparator()
			continue
		}
		// Format the row as a slice.
		// ValueToTableMap does what `reflect.Indirect` does
		rowMap, err := valueToTableMap(reflect.ValueOf(cur))
		if err != nil {
			return "", xerrors.Errorf("get table row map %v: %w", i, err)
		}

		rowSlice := make([]any, len(headers))
		for i, h := range headers {
			v, ok := rowMap[h.(string)]
			if !ok {
				v = nil
			}

			// Special type formatting.
			switch val := v.(type) {
			case time.Time:
				v = val.Format(time.RFC3339)
			case *time.Time:
				if val != nil {
					v = val.Format(time.RFC3339)
				}
			case codersdk.NullTime:
				if val.Valid {
					v = val.Time.Format(time.RFC3339)
				} else {
					v = nil
				}
			case *string:
				if val != nil {
					v = *val
				}
			case *int64:
				if val != nil {
					v = *val
				}
			case *time.Duration:
				if val != nil {
					v = val.String()
				}
			case fmt.Stringer:
				// Protect against typed nils since fmt.Stringer is an interface.
				vv := reflect.ValueOf(v)
				nilPtr := vv.Kind() == reflect.Ptr && vv.IsNil()
				if val != nil && !nilPtr {
					v = val.String()
				} else if nilPtr {
					v = nil
				}
			}

			// Guard against nil dereferences
			if v != nil {
				rt := reflect.TypeOf(v)
				switch rt.Kind() {
				case reflect.Slice:
					// By default, the behavior is '%v', which just returns a string like
					// '[a b c]'. This will add commas in between each value.
					strs := make([]string, 0)
					vt := reflect.ValueOf(v)
					for i := 0; i < vt.Len(); i++ {
						strs = append(strs, fmt.Sprintf("%v", vt.Index(i).Interface()))
					}
					v = "[" + strings.Join(strs, ", ") + "]"
				default:
					// Leave it as it is
				}
			}

			// Last resort, just get the interface value to avoid printing
			// pointer values. For example, if we have a `*MyType("value")`
			// which is defined as `type MyType string`, we want to print
			// the string value, not the pointer.
			if v != nil {
				vv := reflect.ValueOf(v)
				for vv.Kind() == reflect.Ptr && !vv.IsNil() {
					vv = vv.Elem()
				}
				v = vv.Interface()
			}

			rowSlice[i] = v
		}

		tw.AppendRow(table.Row(rowSlice))
	}

	return tw.Render(), nil
}

// parseTableStructTag returns the name of the field according to the `table`
// struct tag. If the table tag does not exist or is "-", an empty string is
// returned. If the table tag is malformed, an error is returned.
//
// The returned name is transformed from "snake_case" to "normal text".
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, noSortOpt, recursive, skipParentName, emptyNil bool, err error) {
	tags, err := structtag.Parse(string(field.Tag))
	if err != nil {
		return "", false, false, false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
	}

	tag, err := tags.Get("table")
	if err != nil || tag.Name == "-" {
		// tags.Get only returns an error if the tag is not found.
		return "", false, false, false, false, false, nil
	}

	defaultSortOpt := false
	noSortOpt = false
	recursiveOpt := false
	skipParentNameOpt := false
	emptyNilOpt := false
	for _, opt := range tag.Options {
		switch opt {
		case "default_sort":
			defaultSortOpt = true
		case "nosort":
			noSortOpt = true
		case "recursive":
			recursiveOpt = true
		case "recursive_inline":
			// recursive_inline is a helper to make recursive tables look nicer.
			// It skips prefixing the parent name to the child name. If you do this,
			// make sure the child name is unique across all nested structs in the parent.
			recursiveOpt = true
			skipParentNameOpt = true
		case "empty_nil":
			emptyNilOpt = true
		default:
			return "", false, false, false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
		}
	}

	return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, noSortOpt, recursiveOpt, skipParentNameOpt, emptyNilOpt, nil
}

func isStructOrStructPointer(t reflect.Type) bool {
	return t.Kind() == reflect.Struct || (t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct)
}

// typeToTableHeaders converts a type to a slice of column names. If the given
// type is invalid (not a struct or a pointer to a struct, has invalid table
// tags, etc.), an error is returned.
//
// requireDefault is only needed for the root call. This is recursive, so nested
// structs do not need the default sort name.
// nolint:revive
func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string, error) {
	if !isStructOrStructPointer(t) {
		return nil, "", xerrors.Errorf("typeToTableHeaders called with a non-struct or a non-pointer-to-a-struct type")
	}
	if t.Kind() == reflect.Pointer {
		t = t.Elem()
	}

	headers := []string{}
	defaultSortName := ""
	noSortOpt := false
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		name, defaultSort, noSort, recursive, skip, _, err := parseTableStructTag(field)
		if err != nil {
			return nil, "", xerrors.Errorf("parse struct tags for field %q in type %q: %w", field.Name, t.String(), err)
		}
		if requireDefault && noSort {
			noSortOpt = true
		}

		if name == "" && (recursive && skip) {
			return nil, "", xerrors.Errorf("a name is required for the field %q. "+
				"recursive_line will ensure this is never shown to the user, but is still needed", field.Name)
		}
		// If recurse and skip is set, the name is intentionally empty.
		if name == "" {
			continue
		}
		if defaultSort {
			if defaultSortName != "" {
				return nil, "", xerrors.Errorf("multiple fields marked as default sort in type %q", t.String())
			}
			defaultSortName = name
		}

		fieldType := field.Type
		if recursive {
			if !isStructOrStructPointer(fieldType) {
				return nil, "", 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())
			}

			childNames, defaultSort, err := typeToTableHeaders(fieldType, false)
			if err != nil {
				return nil, "", xerrors.Errorf("get child field header names for field %q in type %q: %w", field.Name, fieldType.String(), err)
			}
			for _, childName := range childNames {
				fullName := fmt.Sprintf("%s %s", name, childName)
				if skip {
					fullName = childName
				}
				headers = append(headers, fullName)
			}
			if defaultSortName == "" {
				defaultSortName = defaultSort
			}
			continue
		}

		headers = append(headers, name)
	}

	if defaultSortName == "" && requireDefault && !noSortOpt {
		return nil, "", xerrors.Errorf("no field marked as default_sort or nosort in type %q", t.String())
	}

	return headers, defaultSortName, nil
}

// valueToTableMap converts a struct to a map of column name to value. If the
// given type is invalid (not a struct or a pointer to a struct, has invalid
// table tags, etc.), an error is returned.
func valueToTableMap(val reflect.Value) (map[string]any, error) {
	if !isStructOrStructPointer(val.Type()) {
		return nil, xerrors.Errorf("valueToTableMap called with a non-struct or a non-pointer-to-a-struct type")
	}
	if val.Kind() == reflect.Pointer {
		if val.IsNil() {
			// No data for this struct, so return an empty map. All values will
			// be rendered as nil in the resulting table.
			return map[string]any{}, nil
		}

		val = val.Elem()
	}

	row := map[string]any{}
	for i := 0; i < val.NumField(); i++ {
		field := val.Type().Field(i)
		fieldVal := val.Field(i)
		name, _, _, recursive, skip, emptyNil, err := parseTableStructTag(field)
		if err != nil {
			return nil, xerrors.Errorf("parse struct tags for field %q in type %T: %w", field.Name, val, err)
		}
		if name == "" {
			continue
		}

		fieldType := field.Type

		// If empty_nil is set and this is a nil pointer, use a zero value.
		if emptyNil && fieldVal.Kind() == reflect.Pointer && fieldVal.IsNil() {
			fieldVal = reflect.New(fieldType.Elem())
		}

		// Recurse if it's a struct.
		if recursive {
			if !isStructOrStructPointer(fieldType) {
				return nil, 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())
			}

			// valueToTableMap does nothing on pointers so we don't need to
			// filter here.
			childMap, err := valueToTableMap(fieldVal)
			if err != nil {
				return nil, xerrors.Errorf("get child field values for field %q in type %q: %w", field.Name, fieldType.String(), err)
			}
			for childName, childValue := range childMap {
				fullName := fmt.Sprintf("%s %s", name, childName)
				if skip {
					fullName = childName
				}
				row[fullName] = childValue
			}
			continue
		}

		// Otherwise, we just use the field value.
		row[name] = fieldVal.Interface()
	}

	return row, nil
}