Skip to content
Snippets Groups Projects
Commit 9c2d0a2d authored by Alex Clemmer's avatar Alex Clemmer Committed by Jess
Browse files

:nail_care: Add row-padding string pretty printer

Historically, every time we've needed to write out something tabular,
we've hand-rolled an ad hoc padded row writer. Yes, in each case. Such
is #startuplife.

The third time we did this we wrote this with bugs, and so now we are
rewriting it in a general, tested function, and transitioning all places
where we call this to use this function.
parent dec48302
No related branches found
No related tags found
No related merge requests found
......@@ -21,6 +21,7 @@ import (
"strings"
"github.com/ksonnet/ksonnet/metadata"
"github.com/ksonnet/ksonnet/utils"
"github.com/spf13/cobra"
)
......@@ -105,6 +106,13 @@ var depListCmd = &cobra.Command{
Use: "list",
Short: `Lists information about all dependencies known to the current ksonnet app`,
RunE: func(cmd *cobra.Command, args []string) error {
const (
nameHeader = "NAME"
registryHeader = "REGISTRY"
installedHeader = "INSTALLED"
installed = "*"
)
if len(args) != 0 {
return fmt.Errorf("Command 'dep list' does not take arguments")
}
......@@ -125,6 +133,13 @@ var depListCmd = &cobra.Command{
return err
}
rows := [][]string{
[]string{nameHeader, registryHeader, installedHeader},
[]string{
strings.Repeat("=", len(nameHeader)),
strings.Repeat("=", len(registryHeader)),
strings.Repeat("=", len(installedHeader))},
}
for name := range app.Registries {
reg, _, err := manager.GetRegistry(name)
if err != nil {
......@@ -132,10 +147,20 @@ var depListCmd = &cobra.Command{
}
for libName := range reg.Libraries {
fmt.Println(libName)
_, isInstalled := app.Libraries[libName]
if isInstalled {
rows = append(rows, []string{libName, name, installed})
} else {
rows = append(rows, []string{libName, name})
}
}
}
formatted, err := utils.PadRows(rows)
if err != nil {
return err
}
fmt.Print(formatted)
return nil
},
}
......
......@@ -24,6 +24,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/ksonnet/ksonnet/metadata"
"github.com/ksonnet/ksonnet/utils"
)
type EnvAddCmd struct {
......@@ -90,44 +91,22 @@ func (c *EnvListCmd) Run(out io.Writer) error {
// Sort environments by ascending alphabetical name
sort.Slice(envs, func(i, j int) bool { return envs[i].Name < envs[j].Name })
// Format each environment information for pretty printing.
// Each environment should be outputted like the following:
//
// NAME NAMESPACE SERVER
// minikube dev localhost:8080
// us-west/staging staging http://example.com
//
// To accomplish this, need to find the longest env name and the longest
// env namespace for proper padding.
maxNameLen := len(nameHeader)
for _, env := range envs {
if l := len(env.Name); l > maxNameLen {
maxNameLen = l
}
rows := [][]string{
[]string{nameHeader, namespaceHeader, serverHeader},
[]string{
strings.Repeat("=", len(nameHeader)),
strings.Repeat("=", len(namespaceHeader)),
strings.Repeat("=", len(serverHeader))},
}
maxNamespaceLen := len(namespaceHeader) + maxNameLen + 1
for _, env := range envs {
if l := len(env.Namespace) + maxNameLen + 1; l > maxNamespaceLen {
maxNamespaceLen = l
}
rows = append(rows, []string{env.Name, env.Namespace, env.Server})
}
lines := []string{}
headerNameSpacing := strings.Repeat(" ", maxNameLen-len(nameHeader)+1)
headerNamespaceSpacing := strings.Repeat(" ", maxNamespaceLen-maxNameLen-len(namespaceHeader))
lines = append(lines, nameHeader+headerNameSpacing+namespaceHeader+headerNamespaceSpacing+serverHeader+"\n")
for _, env := range envs {
nameSpacing := strings.Repeat(" ", maxNameLen-len(env.Name)+1)
namespaceSpacing := strings.Repeat(" ", maxNamespaceLen-maxNameLen-len(env.Namespace))
lines = append(lines, env.Name+nameSpacing+env.Namespace+namespaceSpacing+env.Server+"\n")
formattedEnvsList, err := utils.PadRows(rows)
if err != nil {
return err
}
formattedEnvsList := strings.Join(lines, "")
_, err = fmt.Fprint(out, formattedEnvsList)
return err
}
......
......@@ -24,6 +24,7 @@ import (
"strings"
param "github.com/ksonnet/ksonnet/metadata/params"
"github.com/ksonnet/ksonnet/utils"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
......@@ -158,88 +159,46 @@ func (c *ParamListCmd) Run(out io.Writer) error {
func outputParamsFor(component string, params param.Params, out io.Writer) error {
keys := sortedParams(params)
//
// Format each parameter information for pretty printing.
// Each parameter will be outputted alphabetically like the following:
//
// PARAM VALUE
// name "foo"
// replicas 1
//
maxParamLen := len(paramNameHeader)
for _, k := range keys {
if l := len(k); l > maxParamLen {
maxParamLen = l
}
rows := [][]string{
[]string{paramNameHeader, paramValueHeader},
[]string{strings.Repeat("=", len(paramNameHeader)), strings.Repeat("=", len(paramValueHeader))},
}
nameSpacing := strings.Repeat(" ", maxParamLen-len(paramNameHeader)+1)
lines := []string{}
lines = append(lines, paramNameHeader+nameSpacing+paramValueHeader+"\n")
lines = append(lines, strings.Repeat("=", len(paramNameHeader))+nameSpacing+
strings.Repeat("=", len(paramValueHeader))+"\n")
for _, k := range keys {
nameSpacing = strings.Repeat(" ", maxParamLen-len(k)+1)
lines = append(lines, k+nameSpacing+params[k]+"\n")
rows = append(rows, []string{k, params[k]})
}
_, err := fmt.Fprint(out, strings.Join(lines, ""))
formatted, err := utils.PadRows(rows)
if err != nil {
return err
}
_, err = fmt.Fprint(out, formatted)
return err
}
func outputParams(params map[string]param.Params, out io.Writer) error {
keys := sortedKeys(params)
//
// Format each component parameter information for pretty printing.
// Each component will be outputted alphabetically like the following:
//
// COMPONENT PARAM VALUE
// bar name "bar"
// bar replicas 2
// foo name "foo"
// foo replicas 1
//
maxComponentLen := len(paramComponentHeader)
for _, k := range keys {
if l := len(k); l > maxComponentLen {
maxComponentLen = l
}
rows := [][]string{
[]string{paramComponentHeader, paramNameHeader, paramValueHeader},
[]string{
strings.Repeat("=", len(paramComponentHeader)),
strings.Repeat("=", len(paramNameHeader)),
strings.Repeat("=", len(paramValueHeader))},
}
maxParamLen := len(paramNameHeader) + maxComponentLen + 1
for _, k := range keys {
for p := range params[k] {
if l := len(p) + maxComponentLen + 1; l > maxParamLen {
maxParamLen = l
}
}
}
componentSpacing := strings.Repeat(" ", maxComponentLen-len(paramComponentHeader)+1)
nameSpacing := strings.Repeat(" ", maxParamLen-maxComponentLen-len(paramNameHeader))
lines := []string{}
lines = append(lines, paramComponentHeader+componentSpacing+paramNameHeader+nameSpacing+paramValueHeader+"\n")
lines = append(lines, strings.Repeat("=", len(paramComponentHeader))+componentSpacing+
strings.Repeat("=", len(paramNameHeader))+nameSpacing+strings.Repeat("=", len(paramValueHeader))+"\n")
for _, k := range keys {
// sort params to display alphabetically
ps := sortedParams(params[k])
for _, p := range ps {
componentSpacing = strings.Repeat(" ", maxComponentLen-len(k)+1)
nameSpacing = strings.Repeat(" ", maxParamLen-maxComponentLen-len(p))
lines = append(lines, k+componentSpacing+p+nameSpacing+params[k][p]+"\n")
rows = append(rows, []string{k, p, params[k][p]})
}
}
_, err := fmt.Fprint(out, strings.Join(lines, ""))
formatted, err := utils.PadRows(rows)
if err != nil {
return err
}
_, err = fmt.Fprint(out, formatted)
return err
}
......
......@@ -6,6 +6,9 @@ import (
"sort"
"strconv"
"strings"
"github.com/ksonnet/ksonnet/utils"
log "github.com/sirupsen/logrus"
)
//
......@@ -135,46 +138,27 @@ type SpecificationSchema struct {
type SpecificationSchemas []*SpecificationSchema
func (ss SpecificationSchemas) String() string {
//
// We want output that's lined up, like:
//
// io.whatever.pkg.foo Foo's main template [jsonnet, yaml]
// io.whatever.pkg.foobar Foobar's main template [jsonnet, yaml, json]
//
// To accomplish this we find (1) the longest prototype name, and (2) the
// longest description, so that we can properly pad the output.
//
maxNameLen := 0
for _, proto := range ss {
if l := len(proto.Name); l > maxNameLen {
maxNameLen = l
}
}
const (
nameHeader = "NAME"
descriptionHeader = "DESCRIPTION"
)
maxNameDescLen := 0
for _, proto := range ss {
nameDescLen := maxNameLen + 1 + len(proto.Template.ShortDescription)
if nameDescLen > maxNameDescLen {
maxNameDescLen = nameDescLen
}
}
sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name })
lines := []string{}
rows := [][]string{
[]string{nameHeader, descriptionHeader},
[]string{strings.Repeat("=", len(nameHeader)), strings.Repeat("=", len(descriptionHeader))},
}
for _, proto := range ss {
// NOTE: If we don't add 1 below, the longest name will look like :
// `io.whatever.pkg.fooDescription is here.`
nameSpace := strings.Repeat(" ", maxNameLen-len(proto.Name)+1)
descSpace := strings.Repeat(" ", maxNameDescLen-maxNameLen-len(proto.Template.ShortDescription)+2)
avail := fmt.Sprintf("%s", proto.Template.AvailableTemplates())
lines = append(lines, proto.Name+nameSpace+proto.Template.ShortDescription+descSpace+avail+"\n")
rows = append(rows, []string{proto.Name, proto.Template.ShortDescription})
}
sort.Slice(lines, func(i, j int) bool { return lines[i] < lines[j] })
formatted, err := utils.PadRows(rows)
if err != nil {
log.Errorf("Failed to print spec rows:\n%v", err)
}
return strings.Join(lines, "")
return formatted
}
// RequiredParams retrieves all parameters that are required by a prototype.
......
......@@ -16,6 +16,7 @@
package utils
import (
"bytes"
"strings"
)
......@@ -30,3 +31,59 @@ func IsASCIIIdentifier(s string) bool {
}
return true
}
func PadRows(rows [][]string) (string, error) {
maxRowLen := 0
for _, row := range rows {
if rowLen := len(row); rowLen > maxRowLen {
maxRowLen = rowLen
}
}
colMaxes := make([]int, maxRowLen)
for currCol := 0; currCol < maxRowLen; currCol++ {
for _, row := range rows {
rowLen := len(row)
if currCol >= rowLen {
continue
}
cellLen := len(row[currCol])
if currCol < rowLen && colMaxes[currCol] < cellLen {
colMaxes[currCol] = cellLen
}
}
}
var err error
var buf bytes.Buffer
for _, row := range rows {
rowLen := len(row)
for j, col := range row {
_, err = buf.WriteString(col)
if err != nil {
return "", err
}
// Don't add space to the end of the last column.
if j >= rowLen-1 {
continue
}
padSize := colMaxes[j] + 1 - len(col)
_, err = buf.WriteString(strings.Repeat(" ", padSize))
if err != nil {
return "", err
}
}
// Add a newline to the end of the row (but only if there is more
// than 0 rows).
_, err = buf.WriteString("\n")
if err != nil {
return "", err
}
}
return buf.String(), nil
}
......@@ -16,6 +16,7 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
......@@ -51,3 +52,74 @@ func TestIsASCIIIdentifier(t *testing.T) {
require.EqualValues(t, test.expected, IsASCIIIdentifier(test.input))
}
}
func TestPadRows(t *testing.T) {
tests := []struct {
input [][]string
expected string
}{
{
input: [][]string{},
expected: ``,
},
{
input: [][]string{
[]string{"Hello", "World"},
},
expected: "Hello World\n",
},
{
input: [][]string{
[]string{"Hello", "World"},
[]string{"Hi", "World"},
},
expected: `Hello World
Hi World
`,
},
{
input: [][]string{
[]string{"Hello"},
[]string{"Hi", "World"},
},
expected: `Hello
Hi World
`,
},
{
input: [][]string{
[]string{},
[]string{"Hi", "World"},
},
expected: `
Hi World
`,
},
{
input: [][]string{
[]string{"Hello", "World"},
[]string{""},
},
expected: `Hello World
`,
},
{
input: [][]string{
[]string{""},
[]string{""},
},
expected: `
`,
},
}
for _, test := range tests {
fmt.Println(test.expected)
padded, err := PadRows(test.input)
if err != nil {
t.Error(err)
}
require.EqualValues(t, test.expected, padded)
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment