blob: cd8ae6c4c4758208ca3fb58ed1954cb9bf239502 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package writer
import (
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"unicode/utf8"
"v.io/v23/syncbase"
"v.io/v23/vdl"
vtime "v.io/v23/vdlroot/time"
)
type Justification int
const (
Unknown Justification = iota
Left
Right
)
// WriteTable formats the results as ASCII tables.
func WriteTable(out io.Writer, columnNames []string, rs syncbase.ResultStream) error {
// Buffer the results so we can compute the column widths.
columnWidths := make([]int, len(columnNames))
for i, cName := range columnNames {
columnWidths[i] = utf8.RuneCountInString(cName)
}
justification := make([]Justification, len(columnNames))
var results [][]string
for rs.Advance() {
row := make([]string, len(columnNames))
columnCount := rs.ResultCount()
for i := 0; i != columnCount; i++ {
var column interface{}
if err := rs.Result(i, &column); err != nil {
return err
}
if i >= len(columnNames) {
return errors.New("more columns in result than in columnNames")
}
if justification[i] == Unknown {
justification[i] = getJustification(column)
}
columnStr := toStringRaw(column, false)
row[i] = columnStr
columnLen := utf8.RuneCountInString(columnStr)
if columnLen > columnWidths[i] {
columnWidths[i] = columnLen
}
}
results = append(results, row)
}
if rs.Err() != nil {
return rs.Err()
}
writeBorder(out, columnWidths)
sep := "| "
for i, cName := range columnNames {
io.WriteString(out, fmt.Sprintf("%s%*s", sep, columnWidths[i], cName))
sep = " | "
}
io.WriteString(out, " |\n")
writeBorder(out, columnWidths)
for _, result := range results {
sep = "| "
for i, column := range result {
if justification[i] == Right {
io.WriteString(out, fmt.Sprintf("%s%*s", sep, columnWidths[i], column))
} else {
io.WriteString(out, fmt.Sprintf("%s%-*s", sep, columnWidths[i], column))
}
sep = " | "
}
io.WriteString(out, " |\n")
}
writeBorder(out, columnWidths)
return nil
}
func writeBorder(out io.Writer, columnWidths []int) {
sep := "+-"
for _, width := range columnWidths {
io.WriteString(out, fmt.Sprintf("%s%s", sep, strings.Repeat("-", width)))
sep = "-+-"
}
io.WriteString(out, "-+\n")
}
func getJustification(val interface{}) Justification {
switch val.(type) {
// TODO(kash): Floating point numbers should have the decimal point line up.
case bool, uint8, uint16, uint32, uint64, int8, int16, int32, int64,
float32, float64, complex64, complex128, int, uint, uintptr:
return Right
// TODO(kash): Leave nil values as unknown.
default:
return Left
}
}
// WriteCSV formats the results as CSV as specified by https://tools.ietf.org/html/rfc4180.
func WriteCSV(out io.Writer, columnNames []string, rs syncbase.ResultStream, delimiter string) error {
delim := ""
for _, cName := range columnNames {
str := doubleQuoteForCSV(cName, delimiter)
io.WriteString(out, fmt.Sprintf("%s%s", delim, str))
delim = delimiter
}
io.WriteString(out, "\n")
for rs.Advance() {
delim := ""
for i, n := 0, rs.ResultCount(); i != n; i++ {
var column interface{}
if err := rs.Result(i, &column); err != nil {
return err
}
str := doubleQuoteForCSV(toStringRaw(column, false), delimiter)
io.WriteString(out, fmt.Sprintf("%s%s", delim, str))
delim = delimiter
}
io.WriteString(out, "\n")
}
return rs.Err()
}
// doubleQuoteForCSV follows the escaping rules from
// https://tools.ietf.org/html/rfc4180. In particular, values containing
// newlines, double quotes, and the delimiter must be enclosed in double
// quotes.
func doubleQuoteForCSV(str, delimiter string) string {
doubleQuote := strings.Index(str, delimiter) != -1 || strings.Index(str, "\n") != -1
if strings.Index(str, "\"") != -1 {
str = strings.Replace(str, "\"", "\"\"", -1)
doubleQuote = true
}
if doubleQuote {
str = "\"" + str + "\""
}
return str
}
// WriteJson formats the result as a JSON array of arrays (rows) of values.
func WriteJson(out io.Writer, columnNames []string, rs syncbase.ResultStream) error {
io.WriteString(out, "[")
jsonColNames := make([][]byte, len(columnNames))
for i, cName := range columnNames {
jsonCName, err := json.Marshal(cName)
if err != nil {
panic(fmt.Sprintf("JSON marshalling failed for column name: %v", err))
}
jsonColNames[i] = jsonCName
}
bOpen := "{"
for rs.Advance() {
io.WriteString(out, bOpen)
linestart := "\n "
for i, n := 0, rs.ResultCount(); i != n; i++ {
var column interface{}
if err := rs.Result(i, &column); err != nil {
return err
}
str := toJson(column)
io.WriteString(out, fmt.Sprintf("%s%s: %s", linestart, jsonColNames[i], str))
linestart = ",\n "
}
io.WriteString(out, "\n}")
bOpen = ", {"
}
io.WriteString(out, "]\n")
return rs.Err()
}
func toStringRaw(rb interface{}, nested bool) string {
return toString(vdl.ValueOf(rb), nested)
}
// Converts VDL value to readable yet parseable string representation.
// If nested is not set, strings outside composites are left unquoted.
// TODO(ivanpi): Handle cycles and improve non-tree DAG handling.
func toString(val *vdl.Value, nested bool) string {
switch val.Type() {
case vdl.TypeOf(vtime.Time{}), vdl.TypeOf(vtime.Duration{}):
s, err := toStringNative(val)
if err != nil {
panic(fmt.Sprintf("toStringNative failed for builtin time type: %v", err))
}
if nested {
s = strconv.Quote(s)
}
return s
default:
// fall through to Kind switch
}
switch val.Kind() {
case vdl.Bool:
return fmt.Sprint(val.Bool())
case vdl.Byte, vdl.Uint16, vdl.Uint32, vdl.Uint64:
return fmt.Sprint(val.Uint())
case vdl.Int8, vdl.Int16, vdl.Int32, vdl.Int64:
return fmt.Sprint(val.Int())
case vdl.Float32, vdl.Float64:
return fmt.Sprint(val.Float())
case vdl.String:
s := val.RawString()
if nested {
s = strconv.Quote(s)
}
return s
case vdl.Enum:
return val.EnumLabel()
case vdl.Array, vdl.List:
return listToString("[", ", ", "]", val.Len(), func(i int) string {
return toString(val.Index(i), true)
})
case vdl.Any, vdl.Optional:
if val.IsNil() {
if nested {
return "nil"
}
// TODO(ivanpi): Blank is better for CSV, but <nil> might be better for table and TSV.
return ""
}
return toString(val.Elem(), nested)
case vdl.Struct:
return listToString("{", ", ", "}", val.Type().NumField(), func(i int) string {
field := toString(val.StructField(i), true)
return fmt.Sprintf("%s: %s", val.Type().Field(i).Name, field)
})
case vdl.Union:
ui, uv := val.UnionField()
field := toString(uv, true)
return fmt.Sprintf("%s: %s", val.Type().Field(ui).Name, field)
case vdl.Set:
// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
// Use a more sensible sort for numbers etc.
keys := vdl.SortValuesAsString(val.Keys())
return listToString("{", ", ", "}", len(keys), func(i int) string {
return toString(keys[i], true)
})
case vdl.Map:
// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
// Use a more sensible sort for numbers etc.
keys := vdl.SortValuesAsString(val.Keys())
return listToString("{", ", ", "}", len(keys), func(i int) string {
k := toString(keys[i], true)
v := toString(val.MapIndex(keys[i]), true)
return fmt.Sprintf("%s: %s", k, v)
})
case vdl.TypeObject:
return val.String()
default:
panic(fmt.Sprintf("unknown Kind %s", val.Kind()))
}
}
// Converts a VDL value to string using the corresponding native type String()
// method.
func toStringNative(val *vdl.Value) (string, error) {
var natVal interface{}
if err := vdl.Convert(&natVal, val); err != nil {
return "", fmt.Errorf("failed converting %s to native value: %v", val.Type().String(), err)
}
if _, ok := natVal.(*vdl.Value); ok {
return "", fmt.Errorf("failed converting %s to native value: got vdl.Value", val.Type().String())
}
if strNatVal, ok := natVal.(fmt.Stringer); !ok {
return "", fmt.Errorf("native value of %s doesn't implement String()", val.Type().String())
} else {
return strNatVal.String(), nil
}
}
// Stringifies a sequence of n elements, where element i string representation
// is obtained using elemToString(i),
func listToString(begin, sep, end string, n int, elemToString func(i int) string) string {
elems := make([]string, n)
for i, _ := range elems {
elems[i] = elemToString(i)
}
return begin + strings.Join(elems, sep) + end
}
// Converts VDL value to JSON representation.
func toJson(val interface{}) string {
jf := toJsonFriendly(vdl.ValueOf(val))
jOut, err := json.Marshal(jf)
if err != nil {
panic(fmt.Sprintf("JSON marshalling failed: %v", err))
}
return string(jOut)
}
// Converts VDL value to Go type compatible with json.Marshal().
func toJsonFriendly(val *vdl.Value) interface{} {
switch val.Type() {
case vdl.TypeOf(vtime.Time{}), vdl.TypeOf(vtime.Duration{}):
s, err := toStringNative(val)
if err != nil {
panic(fmt.Sprintf("toStringNative failed for builtin time type: %v", err))
}
return s
default:
// fall through to Kind switch
}
switch val.Kind() {
case vdl.Bool:
return val.Bool()
case vdl.Byte, vdl.Uint16, vdl.Uint32, vdl.Uint64:
return val.Uint()
case vdl.Int8, vdl.Int16, vdl.Int32, vdl.Int64:
return val.Int()
case vdl.Float32, vdl.Float64:
return val.Float()
case vdl.String:
return val.RawString()
case vdl.Enum:
return val.EnumLabel()
case vdl.Array, vdl.List:
arr := make([]interface{}, val.Len())
for i, _ := range arr {
arr[i] = toJsonFriendly(val.Index(i))
}
return arr
case vdl.Any, vdl.Optional:
if val.IsNil() {
return nil
}
return toJsonFriendly(val.Elem())
case vdl.Struct:
// TODO(ivanpi): Consider lowercasing field names.
return toOrderedMap(val.Type().NumField(), func(i int) (string, interface{}) {
return val.Type().Field(i).Name, toJsonFriendly(val.StructField(i))
})
case vdl.Union:
// TODO(ivanpi): Consider lowercasing field name.
ui, uv := val.UnionField()
return toOrderedMap(1, func(_ int) (string, interface{}) {
return val.Type().Field(ui).Name, toJsonFriendly(uv)
})
case vdl.Set:
// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
// Use a more sensible sort for numbers etc.
keys := vdl.SortValuesAsString(val.Keys())
return toOrderedMap(len(keys), func(i int) (string, interface{}) {
return toString(keys[i], false), true
})
case vdl.Map:
// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
// Use a more sensible sort for numbers etc.
keys := vdl.SortValuesAsString(val.Keys())
return toOrderedMap(len(keys), func(i int) (string, interface{}) {
return toString(keys[i], false), toJsonFriendly(val.MapIndex(keys[i]))
})
case vdl.TypeObject:
return val.String()
default:
panic(fmt.Sprintf("unknown Kind %s", val.Kind()))
}
}
// Serializes to JSON object, preserving key order.
// Native Go map will serialize to JSON object with sorted keys, which is
// unexpected behaviour for a struct.
type orderedMap []orderedMapElem
type orderedMapElem struct {
Key string
Val interface{}
}
var _ json.Marshaler = (*orderedMap)(nil)
// Builds an orderedMap with n elements, obtaining the key and value of element
// i using elemToKeyVal(i).
func toOrderedMap(n int, elemToKeyVal func(i int) (string, interface{})) orderedMap {
om := make(orderedMap, n)
for i, _ := range om {
om[i].Key, om[i].Val = elemToKeyVal(i)
}
return om
}
// Serializes orderedMap to JSON object, preserving key order.
func (om orderedMap) MarshalJSON() (_ []byte, rerr error) {
defer func() {
if r := recover(); r != nil {
rerr = fmt.Errorf("orderedMap: %v", r)
}
}()
return []byte(listToString("{", ",", "}", len(om), func(i int) string {
keyJson, err := json.Marshal(om[i].Key)
if err != nil {
panic(err)
}
valJson, err := json.Marshal(om[i].Val)
if err != nil {
panic(err)
}
return fmt.Sprintf("%s:%s", keyJson, valJson)
})), nil
}