github.com/shipa-corp/ketch@v0.6.0/cmd/ketch/output/column.go (about)

     1  package output
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"reflect"
     8  	"sort"
     9  	"strings"
    10  	"text/tabwriter"
    11  )
    12  
    13  // columnOutput represents data and a writer for standard output type
    14  type columnOutput struct {
    15  	data   interface{}
    16  	writer io.Writer
    17  }
    18  
    19  // val is a structure for storing a Value and it's column struct tag together
    20  type val struct {
    21  	tag   reflect.StructTag
    22  	value reflect.Value
    23  }
    24  
    25  type valSet []val
    26  
    27  // write implements Writer for type Column
    28  func (c *columnOutput) write() error {
    29  	d, err := c.marshal(c.data)
    30  	if err != nil {
    31  		return err
    32  	}
    33  	_, err = fmt.Fprintln(c.writer, string(d))
    34  	return err
    35  }
    36  
    37  // marshal creates valSets from v, depending on whether it is a slice, struct, map, or pointer.
    38  // Column headings are pulled from the "column" struct tag or spaced-and-capitalized from the field name
    39  // if the tag does not exist. Data is then printed to the tabwriter. Fields with the dash struct tag, e.g. `column"-"`
    40  // are omitted. The 'omitempty' tag directive has not been implemented.
    41  func (c *columnOutput) marshal(v interface{}) ([]byte, error) {
    42  	// create valSets from the ValueOf v, depending on v's underlying kind
    43  	var valSets []valSet
    44  	value := reflect.ValueOf(v)
    45  	switch value.Kind() {
    46  	case reflect.Struct:
    47  		valSet := newValSet(value)
    48  		valSets = append(valSets, valSet)
    49  
    50  	case reflect.Slice:
    51  		for i := 0; i < reflect.Value.Len(value); i++ {
    52  			valSet := newValSet(value.Index(i))
    53  			valSets = append(valSets, valSet)
    54  		}
    55  
    56  	case reflect.Ptr:
    57  		valSet := newValSet(value.Elem())
    58  		valSets = append(valSets, valSet)
    59  
    60  	case reflect.Map:
    61  		var vs valSet
    62  		keys := value.MapKeys()
    63  		sort.Slice(keys, func(i, j int) bool {
    64  			return keys[i].String() < keys[j].String()
    65  		})
    66  		for _, key := range keys {
    67  			v := value.MapIndex(key)
    68  			vs = append(vs, val{tag: reflect.StructTag(fmt.Sprintf("column:\"%s\"", key.String())), value: v})
    69  		}
    70  		valSets = append(valSets, vs)
    71  
    72  	default:
    73  		return nil, fmt.Errorf("unsupported kind: %s", value.Kind())
    74  	}
    75  
    76  	// no data
    77  	if len(valSets) < 1 {
    78  		return nil, nil
    79  	}
    80  
    81  	var buf bytes.Buffer
    82  	w := tabwriter.NewWriter(&buf, 0, 4, 4, ' ', 0)
    83  
    84  	// write header columns using tags from first item in valSets
    85  	for i, val := range valSets[0] {
    86  		tag := val.tag.Get("column")
    87  		// omit?
    88  		if tag == "-" {
    89  			continue
    90  		}
    91  
    92  		fmt.Fprint(w, tag)
    93  		// tab
    94  		if i+1 < len(valSets[0]) {
    95  			fmt.Fprint(w, "\t")
    96  		}
    97  	}
    98  	fmt.Fprint(w, "\n")
    99  
   100  	// write field columns from valSets values
   101  	for i, valSet := range valSets {
   102  		for j, val := range valSet {
   103  			// omit if column tag is '-'
   104  			if val.tag.Get("column") == "-" {
   105  				continue
   106  			}
   107  
   108  			fmt.Fprint(w, val.value)
   109  
   110  			// tab
   111  			if j+1 < len(valSet) {
   112  				fmt.Fprint(w, "\t")
   113  			}
   114  		}
   115  		// newline
   116  		if i+1 < len(valSets) {
   117  			fmt.Fprint(w, "\n")
   118  		}
   119  	}
   120  	err := w.Flush()
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	return buf.Bytes(), nil
   126  }
   127  
   128  // newValSet iterates over a value's fields and assigns the Value and StructTag to a valSet
   129  func newValSet(value reflect.Value) valSet {
   130  	var valSet valSet
   131  	for i := 0; i < value.NumField(); i++ {
   132  		tag := value.Type().Field(i).Tag
   133  		// use Field Type as StructTag if column tag is not set
   134  		if tag.Get("column") == "" {
   135  			fieldName := value.Type().Field(i).Name
   136  			// split and uppercase field name
   137  			var builder strings.Builder
   138  			for i, r := range fieldName {
   139  				if r > 96 && r < 123 { // lowercase -> uppercase
   140  					builder.WriteString(string(r - 32))
   141  				} else {
   142  					if i > 0 { // prepend space if not first value
   143  						builder.WriteString(" ")
   144  					}
   145  					builder.WriteString(string(r)) // write currently uppercase runes
   146  				}
   147  			}
   148  			tag = reflect.StructTag(fmt.Sprintf("column:\"%s\"", builder.String()))
   149  		}
   150  		valSet = append(valSet, val{tag: tag, value: value.Field(i)})
   151  	}
   152  	return valSet
   153  }