admin-cli/tabular/template.go (161 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package tabular
import (
"fmt"
"io"
"strconv"
"github.com/dustin/go-humanize"
"gopkg.in/yaml.v2"
)
// Template receives a yaml template for printing the tabular contents.
// A template must contain one or more sections, each is displayed as
// a table. A sections could contain multiple keys, each represents as
// the table column. Template iterates all the items in every section,
// one item acts as a row in the table.
type Template struct {
// The sections are in the top-down order as they are declared in the template.
sections []*section
colValFunc ColumnValueFunc
commonColFunc CommonColumnsFunc
commonColNames []string
}
// NewTemplate parses the given template.
func NewTemplate(template string) *Template {
// use MapSlice to preserve the order.
var sections yaml.MapSlice
err := yaml.Unmarshal([]byte(template), §ions)
if err != nil {
panic(err)
}
t := &Template{}
for _, kv := range sections {
sec := §ion{name: fmt.Sprint(kv.Key)}
columns := kv.Value.(yaml.MapSlice)
for _, col := range columns {
colAttrs := &ColumnAttributes{Name: fmt.Sprint(col.Key), Attrs: make(map[string]string)}
if col.Value != nil {
attrs := col.Value.(yaml.MapSlice)
for _, attr := range attrs {
name := fmt.Sprint(attr.Key)
value := fmt.Sprint(attr.Value)
colAttrs.Attrs[name] = value
}
}
colAttrs.formatter = getFormatter(colAttrs.Attrs)
colAttrs.aggregator = getAggregator(colAttrs.Attrs)
sec.columns = append(sec.columns, colAttrs)
}
t.sections = append(t.sections, sec)
}
return t
}
func getFormatter(attrs map[string]string) columnValueFormatter {
t, ok := attrs["unit"]
if !ok {
return defaultFormatter
}
switch t {
case "byte":
return byteStatFormatter
case "MB":
return megabyteStatFormatter
default:
panic(fmt.Sprintf("unexpected unit %vs", t))
}
}
func getAggregator(attrs map[string]string) columnValueAggregator {
t, ok := attrs["aggregate"]
if !ok {
return defaultAggregator
}
switch t {
case "average":
return averageAggregator
default:
panic(fmt.Sprintf("unexpected aggregate %vs", t))
}
}
// ColumnValueFunc takes the column value from a user record.
type ColumnValueFunc func(col *ColumnAttributes, rowData interface{}) interface{}
// SetColumnValueFunc configures ColumnValueFunc
func (t *Template) SetColumnValueFunc(f ColumnValueFunc) {
t.colValFunc = f
}
// CommonColumnsFunc returns a list of common columns.
type CommonColumnsFunc func(rowData interface{}) []string
// SetCommonColumns sets the columns that are exactly the same among sections.
// This is a conveninence util to prevent repeatedly declaring the column for each section.
func (t *Template) SetCommonColumns(columnNames []string, f CommonColumnsFunc) {
t.commonColNames = columnNames
t.commonColFunc = f
}
// Render template output
func (t *Template) Render(writer io.Writer, rows []interface{}) {
for _, sect := range t.sections {
// print section
fmt.Fprintf(writer, "[%s]\n", sect.name)
header := t.commonColNames
for _, col := range sect.columns {
header = append(header, col.Name)
}
var totalRowColumns = []string{"total"}
for n := range header {
if n == 0 {
continue
}
totalRowColumns = append(totalRowColumns, "0")
}
tabWriter := NewTabWriter(writer, header)
for _, row := range rows {
rowColumns := t.commonColFunc(row)
for n, row := range rowColumns {
if n == 0 {
continue
}
value, _ := strconv.ParseFloat(row, 64)
defaultAggregator(len(rows), totalRowColumns, n, value)
}
for n, col := range sect.columns {
columnValue := t.colValFunc(col, row)
col.aggregator(len(rows), totalRowColumns, n+len(t.commonColNames), columnValue.(float64))
rowColumns = append(rowColumns, col.formatter(columnValue))
}
tabWriter.Append(rowColumns)
}
for n, col := range sect.columns {
value, _ := strconv.ParseFloat(totalRowColumns[n+len(t.commonColNames)], 64)
totalRowColumns[n+len(t.commonColNames)] = col.formatter(value)
}
tabWriter.SetFooter(totalRowColumns)
tabWriter.Render()
}
}
// section display as a table.
type section struct {
name string
columns []*ColumnAttributes
}
// ColumnAttributes is the
type ColumnAttributes struct {
Name string
Attrs map[string]string
// The formatter is optionally declared in `unit`
// If `unit` is "byte", byteStatFormatter is used.
// If `unit` is "MB", megabyteStatFormatter is used.
// Otherwise defaultFormatter is used.
formatter columnValueFormatter
// The aggregator is optionally declared in `aggregate`
// if `aggregate` is `average`, averageAggregator is used
// Otherwise defaultAggregator is used
aggregator columnValueAggregator
}
// The default if no unit is specified.
func defaultFormatter(v interface{}) string {
if fv, ok := v.(float64); ok {
if fv < 1 {
return fmt.Sprintf("%.2f", fv)
}
return humanize.SIWithDigits(fv, 2, "")
}
return fmt.Sprintf("%v", v)
}
// Used for `"unit" : "byte"`.
func byteStatFormatter(v interface{}) string {
fv, ok := v.(float64)
if !ok {
panic("data to unit:\"byte\" must be float64")
}
return humanize.Bytes(uint64(fv))
}
// Used for `"unit" : "MB"`.
func megabyteStatFormatter(v interface{}) string {
fv, ok := v.(float64)
if !ok {
panic("data to unit:\"MB\" must be float64")
}
return humanize.Bytes(uint64(fv) << 20)
}
type columnValueFormatter func(interface{}) string
// The default column aggregate type, sum(value...)
func defaultAggregator(rows int, totalRowColumns []string, index int, deltaValue float64) {
oldValue, _ := strconv.ParseFloat(totalRowColumns[index], 64)
total := oldValue + deltaValue
totalRowColumns[index] = strconv.FormatFloat(total, 'g', 5, 64)
}
// The column aggregate type, average(value...)
func averageAggregator(rows int, totalRowColumns []string, index int, deltaValue float64) {
oldValue, _ := strconv.ParseFloat(totalRowColumns[index], 64)
average := oldValue + deltaValue/float64(rows)
totalRowColumns[index] = strconv.FormatFloat(average, 'g', 5, 64)
}
type columnValueAggregator func(int, []string, int, float64)