forked from toolshed/abra
		
	Introduce a JSON output table mechanic
- Create JSONTable as a proxy/extension to tablewriter which can also output JSON. - Implement machine readable output for `server list` and `recipe list`
This commit is contained in:
		| @ -7,12 +7,12 @@ import ( | ||||
| 	"coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/jsontable" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/abra/pkg/secret" | ||||
| 	"coopcloud.tech/abra/pkg/ssh" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/olekukonko/tablewriter" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli" | ||||
| ) | ||||
| @ -144,7 +144,7 @@ func NewAction(c *cli.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	var secrets AppSecrets | ||||
| 	var secretTable *tablewriter.Table | ||||
| 	var secretTable *jsontable.JSONTable | ||||
| 	if Secrets { | ||||
| 		if err := ssh.EnsureHostKey(NewAppServer); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
|  | ||||
| @ -27,6 +27,7 @@ var recipeListCommand = cli.Command{ | ||||
| 	Aliases: []string{"ls"}, | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.DebugFlag, | ||||
| 		internal.MachineReadableFlag, | ||||
| 		patternFlag, | ||||
| 	}, | ||||
| 	Before: internal.SubCommandBefore, | ||||
| @ -66,10 +67,14 @@ var recipeListCommand = cli.Command{ | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		table.SetCaption(true, fmt.Sprintf("total recipes: %v", len)) | ||||
|  | ||||
| 		if table.NumLines() > 0 { | ||||
| 			table.Render() | ||||
| 			if internal.MachineReadable { | ||||
| 				table.SetCaption(false, "") | ||||
| 				table.JSONRender() | ||||
| 			} else { | ||||
| 				table.SetCaption(true, fmt.Sprintf("total recipes: %v", len)) | ||||
| 				table.Render() | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
|  | ||||
| @ -18,6 +18,7 @@ var serverListCommand = cli.Command{ | ||||
| 	Usage:   "List managed servers", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.DebugFlag, | ||||
| 		internal.MachineReadableFlag, | ||||
| 	}, | ||||
| 	Before: internal.SubCommandBefore, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| @ -29,8 +30,11 @@ var serverListCommand = cli.Command{ | ||||
|  | ||||
| 		tableColumns := []string{"name", "host", "user", "port"} | ||||
| 		table := formatter.CreateTable(tableColumns) | ||||
| 		defer table.Render() | ||||
|  | ||||
| 		if internal.MachineReadable { | ||||
| 			defer table.JSONRender() | ||||
| 		} else { | ||||
| 			defer table.Render() | ||||
| 		} | ||||
| 		serverNames, err := config.ReadServerNames() | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
|  | ||||
| @ -6,7 +6,8 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/docker/go-units" | ||||
| 	"github.com/olekukonko/tablewriter" | ||||
| 	// "github.com/olekukonko/tablewriter" | ||||
| 	"coopcloud.tech/abra/pkg/jsontable" | ||||
| 	"github.com/schollz/progressbar/v3" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
| @ -32,8 +33,8 @@ func HumanDuration(timestamp int64) string { | ||||
| } | ||||
|  | ||||
| // CreateTable prepares a table layout for output. | ||||
| func CreateTable(columns []string) *tablewriter.Table { | ||||
| 	table := tablewriter.NewWriter(os.Stdout) | ||||
| func CreateTable(columns []string) *jsontable.JSONTable { | ||||
| 	table := jsontable.NewJSONTable(os.Stdout) | ||||
| 	table.SetAutoWrapText(false) | ||||
| 	table.SetHeader(columns) | ||||
| 	return table | ||||
|  | ||||
							
								
								
									
										203
									
								
								pkg/jsontable/jsontable.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								pkg/jsontable/jsontable.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | ||||
| package jsontable | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/olekukonko/tablewriter" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| // A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output | ||||
| // - Does not strictly support types, just quoted or unquoted values | ||||
| // - Does not support nested values. | ||||
| // If a datalabel is set with SetDataLabel(true, "..."), that will be used as the key for teh data of the table, | ||||
| // otherwise if the caption is set with SetCaption(true, "..."), the data label will be set to the default of | ||||
| // "rows", otherwise the table will output as a JSON list. | ||||
| // | ||||
| // Proxys all actions through to the tablewriter except addrow and addbatch, which it does at render time | ||||
| // | ||||
|  | ||||
| type JSONTable struct { | ||||
| 	out             io.Writer | ||||
| 	colsize         int | ||||
| 	rows            [][]string | ||||
| 	keys            []string | ||||
| 	quoted          []bool // hack to do output typing, quoted vs. unquoted | ||||
| 	hasDataLabel    bool | ||||
| 	dataLabel       string | ||||
| 	hasCaption      bool | ||||
| 	caption         string // the actual caption | ||||
| 	hasCaptionLabel bool | ||||
| 	captionLabel    string // the key in the dictionary for the caption | ||||
| 	tbl             *tablewriter.Table | ||||
| } | ||||
|  | ||||
| func writeChar(w io.Writer, c byte) { | ||||
| 	w.Write([]byte{c}) | ||||
|  | ||||
| } | ||||
|  | ||||
| func NewJSONTable(writer io.Writer) *JSONTable { | ||||
| 	t := &JSONTable{ | ||||
| 		out:             writer, | ||||
| 		colsize:         0, | ||||
| 		rows:            [][]string{}, | ||||
| 		keys:            []string{}, | ||||
| 		quoted:          []bool{}, | ||||
| 		hasDataLabel:    false, | ||||
| 		dataLabel:       "rows", | ||||
| 		hasCaption:      false, | ||||
| 		caption:         "", | ||||
| 		hasCaptionLabel: false, | ||||
| 		captionLabel:    "caption", | ||||
| 		tbl:             tablewriter.NewWriter(writer), | ||||
| 	} | ||||
| 	return t | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) NumLines() int { | ||||
| 	// JSON only but reflects a shared state. | ||||
| 	return len(t.rows) | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetHeader(keys []string) { | ||||
| 	// Set the keys value which will assign each column to the keys. | ||||
| 	// Note that we'll ignore values that are beyond the length of the keys list | ||||
| 	t.colsize = len(keys) | ||||
| 	t.keys = []string{} | ||||
| 	for _, k := range keys { | ||||
| 		t.keys = append(t.keys, k) | ||||
| 		t.quoted = append(t.quoted, true) | ||||
| 	} | ||||
| 	t.tbl.SetHeader(keys) | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetColumnQuoting(quoting []bool) { | ||||
| 	// Specify which columns are quoted or unquoted in output | ||||
| 	// JSON only | ||||
| 	for i := 0; i < t.colsize; i++ { | ||||
| 		t.quoted[i] = quoting[i] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) Append(row []string) { | ||||
| 	// We'll just append whatever to the rows list. If they fix the keys after appending rows, it'll work as | ||||
| 	// expected. | ||||
| 	// We should detect if the row is narrower than the key list tho. | ||||
| 	// JSON only (but we use the rows later when rendering a regular table) | ||||
| 	t.rows = append(t.rows, row) | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) Render() { | ||||
| 	// Load the table with rows and render. | ||||
| 	// Proxy only | ||||
| 	for _, row := range t.rows { | ||||
| 		t.tbl.Append(row) | ||||
| 	} | ||||
|  | ||||
| 	t.tbl.Render() | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) _JSONRenderInner() { | ||||
| 	// JSON only | ||||
| 	// Render the list of dictionaries to the writer. | ||||
| 	//// inner render loop | ||||
| 	writeChar(t.out, '[') | ||||
| 	for rowidx, row := range t.rows { | ||||
| 		if rowidx != 0 { | ||||
| 			writeChar(t.out, ',') | ||||
| 		} | ||||
| 		writeChar(t.out, '{') | ||||
| 		for keyidx, key := range t.keys { | ||||
| 			value := "nil" | ||||
| 			if keyidx <= len(row) { | ||||
| 				value = row[keyidx] | ||||
| 			} | ||||
| 			if keyidx != 0 { | ||||
| 				writeChar(t.out, ',') | ||||
| 			} | ||||
| 			if t.quoted[keyidx] { | ||||
| 				fmt.Fprintf(t.out, "\"%s\":\"%s\"", key, value) | ||||
| 			} else { | ||||
| 				fmt.Fprintf(t.out, "\"%s\":%s", key, value) | ||||
| 			} | ||||
| 		} | ||||
| 		writeChar(t.out, '}') | ||||
| 	} | ||||
| 	writeChar(t.out, ']') | ||||
|  | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) JSONRender() { | ||||
| 	// write JSON table to output | ||||
| 	// JSON only | ||||
|  | ||||
| 	if t.hasDataLabel || t.hasCaption { | ||||
| 		// dict mode | ||||
| 		writeChar(t.out, '{') | ||||
|  | ||||
| 		if t.hasCaption { | ||||
| 			fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption) | ||||
|  | ||||
| 		} | ||||
| 		fmt.Fprintf(t.out, "\"%s\":", t.dataLabel) | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	// write list | ||||
| 	t._JSONRenderInner() | ||||
|  | ||||
| 	if t.hasDataLabel || t.hasCaption { | ||||
| 		// dict mode | ||||
| 		writeChar(t.out, '}') | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetCaption(caption bool, captionText ...string) { | ||||
| 	t.hasCaption = caption | ||||
| 	if len(captionText) == 1 { | ||||
| 		t.caption = captionText[0] | ||||
| 	} | ||||
| 	t.tbl.SetCaption(caption, captionText...) | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetCaptionLabel(captionLabel bool, captionLabelText ...string) { | ||||
| 	// JSON only | ||||
| 	t.hasCaptionLabel = captionLabel | ||||
| 	if len(captionLabelText) == 1 { | ||||
| 		t.captionLabel = captionLabelText[0] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetDataLabel(dataLabel bool, dataLabelText ...string) { | ||||
| 	// JSON only | ||||
| 	t.hasDataLabel = dataLabel | ||||
| 	if len(dataLabelText) == 1 { | ||||
| 		t.dataLabel = dataLabelText[0] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) AppendBulk(rows [][]string) { | ||||
| 	// JSON only but reflects shared state | ||||
| 	for _, row := range rows { | ||||
| 		t.Append(row) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /// Stuff we should implement but we just proxy for now. | ||||
|  | ||||
| func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) { | ||||
| 	// FIXME | ||||
| 	t.tbl.SetAutoMergeCellsByColumnIndex(cols) | ||||
| } | ||||
|  | ||||
| func (t *JSONTable) SetAutoMergeCells(auto bool) { | ||||
| 	// FIXME | ||||
| 	t.tbl.SetAutoMergeCells(auto) | ||||
| } | ||||
|  | ||||
| //// Stub functions | ||||
| func (t *JSONTable) SetAutoWrapText(auto bool) { | ||||
| 	t.tbl.SetAutoWrapText(auto) | ||||
| 	return | ||||
| } | ||||
		Reference in New Issue
	
	Block a user