From fd0251bc31f3810650bcaffd13cbb9327616c349 Mon Sep 17 00:00:00 2001
From: Alex Clemmer <clemmer.alexander@gmail.com>
Date: Sun, 10 Sep 2017 14:19:01 -0700
Subject: [PATCH] Add pretty-printing for `prototype search` results

When a user runs `prototype search`, we'd like for the output to include
the name, description, and available template types, and we'd like that
output to be padded for readability. For example, if the user runs
`prototype search io.`, we'd like to output something like this:

   io.whatever.pkg.foo    Foo's main template    [jsonnet, yaml]
   io.whatever.pkg.foobar Foobar's main template [jsonnet,   yaml, json]

This commit will introduce this style of padded output to the `prototype
search` subcommand.
---
 cmd/prototype.go              |  4 +--
 prototype/index.go            |  2 +-
 prototype/interface.go        |  2 +-
 prototype/specification.go    | 50 +++++++++++++++++++++++++++++++++++
 prototype/systemPrototypes.go |  8 ++++--
 5 files changed, 59 insertions(+), 7 deletions(-)

diff --git a/cmd/prototype.go b/cmd/prototype.go
index 92f85e91..f7076459 100644
--- a/cmd/prototype.go
+++ b/cmd/prototype.go
@@ -151,9 +151,7 @@ var prototypeSearchCmd = &cobra.Command{
 			return fmt.Errorf("Failed to find any search results for query '%s'", query)
 		}
 
-		for _, proto := range protos {
-			fmt.Println(proto.Name)
-		}
+		fmt.Print(protos)
 
 		return nil
 	},
diff --git a/prototype/index.go b/prototype/index.go
index 385dddd3..de3765b7 100644
--- a/prototype/index.go
+++ b/prototype/index.go
@@ -13,7 +13,7 @@ type index struct {
 	prototypes map[string]*SpecificationSchema
 }
 
-func (idx *index) SearchNames(query string, opts SearchOptions) ([]*SpecificationSchema, error) {
+func (idx *index) SearchNames(query string, opts SearchOptions) (SpecificationSchemas, error) {
 	// TODO(hausdorff): This is the world's worst search algorithm. Improve it at
 	// some point.
 
diff --git a/prototype/interface.go b/prototype/interface.go
index ad258891..b5e00b2e 100644
--- a/prototype/interface.go
+++ b/prototype/interface.go
@@ -31,7 +31,7 @@ const (
 
 // Index represents a queryable index of prototype specifications.
 type Index interface {
-	SearchNames(query string, opts SearchOptions) ([]*SpecificationSchema, error)
+	SearchNames(query string, opts SearchOptions) (SpecificationSchemas, error)
 }
 
 // NewIndex constructs an index of prototype specifications from a list.
diff --git a/prototype/specification.go b/prototype/specification.go
index d0322d52..13e5cd7b 100644
--- a/prototype/specification.go
+++ b/prototype/specification.go
@@ -2,6 +2,7 @@ package prototype
 
 import (
 	"fmt"
+	"sort"
 	"strconv"
 	"strings"
 )
@@ -26,6 +27,52 @@ type SpecificationSchema struct {
 	Template SnippetSchema `json:"template"`
 }
 
+// SpecificationSchemas is a slice of pointer to `SpecificationSchema`.
+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
+		}
+	}
+
+	maxNameDescLen := 0
+	for _, proto := range ss {
+		nameDescLen := maxNameLen + 1 + len(proto.Template.ShortDescription)
+		if nameDescLen > maxNameDescLen {
+			maxNameDescLen = nameDescLen
+		}
+	}
+
+	lines := []string{}
+	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")
+	}
+
+	sort.Slice(lines, func(i, j int) bool { return lines[i] < lines[j] })
+
+	return strings.Join(lines, "")
+}
+
 // RequiredParams retrieves all parameters that are required by a prototype.
 func (s *SpecificationSchema) RequiredParams() ParamSchemas {
 	reqd := ParamSchemas{}
@@ -87,6 +134,9 @@ type SnippetSchema struct {
 	// Description describes what the prototype does.
 	Description string `json:"description"`
 
+	// ShortDescription briefly describes what the prototype does.
+	ShortDescription string `json:"shortDescription"`
+
 	// Various body types of the prototype. Follows the TextMate snippets syntax,
 	// with several features disallowed. At least one of these is required to be
 	// filled out.
diff --git a/prototype/systemPrototypes.go b/prototype/systemPrototypes.go
index 192c575e..3526db6e 100644
--- a/prototype/systemPrototypes.go
+++ b/prototype/systemPrototypes.go
@@ -10,6 +10,7 @@ var defaultPrototypes = []*SpecificationSchema{
 		Template: SnippetSchema{
 			Description: `A simple namespace. Labels are automatically populated from the name of the
 namespace.`,
+			ShortDescription: `Namespace with labels automatically populated from the name`,
 			YAMLBody: []string{
 				"kind: Namespace",
 				"apiVersion: v1",
@@ -53,6 +54,7 @@ namespace.`,
 		Template: SnippetSchema{
 			Description: `A service that exposes 'servicePort', and directs traffic
 to 'targetLabelSelector', at 'targetPort'.`,
+			ShortDescription: `Service that exposes a single port`,
 			YAMLBody: []string{
 				"kind: Service",
 				"apiVersion: v1",
@@ -101,13 +103,14 @@ to 'targetLabelSelector', at 'targetPort'.`,
 	},
 	&SpecificationSchema{
 		APIVersion: "0.1",
-		Name:       "io.ksonnet.pkg.empty-configMap",
+		Name:       "io.ksonnet.pkg.configMap",
 		Params: ParamSchemas{
 			RequiredParam("name", "name", "Name to give the configMap.", String),
 			OptionalParam("data", "data", "Data for the configMap.", "{}", Object),
 		},
 		Template: SnippetSchema{
-			Description: `A simple config map. Contains no data.`,
+			Description:      `A simple config map with optional user-specified data.`,
+			ShortDescription: `A simple config map with optional user-specified data`,
 			YAMLBody: []string{
 				"apiVersion: v1",
 				"kind: ConfigMap",
@@ -148,6 +151,7 @@ to 'targetLabelSelector', at 'targetPort'.`,
 			Description: `A deployment that replicates container 'image' some number of times
 (default: 1), and exposes a port (default: 80). Labels are automatically
 populated from 'name'.`,
+			ShortDescription: `Replicates a container n times, exposes a single port`,
 			YAMLBody: []string{
 				"apiVersion: apps/v1beta1",
 				"kind: Deployment",
-- 
GitLab