| // 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 |
| ) |
| |
| type FormattingWriter interface { |
| Write(columnNames []string, rs syncbase.ResultStream) error |
| } |
| |
| type tableWriter struct { |
| w io.Writer |
| } |
| |
| func NewTableWriter(w io.Writer) FormattingWriter { |
| return &tableWriter{w} |
| } |
| |
| // Write formats the results as ASCII tables. |
| func (t *tableWriter) Write(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(t.w, columnWidths) |
| sep := "| " |
| for i, cName := range columnNames { |
| io.WriteString(t.w, fmt.Sprintf("%s%*s", sep, columnWidths[i], cName)) |
| sep = " | " |
| } |
| io.WriteString(t.w, " |\n") |
| writeBorder(t.w, columnWidths) |
| for _, result := range results { |
| sep = "| " |
| for i, column := range result { |
| if justification[i] == Right { |
| io.WriteString(t.w, fmt.Sprintf("%s%*s", sep, columnWidths[i], column)) |
| } else { |
| io.WriteString(t.w, fmt.Sprintf("%s%-*s", sep, columnWidths[i], column)) |
| } |
| sep = " | " |
| } |
| io.WriteString(t.w, " |\n") |
| } |
| writeBorder(t.w, 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 |
| } |
| } |
| |
| type csvWriter struct { |
| w io.Writer |
| delimiter string |
| } |
| |
| func NewCSVWriter(w io.Writer, delimiter string) FormattingWriter { |
| return &csvWriter{w, delimiter} |
| } |
| |
| // Write formats the results as CSV as specified by https://tools.ietf.org/html/rfc4180. |
| func (c *csvWriter) Write(columnNames []string, rs syncbase.ResultStream) error { |
| delim := "" |
| for _, cName := range columnNames { |
| str := doubleQuoteForCSV(cName, c.delimiter) |
| io.WriteString(c.w, fmt.Sprintf("%s%s", delim, str)) |
| delim = c.delimiter |
| } |
| io.WriteString(c.w, "\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), c.delimiter) |
| io.WriteString(c.w, fmt.Sprintf("%s%s", delim, str)) |
| delim = c.delimiter |
| } |
| io.WriteString(c.w, "\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 |
| } |
| |
| type jsonWriter struct { |
| w io.Writer |
| } |
| |
| func NewJSONWriter(w io.Writer) FormattingWriter { |
| return &jsonWriter{w} |
| } |
| |
| // Write formats the result as a JSON array of arrays (rows) of values. |
| func (j *jsonWriter) Write(columnNames []string, rs syncbase.ResultStream) error { |
| io.WriteString(j.w, "[") |
| 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(j.w, 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(j.w, fmt.Sprintf("%s%s: %s", linestart, jsonColNames[i], str)) |
| linestart = ",\n " |
| } |
| io.WriteString(j.w, "\n}") |
| bOpen = ", {" |
| } |
| io.WriteString(j.w, "]\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 |
| } |