Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
984fdfdb79 | ||
|
|
03f089c9c3 |
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Binaries
|
||||||
|
ry
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of go coverage
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
@@ -5,6 +5,7 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
|
|||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printJSON(w io.Writer, data any) error {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(data)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Printer struct {
|
||||||
|
format string
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(format string, w io.Writer) *Printer {
|
||||||
|
return &Printer{format: format, writer: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Printer) Print(data any) error {
|
||||||
|
switch p.format {
|
||||||
|
case "table":
|
||||||
|
return printTable(p.writer, data)
|
||||||
|
case "json":
|
||||||
|
return printJSON(p.writer, data)
|
||||||
|
case "yaml":
|
||||||
|
return printYAML(p.writer, data)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown output format: %s", p.format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Printer) PrintRaw(raw []byte) error {
|
||||||
|
_, err := fmt.Fprintln(p.writer, string(raw))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testItem struct {
|
||||||
|
ID string `json:"id" table:"ID"`
|
||||||
|
Name string `json:"name" table:"NAME"`
|
||||||
|
Age int `json:"age" table:"AGE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableFormat(t *testing.T) {
|
||||||
|
items := []testItem{
|
||||||
|
{ID: "1", Name: "Alice", Age: 30},
|
||||||
|
{ID: "2", Name: "Bob", Age: 25},
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("table", &buf)
|
||||||
|
if err := printer.Print(items); err != nil {
|
||||||
|
t.Fatalf("Print table error: %v", err)
|
||||||
|
}
|
||||||
|
out := buf.String()
|
||||||
|
if !strings.Contains(out, "ID") || !strings.Contains(out, "Alice") {
|
||||||
|
t.Errorf("table output missing headers or data:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONFormat(t *testing.T) {
|
||||||
|
items := []testItem{
|
||||||
|
{ID: "1", Name: "Alice", Age: 30},
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("json", &buf)
|
||||||
|
if err := printer.Print(items); err != nil {
|
||||||
|
t.Fatalf("Print json error: %v", err)
|
||||||
|
}
|
||||||
|
var parsed []testItem
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
|
||||||
|
t.Fatalf("invalid JSON output: %v", err)
|
||||||
|
}
|
||||||
|
if len(parsed) != 1 || parsed[0].ID != "1" {
|
||||||
|
t.Errorf("JSON content mismatch: %+v", parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYAMLFormat(t *testing.T) {
|
||||||
|
items := []testItem{
|
||||||
|
{ID: "1", Name: "Alice", Age: 30},
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("yaml", &buf)
|
||||||
|
if err := printer.Print(items); err != nil {
|
||||||
|
t.Fatalf("Print yaml error: %v", err)
|
||||||
|
}
|
||||||
|
out := buf.String()
|
||||||
|
if !strings.Contains(out, "id:") || !strings.Contains(out, "Alice") {
|
||||||
|
t.Errorf("yaml output missing data:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRawFormat(t *testing.T) {
|
||||||
|
raw := []byte(`{"raw":"data"}`)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("raw", &buf)
|
||||||
|
if err := printer.PrintRaw(raw); err != nil {
|
||||||
|
t.Fatalf("PrintRaw error: %v", err)
|
||||||
|
}
|
||||||
|
if buf.String() != `{"raw":"data"}`+"\n" {
|
||||||
|
t.Errorf("raw output = %q", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleItem(t *testing.T) {
|
||||||
|
item := testItem{ID: "1", Name: "Solo", Age: 99}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("json", &buf)
|
||||||
|
if err := printer.Print(item); err != nil {
|
||||||
|
t.Fatalf("Print single item error: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(buf.String(), "Solo") {
|
||||||
|
t.Errorf("single item output missing data:\n%s", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownFormat(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printer := New("unknown", &buf)
|
||||||
|
err := printer.Print([]testItem{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for unknown format")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printTable(w io.Writer, data any) error {
|
||||||
|
v := reflect.ValueOf(data)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() == reflect.Slice {
|
||||||
|
return printTableSlice(w, v)
|
||||||
|
}
|
||||||
|
return printTableSingle(w, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTableSlice(w io.Writer, v reflect.Value) error {
|
||||||
|
if v.Len() == 0 {
|
||||||
|
fmt.Fprintln(w, "(empty)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
elemType := v.Index(0).Type()
|
||||||
|
if elemType.Kind() == reflect.Ptr {
|
||||||
|
elemType = elemType.Elem()
|
||||||
|
}
|
||||||
|
headers, cols := extractTableTags(elemType)
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return fmt.Errorf("no table tags found on type %s", elemType.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all rows
|
||||||
|
rows := make([][]string, v.Len())
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
rows[i] = extractRow(v.Index(i), cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
widths := make([]int, len(headers))
|
||||||
|
for i, h := range headers {
|
||||||
|
widths[i] = len(h)
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
for i, cell := range row {
|
||||||
|
if len(cell) > widths[i] {
|
||||||
|
widths[i] = len(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print headers
|
||||||
|
sep := " "
|
||||||
|
renderRow(w, headers, widths, sep)
|
||||||
|
|
||||||
|
// Print separator line
|
||||||
|
separator := make([]string, len(headers))
|
||||||
|
for i, ww := range widths {
|
||||||
|
separator[i] = strings.Repeat("-", ww)
|
||||||
|
}
|
||||||
|
renderRow(w, separator, widths, sep)
|
||||||
|
|
||||||
|
// Print data rows
|
||||||
|
for _, row := range rows {
|
||||||
|
renderRow(w, row, widths, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRow(w io.Writer, cells []string, widths []int, sep string) {
|
||||||
|
parts := make([]string, len(cells))
|
||||||
|
for i, cell := range cells {
|
||||||
|
parts[i] = fmt.Sprintf("%-*s", widths[i], cell)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, strings.Join(parts, sep))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTableSingle(w io.Writer, v reflect.Value) error {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
t := v.Type()
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
tag := t.Field(i).Tag.Get("table")
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := fmt.Sprintf("%v", v.Field(i).Interface())
|
||||||
|
fmt.Fprintf(w, "%-10s %s\n", tag+":", val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTableTags(t reflect.Type) ([]string, []int) {
|
||||||
|
var headers []string
|
||||||
|
var cols []int
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
tag := t.Field(i).Tag.Get("table")
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
headers = append(headers, strings.ToUpper(tag))
|
||||||
|
cols = append(cols, i)
|
||||||
|
}
|
||||||
|
return headers, cols
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRow(v reflect.Value, cols []int) []string {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
row := make([]string, len(cols))
|
||||||
|
for i, idx := range cols {
|
||||||
|
row[i] = fmt.Sprintf("%v", v.Field(idx).Interface())
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printYAML(w io.Writer, data any) error {
|
||||||
|
enc := yaml.NewEncoder(w)
|
||||||
|
enc.SetIndent(2)
|
||||||
|
return enc.Encode(data)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user