diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 643af2d..cec3024 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,9 +58,9 @@ jobs: - name: Run tests run: | git diff --cached --exit-code - go test ./... -v -cover -coverprofile=cover.out + go test ./... -v -cover -coverprofile=cover.Render - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - files: ./cover.out + files: ./cover.Render diff --git a/.gitignore b/.gitignore index 9569a44..aca3c39 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.test # Output of the go coverage tool, specifically when used with LiteIDE -*.out +*.Render # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/Makefile b/Makefile index a3f3883..7e38a90 100644 --- a/Makefile +++ b/Makefile @@ -37,11 +37,11 @@ endif .PHONY: test test: - go test ./... -v -cover -coverprofile=cover.out + go test ./... -v -cover -coverprofile=cover.Render .PHONY: cover cover: - go tool cover -html=cover.out -o cover.html + go tool cover -html=cover.Render -o cover.html .PHONY: bench bench: @@ -78,4 +78,4 @@ publish: deps-gobump check-git .PHONY: clean clean: go clean - rm -f cover.out cover.html cpu.prof mem.prof $(BIN).test + rm -f cover.Render cover.html cpu.prof mem.prof $(BIN).test diff --git a/README.md b/README.md index 318b1ee..9e36e74 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Support - Group rows based on previous field value - Ignore specified columns - Escape HTML special characters +- Set multiple values to a field as a joined string +- Set byte slices as a string Notes ----- @@ -55,7 +57,38 @@ Notes Usage ----- -[example](example_test.go) +[Example](example_test.go) + +Benchmark +--------- + +[A quick benchmark](benchmark.go) + +This is only for reference as the functions are different, but for simple drawing, it has better performance than TableWriter. + +```text +go test -run=^$ -bench=. -benchmem -count 5 +goos: darwin +goarch: arm64 +pkg: github.com/nekrassov01/mintab +BenchmarkMintab-8 45578 25500 ns/op 20527 B/op 399 allocs/op +BenchmarkMintab-8 46488 25449 ns/op 20527 B/op 399 allocs/op +BenchmarkMintab-8 44702 26457 ns/op 20528 B/op 399 allocs/op +BenchmarkMintab-8 42699 28344 ns/op 20527 B/op 399 allocs/op +BenchmarkMintab-8 45213 31852 ns/op 20527 B/op 399 allocs/op +BenchmarkMintabSimple-8 55597 19234 ns/op 13033 B/op 242 allocs/op +BenchmarkMintabSimple-8 64444 18966 ns/op 13033 B/op 242 allocs/op +BenchmarkMintabSimple-8 53935 21939 ns/op 13034 B/op 242 allocs/op +BenchmarkMintabSimple-8 61573 18596 ns/op 13033 B/op 242 allocs/op +BenchmarkMintabSimple-8 64854 19147 ns/op 13033 B/op 242 allocs/op +BenchmarkTableWriter-8 21787 47804 ns/op 25421 B/op 701 allocs/op +BenchmarkTableWriter-8 26362 45354 ns/op 25365 B/op 701 allocs/op +BenchmarkTableWriter-8 26691 44275 ns/op 25332 B/op 701 allocs/op +BenchmarkTableWriter-8 26622 44199 ns/op 25360 B/op 701 allocs/op +BenchmarkTableWriter-8 27138 44492 ns/op 25297 B/op 701 allocs/op +PASS +ok github.com/nekrassov01/mintab 24.097s +``` Author ------ diff --git a/benchmark_test.go b/benchmark_test.go index de5178a..05877e5 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -14,7 +14,7 @@ func BenchmarkMintab(b *testing.B) { if err := table.Load(basicsample); err != nil { b.Fatal(err) } - table.Out() + table.Render() } } @@ -37,12 +37,11 @@ func BenchmarkMintabSimple(b *testing.B) { if err := table.Load(data); err != nil { b.Fatal(err) } - table.Out() + table.Render() } } func BenchmarkTableWriter(b *testing.B) { - header := []string{"InstanceID", "InstanceName", "InstanceState"} data := [][]string{ {"i-1", "server-1", "running"}, {"i-2", "server-2", "stopped"}, @@ -54,10 +53,7 @@ func BenchmarkTableWriter(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { table := tablewriter.NewWriter(&bytes.Buffer{}) - table.SetHeader(header) - for _, v := range data { - table.Append(v) - } + table.AppendBulk(data) table.Render() } } diff --git a/example_test.go b/example_test.go index 7f4cd7c..722948d 100644 --- a/example_test.go +++ b/example_test.go @@ -85,7 +85,7 @@ func ExampleTable_Load_basic() { if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+------------+------------+ @@ -111,11 +111,11 @@ func ExampleTable_Load_basic() { } func ExampleTable_Load_markdown() { - table := mintab.New(os.Stdout, mintab.WithFormat(mintab.FormatMarkdown)) + table := mintab.New(os.Stdout, mintab.WithFormat(mintab.MarkdownFormat)) if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // | InstanceID | InstanceName | AttachedLB | AttachedTG | @@ -129,11 +129,11 @@ func ExampleTable_Load_markdown() { } func ExampleTable_Load_backlog() { - table := mintab.New(os.Stdout, mintab.WithFormat(mintab.FormatBacklog)) + table := mintab.New(os.Stdout, mintab.WithFormat(mintab.BacklogFormat)) if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // | InstanceID | InstanceName | AttachedLB | AttachedTG |h @@ -150,7 +150,7 @@ func ExampleTable_Load_disableheader() { if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+------------+------------+ @@ -178,7 +178,7 @@ func ExampleTable_Load_emptyfieldplaceholder() { if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+------------+------------+ @@ -208,7 +208,7 @@ func ExampleTable_Load_worddelimiter() { if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+------------+---------------------+ @@ -233,7 +233,7 @@ func ExampleTable_Load_margin() { if err := table.Load(s1); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +----------------+------------------+----------------+----------------+ @@ -263,7 +263,7 @@ func ExampleTable_Load_mergefields1() { if err := table.Load(s2); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ @@ -292,7 +292,7 @@ func ExampleTable_Load_mergefields2() { if err := table.Load(s2); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ @@ -317,11 +317,11 @@ func ExampleTable_Load_mergefields2() { } func ExampleTable_Load_mergefields3() { - table := mintab.New(os.Stdout, mintab.WithFormat(mintab.FormatCompressedText), mintab.WithMergeFields([]int{0, 1, 2, 3})) + table := mintab.New(os.Stdout, mintab.WithFormat(mintab.CompressedTextFormat), mintab.WithMergeFields([]int{0, 1, 2, 3})) if err := table.Load(s2); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ @@ -344,7 +344,7 @@ func ExampleTable_Load_ignorefields1() { if err := table.Load(s3); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +---------------+---------------+ @@ -361,7 +361,7 @@ func ExampleTable_Load_ignorefields2() { if err := table.Load(s3); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +---------------+ @@ -378,7 +378,7 @@ func ExampleTable_Load_escape1() { if err := table.Load(s4); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // +-------------------------+-----------------------------------------+ @@ -401,11 +401,11 @@ func ExampleTable_Load_escape1() { } func ExampleTable_Load_escape2() { - table := mintab.New(os.Stdout, mintab.WithFormat(mintab.FormatMarkdown), mintab.WithEscape(false)) + table := mintab.New(os.Stdout, mintab.WithFormat(mintab.MarkdownFormat), mintab.WithEscape(false)) if err := table.Load(s4); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // | Name | EscatableValue | @@ -417,11 +417,11 @@ func ExampleTable_Load_escape2() { } func ExampleTable_Load_escape3() { - table := mintab.New(os.Stdout, mintab.WithFormat(mintab.FormatMarkdown), mintab.WithEscape(true)) + table := mintab.New(os.Stdout, mintab.WithFormat(mintab.MarkdownFormat), mintab.WithEscape(true)) if err := table.Load(s4); err != nil { log.Fatal(err) } - table.Out() + table.Render() // Output: // | Name | EscatableValue | @@ -438,7 +438,7 @@ func ExampleTable_Load_string() { if err := table.Load(s2); err != nil { log.Fatal(err) } - table.Out() + table.Render() fmt.Println(builder.String()) // Output: diff --git a/go.mod b/go.mod index 43a8cac..4e74411 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nekrassov01/mintab go 1.21 require ( + github.com/google/go-cmp v0.6.0 github.com/mattn/go-runewidth v0.0.15 github.com/olekukonko/tablewriter v0.0.5 ) diff --git a/go.sum b/go.sum index 52bd10e..de581a7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= diff --git a/mintab.go b/mintab.go index 64b46a2..8b987fa 100644 --- a/mintab.go +++ b/mintab.go @@ -14,26 +14,30 @@ import ( // Default values for table rendering. const ( - DefaultEmptyFieldPlaceholder = "-" - DefaultWordDelimiter = "\n" - MarkdownDefaultEmptyFieldPlaceholder = "\\-" - MarkdownDefaultWordDelimiter = "
" - BacklogDefaultEmptyFieldPlaceholder = "-" - BacklogDefaultWordDelimiter = "&br;" + TextDefaultEmptyFieldPlaceholder = "-" + TextDefaultWordDelimiter = textNewLine + MarkdownDefaultEmptyFieldPlaceholder = "\\" + TextDefaultEmptyFieldPlaceholder + MarkdownDefaultWordDelimiter = markdownNewLine + BacklogDefaultEmptyFieldPlaceholder = TextDefaultEmptyFieldPlaceholder + BacklogDefaultWordDelimiter = backlogNewLine + + textNewLine = "\n" + markdownNewLine = "
" + backlogNewLine = "&br;" ) -// Format defines the output format of the content. +// A Format represents the output format. type Format int -// Enumeration of supported output formats. +// Supported output formats. const ( - FormatText Format = iota // Plain text format. - FormatCompressedText // Compressed plain text format. - FormatMarkdown // Markdown format. - FormatBacklog // Backlog-specific format. + TextFormat Format = iota // Text table format. + CompressedTextFormat // Compressed text table format. + MarkdownFormat // Markdown table format. + BacklogFormat // Backlog-specific table format. ) -// Formats holds the string representations of each format constant. +// Formats are string representations of output format. var Formats = []string{ "text", "compressed", @@ -42,7 +46,6 @@ var Formats = []string{ } // String returns the string representation of a Format. -// If the format is not within the predefined range, an empty string is returned. func (o Format) String() string { if o >= 0 && int(o) < len(Formats) { return Formats[o] @@ -50,33 +53,37 @@ func (o Format) String() string { return "" } -// Table represents a table structure for rendering data in a matrix format. +// Table represents a table structure for rendering data. type Table struct { - writer io.Writer // Destination for table output. - data [][]string // Data holds the content of the table. - header []string // Names of each field in the table header. - format Format // Output format of the table. - border string // Pre-computed border based on column widths. - tableWidth int // - marginWidth int // - margin string // Margin size around cell content. - emptyFieldPlaceholder string // Placeholder for empty fields. - wordDelimiter string // Delimiter for words within a field. - mergedFields []int // Indices of fields to be merged based on content. - ignoredFields []int // Indices of fields to be ignored during rendering. - columnWidths []int // Calculated max width of each column. - hasHeader bool // Indicates if the header should be rendered. - hasEscape bool // Indicates if escaping should be performed. + writer io.Writer // Destination for table output + data [][]string // Table data + splitedData [][][]string // Table data with strings divided by newlines + header []string // Names of each field in the table header + format Format // Output format + newLine string // + emptyFieldPlaceholder string // Placeholder for empty fields + wordDelimiter string // Delimiter for words within a field + lineHeights []int // + columnWidths []int // Max widths of each columns + border string // Border line based on column widths + tableWidth int // + marginWidth int // Margin size around field values + margin string // Repeating whitespace chars as margins + hasHeader bool // Whether header rendering + hasEscape bool // Whether HTML escaping + mergedFields []int // Indices of fields to be merged + ignoredFields []int // Indices of fields to be ignored } -// New instantiates a new Table with the specified writer and options. +// New instantiates a new Table with the writer and options. func New(w io.Writer, opts ...Option) *Table { t := &Table{ writer: w, - format: FormatText, + format: TextFormat, + newLine: textNewLine, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, marginWidth: 1, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, hasHeader: true, } for _, opt := range opts { @@ -86,24 +93,24 @@ func New(w io.Writer, opts ...Option) *Table { return t } -// Option defines a type for functional options used to configure a Table. +// A Option sets an option on a Table. type Option func(*Table) -// WithFormat sets the output format of the table. +// WithFormat sets the output format. func WithFormat(format Format) Option { return func(t *Table) { t.format = format } } -// WithHeader configures the rendering of the table header. +// WithHeader sets the table header. func WithHeader(has bool) Option { return func(t *Table) { t.hasHeader = has } } -// WithMargin sets the margin size around cell content. +// WithMargin sets the margin size around field values. func WithMargin(width int) Option { return func(t *Table) { t.marginWidth = width @@ -124,21 +131,21 @@ func WithWordDelimiter(wordDelimiter string) Option { } } -// WithMergeFields specifies columns for merging based on their content. +// WithMergeFields specifies columns for merging. func WithMergeFields(mergeFields []int) Option { return func(t *Table) { t.mergedFields = mergeFields } } -// WithIgnoreFields specifies columns to be ignored during rendering. +// WithIgnoreFields specifies columns to be ignored. func WithIgnoreFields(ignoreFields []int) Option { return func(t *Table) { t.ignoredFields = ignoreFields } } -// WithEscape enables or disables escaping of field content. +// WithEscape enables or disables HTML escaping. func WithEscape(has bool) Option { return func(t *Table) { t.hasEscape = has @@ -146,15 +153,14 @@ func WithEscape(has bool) Option { } // Load validates the input and converts it into table data. -// Returns an error if the input is not a slice or if it's empty. -func (t *Table) Load(input any) error { +func (t *Table) Load(v any) error { if t.marginWidth < 0 { return fmt.Errorf("only unsigned integers are allowed in margin") } - if _, ok := input.([]interface{}); ok { + if _, ok := v.([]interface{}); ok { return fmt.Errorf("elements of slice must not be empty interface") } - rv := reflect.ValueOf(input) + rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { rv = rv.Elem() } @@ -179,75 +185,65 @@ func (t *Table) Load(input any) error { if err := t.setData(rv); err != nil { return err } - if t.format != FormatBacklog { + if t.format != BacklogFormat { t.setBorder() } return nil } -// Out renders the table to the specified writer. +// Render renders the table to the specified writer. // It supports markdown and backlog formats for easy copying and pasting. -func (t *Table) Out() { +func (t *Table) Render() { if t.hasHeader { switch t.format { - case FormatText, FormatCompressedText: + case TextFormat, CompressedTextFormat: t.printBorder() } t.printHeader() } - if t.format != FormatBacklog { + if t.format != BacklogFormat { t.printBorder() } t.printData() switch t.format { - case FormatText, FormatCompressedText: + case TextFormat, CompressedTextFormat: t.printBorder() } } -// printHeader renders the table header. func (t *Table) printHeader() { var b strings.Builder b.Grow(t.tableWidth) b.WriteString("|") for i, h := range t.header { - t.pad(&b, h, t.columnWidths[i]) + t.writeField(&b, h, t.columnWidths[i]) b.WriteString("|") } - if t.format == FormatBacklog { + if t.format == BacklogFormat { b.WriteString("h") } fmt.Fprintln(t.writer, b.String()) } -// printData renders the table data with dynamic conditional borders. func (t *Table) printData() { for i, row := range t.data { if i > 0 { - if t.format == FormatText { + if t.format == TextFormat { t.printDataBorder(row) } - if t.format == FormatCompressedText && row[0] != "" { + if t.format == CompressedTextFormat && row[0] != "" { t.printBorder() } } - splited := make([][]string, len(row)) - n := 1 - for j, field := range row { - splited[j] = strings.Split(field, "\n") - if len(splited[j]) > n { - n = len(splited[j]) - } - } - for k := 0; k < n; k++ { + for j := 0; j < t.lineHeights[i]; j++ { var b strings.Builder b.Grow(t.tableWidth) b.WriteString("|") - for l, elem := range splited { - if k < len(elem) { - t.pad(&b, elem[k], t.columnWidths[l]) + for k, elem := range t.splitedData[i] { + if j < len(elem) { + t.writeField(&b, elem[j], t.columnWidths[k]) } else { - t.pad(&b, "", t.columnWidths[l]) + t.writeField(&b, "", t.columnWidths[k]) } b.WriteString("|") } @@ -256,8 +252,6 @@ func (t *Table) printData() { } } -// printDataBorder prints a conditional border based on the emptiness of fields in the current row, -// with continuity in border characters based on the emptiness of adjacent fields. func (t *Table) printDataBorder(row []string) { var b strings.Builder b.Grow(t.tableWidth) @@ -276,36 +270,37 @@ func (t *Table) printDataBorder(row []string) { fmt.Fprintln(t.writer, b.String()) } -// printBorder renders the table border based on column widths. func (t *Table) printBorder() { fmt.Fprintln(t.writer, t.border) } -// setAttr configures placeholders and delimiters based on the table format. -// It ensures consistency in the appearance and structure of table output. func (t *Table) setAttr() { - var p, d string + var p, d, n string switch t.format { - case FormatMarkdown: + case MarkdownFormat: p = MarkdownDefaultEmptyFieldPlaceholder d = MarkdownDefaultWordDelimiter - case FormatBacklog: + n = markdownNewLine + case BacklogFormat: p = BacklogDefaultEmptyFieldPlaceholder d = BacklogDefaultWordDelimiter + n = backlogNewLine default: - p = DefaultEmptyFieldPlaceholder - d = DefaultWordDelimiter + p = TextDefaultEmptyFieldPlaceholder + d = TextDefaultWordDelimiter + n = textNewLine } - if t.emptyFieldPlaceholder == DefaultEmptyFieldPlaceholder { + if t.emptyFieldPlaceholder == TextDefaultEmptyFieldPlaceholder { t.emptyFieldPlaceholder = p } - if t.wordDelimiter == DefaultWordDelimiter { + if t.wordDelimiter == TextDefaultWordDelimiter { t.wordDelimiter = d } + if t.format != TextFormat { + t.newLine = n + } } -// setHeader extracts field names from the struct type to create the table header. -// It also initializes column widths based on the header names. func (t *Table) setHeader(typ reflect.Type) { if len(t.header) > 0 { return @@ -321,55 +316,65 @@ func (t *Table) setHeader(typ reflect.Type) { } } -// setData converts the input data to a matrix of strings and calculates column widths. -// It also handles field formatting based on the table format and whether fields are merged. func (t *Table) setData(rv reflect.Value) error { t.data = make([][]string, rv.Len()) + t.splitedData = make([][][]string, rv.Len()) + t.lineHeights = make([]int, rv.Len()) prev := make([]string, len(t.header)) for i := 0; i < rv.Len(); i++ { - row := make([]string, len(t.header)) - field := rv.Index(i) - if field.Kind() == reflect.Ptr { - field = field.Elem() + e := rv.Index(i) + if e.Kind() == reflect.Ptr { + e = e.Elem() } - merge := true + row := make([]string, len(t.header)) + splitedRow := make([][]string, len(t.header)) + isMerge := true + n := 1 for j, h := range t.header { - field := field.FieldByName(h) + field := e.FieldByName(h) if !field.IsValid() { return fmt.Errorf("invalid field detected: %s", h) } - f, err := t.formatField(field) + v, err := t.formatField(field) if err != nil { return fmt.Errorf("failed to format field \"%s\": %w", h, err) } if slices.Contains(t.mergedFields, j) { - if f != prev[j] { - merge = false - prev[j] = f + if v != prev[j] { + isMerge = false + prev[j] = v } - if merge { - f = "" + if isMerge { + v = "" } } - row[j] = f - elems := strings.Split(f, "\n") + row[j] = v + elems := strings.Split(v, "\n") + splitedRow[j] = elems for _, elem := range elems { - lw := runewidth.StringWidth(elem) - if lw > t.columnWidths[j] { - t.columnWidths[j] = lw + width := runewidth.StringWidth(elem) + if width > t.columnWidths[j] { + t.columnWidths[j] = width + } + } + if t.format == TextFormat { + height := len(elems) + if height > n { + n = height } } } t.data[i] = row + t.splitedData[i] = splitedRow + t.lineHeights[i] = n } return nil } -// setBorder computes the table border string based on the calculated column widths. func (t *Table) setBorder() { var sep string switch t.format { - case FormatMarkdown, FormatBacklog: + case MarkdownFormat, BacklogFormat: sep = "|" default: sep = "+" @@ -386,8 +391,6 @@ func (t *Table) setBorder() { t.tableWidth = len(t.border) } -// formatField formats a single field value based on its type and the table's configuration. -// It applies escaping if enabled and handles various data types, including slices and primitive types. func (t *Table) formatField(rv reflect.Value) (string, error) { if rv.Kind() == reflect.Ptr { if rv.IsNil() { @@ -395,7 +398,7 @@ func (t *Table) formatField(rv reflect.Value) (string, error) { } rv = rv.Elem() } - v := getString(rv) + v := stringer(rv) if v == "" { switch rv.Kind() { case reflect.String: @@ -404,7 +407,9 @@ func (t *Table) formatField(rv reflect.Value) (string, error) { v = strconv.FormatInt(rv.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: v = strconv.FormatUint(rv.Uint(), 10) - case reflect.Float32, reflect.Float64: + case reflect.Float32: + v = strconv.FormatFloat(rv.Float(), 'f', -1, 32) + case reflect.Float64: v = strconv.FormatFloat(rv.Float(), 'f', -1, 64) case reflect.Slice, reflect.Array: switch { @@ -413,72 +418,76 @@ func (t *Table) formatField(rv reflect.Value) (string, error) { case rv.Type().Elem().Kind() == reflect.Uint8: v = string(rv.Bytes()) default: - ss := make([]string, rv.Len()) + var b strings.Builder for i := 0; i < rv.Len(); i++ { e := rv.Index(i) if i != 0 { - ss[i] = t.wordDelimiter + b.WriteString(t.wordDelimiter) } if e.Kind() == reflect.Ptr { if e.IsNil() { - ss[i] = t.emptyFieldPlaceholder + b.WriteString(t.emptyFieldPlaceholder) continue } e = e.Elem() } - if s := getString(e); s != "" { - ss[i] = s + if f := stringer(e); f != "" { + b.WriteString(f) continue } if e.Kind() == reflect.Slice && e.Type().Elem().Kind() == reflect.Uint8 { - ss[i] = string(e.Bytes()) + b.WriteString(string(e.Bytes())) continue } if e.Kind() == reflect.Slice || e.Kind() == reflect.Array || e.Kind() == reflect.Struct { return "", fmt.Errorf("cannot represent nested fields") } - f, err := t.formatField(e) - if err != nil { - return "", err + switch e.Kind() { + case reflect.String: + v = e.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v = strconv.FormatInt(e.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v = strconv.FormatUint(e.Uint(), 10) + case reflect.Float32: + v = strconv.FormatFloat(e.Float(), 'f', -1, 32) + case reflect.Float64: + v = strconv.FormatFloat(e.Float(), 'f', -1, 64) + default: + v = fmt.Sprint(e.Interface()) + } + if v == "" { + v = t.emptyFieldPlaceholder } - ss[i] = f + b.WriteString(v) } - v = strings.Join(ss, t.wordDelimiter) + v = b.String() } default: v = fmt.Sprint(rv.Interface()) } } - if t.hasEscape { - v = t.escape(v) - } - if t.format == FormatMarkdown && strings.HasPrefix(v, "*") { - v = "\\" + v - } - if v == "" { - v = t.emptyFieldPlaceholder - } - return strings.TrimSpace(t.replaceNL(v)), nil + return strings.TrimSuffix(t.sanitize(v), "\n"), nil } -func getString(v reflect.Value) string { - if v.CanInterface() { - if s, ok := v.Interface().(fmt.Stringer); ok { - return s.String() - } +func (t *Table) sanitize(s string) string { + if s == "" { + return t.emptyFieldPlaceholder } - return "" -} - -func (t *Table) replaceNL(s string) string { - if t.wordDelimiter == "\n" { + if t.hasEscape { + s = t.escape(s) + } + if t.format == MarkdownFormat && strings.HasPrefix(s, "*") { + s = "\\" + s + } + if t.format == TextFormat { return s } var b strings.Builder for _, r := range s { switch r { case '\n': - b.WriteString(t.wordDelimiter) + b.WriteString(t.newLine) default: b.WriteRune(r) } @@ -486,7 +495,6 @@ func (t *Table) replaceNL(s string) string { return b.String() } -// escape applies HTML escaping to a string for safe rendering in Markdown and other formats. func (t *Table) escape(s string) string { var b strings.Builder for _, r := range s { @@ -518,8 +526,16 @@ func (t *Table) escape(s string) string { return b.String() } -// pad right-aligns numeric strings and left-aligns all other strings within a field of specified width. -func (t *Table) pad(b *strings.Builder, s string, width int) { +func stringer(rv reflect.Value) string { + if rv.CanInterface() { + if s, ok := rv.Interface().(fmt.Stringer); ok { + return s.String() + } + } + return "" +} + +func (t *Table) writeField(b *strings.Builder, s string, width int) { b.WriteString(t.margin) isN := isNum(s) if !isN { @@ -537,13 +553,12 @@ func (t *Table) pad(b *strings.Builder, s string, width int) { b.WriteString(t.margin) } -// isNum checks if a string represents a numeric value. func isNum(s string) bool { if len(s) == 0 { return false } start := 0 - if s[0] == '-' || s[0] == '+' { + if s[0] == '-' { start = 1 if len(s) == 1 { return false diff --git a/mintab_test.go b/mintab_test.go index 472b258..56c7ff1 100644 --- a/mintab_test.go +++ b/mintab_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" "time" + + "github.com/google/go-cmp/cmp" ) type basicSample struct { @@ -178,22 +180,22 @@ func TestFormat_String(t *testing.T) { }{ { name: "text", - o: FormatText, + o: TextFormat, want: "text", }, { name: "compressed", - o: FormatCompressedText, + o: CompressedTextFormat, want: "compressed", }, { name: "markdown", - o: FormatMarkdown, + o: MarkdownFormat, want: "markdown", }, { name: "backlog", - o: FormatBacklog, + o: BacklogFormat, want: "backlog", }, { @@ -230,12 +232,13 @@ func TestNew(t *testing.T) { writer: &bytes.Buffer{}, data: nil, header: nil, - format: FormatText, + format: TextFormat, + newLine: textNewLine, border: "", marginWidth: 1, margin: " ", - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, mergedFields: nil, ignoredFields: nil, columnWidths: nil, @@ -247,7 +250,7 @@ func TestNew(t *testing.T) { name: "not-default", args: args{ opts: []Option{ - WithFormat(FormatMarkdown), + WithFormat(MarkdownFormat), WithHeader(false), WithMargin(2), WithEmptyFieldPlaceholder(MarkdownDefaultEmptyFieldPlaceholder), @@ -261,7 +264,8 @@ func TestNew(t *testing.T) { writer: &bytes.Buffer{}, data: nil, header: nil, - format: FormatMarkdown, + format: MarkdownFormat, + newLine: textNewLine, // change after setAttr() border: "", marginWidth: 2, margin: " ", @@ -417,12 +421,15 @@ func TestTable_Load(t *testing.T) { } } -func TestTable_Out(t *testing.T) { +func TestTable_Render(t *testing.T) { type fields struct { - format Format - header []string - data [][]string + format Format + header []string + data [][]string + splitedData [][][]string + columnWidths []int + lineHeights []int } tests := []struct { name string @@ -432,7 +439,7 @@ func TestTable_Out(t *testing.T) { { name: "text", fields: fields{ - format: FormatText, + format: TextFormat, header: []string{"InstanceID", "InstanceName", "AttachedLB", "AttachedTG"}, data: [][]string{ {"i-1", "server-1", "lb-1", "tg-1"}, @@ -442,7 +449,16 @@ func TestTable_Out(t *testing.T) { {"i-5", "server-5", "lb-5", "-"}, {"i-6", "server-6", "-", "tg-5\ntg-6\ntg-7\ntg-8"}, }, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2", "lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3", "tg-4"}}, + {{"i-4"}, {"server-4"}, {"-"}, {"-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"-"}}, + {{"i-6"}, {"server-6"}, {"-"}, {"tg-5", "tg-6", "tg-7", "tg-8"}}, + }, columnWidths: []int{10, 12, 10, 10}, + lineHeights: []int{1, 2, 2, 1, 1, 4}, }, want: `+------------+--------------+------------+------------+ | InstanceID | InstanceName | AttachedLB | AttachedTG | @@ -469,7 +485,7 @@ func TestTable_Out(t *testing.T) { { name: "text_with_compressed", fields: fields{ - format: FormatCompressedText, + format: CompressedTextFormat, header: []string{"InstanceID", "InstanceName", "VPCID", "SecurityGroupID", "FlowDirection", "IPProtocol", "FromPort", "ToPort", "AddressType", "CidrBlock"}, data: [][]string{ {"i-1", "server-1", "vpc-1", "sg-1", "Ingress", "tcp", "22", "22", "SecurityGroup", "sg-10"}, @@ -481,7 +497,18 @@ func TestTable_Out(t *testing.T) { {"", "", "", "", "Ingress", "tcp", "0", "65535", "PrefixList", "pl-id/pl-name"}, {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, }, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"vpc-1"}, {"sg-1"}, {"Ingress"}, {"tcp"}, {"22"}, {"22"}, {"SecurityGroup"}, {"sg-10"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{""}, {""}, {""}, {"sg-2"}, {"Ingress"}, {"tcp"}, {"443"}, {"443"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{"i-2"}, {"server-2"}, {"vpc-1"}, {"sg-3"}, {"Ingress"}, {"icmp"}, {"-1"}, {"-1"}, {"SecurityGroup"}, {"sg-11"}}, + {{""}, {""}, {""}, {""}, {"Ingress"}, {"tcp"}, {"3389"}, {"3389"}, {"Ipv4"}, {"10.1.0.0/16"}}, + {{""}, {""}, {""}, {""}, {"Ingress"}, {"tcp"}, {"0"}, {"65535"}, {"PrefixList"}, {"pl-id/pl-name"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + }, columnWidths: []int{10, 12, 5, 15, 13, 10, 8, 6, 13, 13}, + lineHeights: []int{1, 1, 1, 1, 1, 1, 1, 1}, }, want: `+------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ | InstanceID | InstanceName | VPCID | SecurityGroupID | FlowDirection | IPProtocol | FromPort | ToPort | AddressType | CidrBlock | @@ -501,7 +528,7 @@ func TestTable_Out(t *testing.T) { { name: "markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, header: []string{"InstanceID", "InstanceName", "AttachedLB", "AttachedTG"}, data: [][]string{ {"i-1", "server-1", "lb-1", "tg-1"}, @@ -511,7 +538,16 @@ func TestTable_Out(t *testing.T) { {"i-5", "server-5", "lb-5", "\\-"}, {"i-6", "server-6", "\\-", "tg-5
tg-6
tg-7
tg-8"}, }, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2
lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3
tg-4"}}, + {{"i-4"}, {"server-4"}, {"\\-"}, {"\\-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"\\-"}}, + {{"i-6"}, {"server-6"}, {"\\-"}, {"tg-5
tg-6
tg-7
tg-8"}}, + }, columnWidths: []int{10, 12, 12, 28}, + lineHeights: []int{1, 1, 1, 1, 1, 1}, }, want: `| InstanceID | InstanceName | AttachedLB | AttachedTG | |------------|--------------|--------------|------------------------------| @@ -526,7 +562,7 @@ func TestTable_Out(t *testing.T) { { name: "backlog", fields: fields{ - format: FormatBacklog, + format: BacklogFormat, header: []string{"InstanceID", "InstanceName", "AttachedLB", "AttachedTG"}, data: [][]string{ {"i-1", "server-1", "lb-1", "tg-1"}, @@ -536,7 +572,16 @@ func TestTable_Out(t *testing.T) { {"i-5", "server-5", "lb-5", "-"}, {"i-6", "server-6", "-", "tg-5&br;tg-6&br;tg-7&br;tg-8"}, }, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2&br;lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3&br;tg-4"}}, + {{"i-4"}, {"server-4"}, {"-"}, {"-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"-"}}, + {{"i-6"}, {"server-6"}, {"-"}, {"tg-5&br;tg-6&br;tg-7&br;tg-8"}}, + }, columnWidths: []int{10, 12, 12, 28}, + lineHeights: []int{1, 1, 1, 1, 1, 1}, }, want: `| InstanceID | InstanceName | AttachedLB | AttachedTG |h | i-1 | server-1 | lb-1 | tg-1 | @@ -555,12 +600,17 @@ func TestTable_Out(t *testing.T) { tr.format = tt.fields.format tr.header = tt.fields.header tr.data = tt.fields.data + tr.splitedData = tt.fields.splitedData tr.columnWidths = tt.fields.columnWidths + tr.lineHeights = tt.fields.lineHeights tr.setBorder() - tr.Out() + tr.Render() if !reflect.DeepEqual(buf.String(), tt.want) { t.Errorf("\ngot:\n%v\nwant:\n%v\n", buf.String(), tt.want) } + if diff := cmp.Diff(buf.String(), tt.want); diff != "" { + t.Errorf(diff) + } }) } } @@ -581,7 +631,7 @@ func TestTable_printHeader(t *testing.T) { name: "text", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatText, + format: TextFormat, marginWidth: 1, columnWidths: []int{1, 2, 3}, }, @@ -591,7 +641,7 @@ func TestTable_printHeader(t *testing.T) { name: "markdown", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatMarkdown, + format: MarkdownFormat, marginWidth: 1, columnWidths: []int{1, 2, 3}, }, @@ -601,7 +651,7 @@ func TestTable_printHeader(t *testing.T) { name: "backlog", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatBacklog, + format: BacklogFormat, marginWidth: 1, columnWidths: []int{1, 2, 3}, }, @@ -611,7 +661,7 @@ func TestTable_printHeader(t *testing.T) { name: "margin", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatText, + format: TextFormat, marginWidth: 3, columnWidths: []int{1, 2, 3}, }, @@ -621,7 +671,7 @@ func TestTable_printHeader(t *testing.T) { name: "long", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatText, + format: TextFormat, marginWidth: 1, columnWidths: []int{10, 2, 3}, }, @@ -631,7 +681,7 @@ func TestTable_printHeader(t *testing.T) { name: "short", fields: fields{ header: []string{"a", "bb", "ccc"}, - format: FormatText, + format: TextFormat, marginWidth: 1, columnWidths: []int{1, 2, 1}, }, @@ -657,8 +707,10 @@ func TestTable_printHeader(t *testing.T) { func TestTable_printData(t *testing.T) { type fields struct { data [][]string + splitedData [][][]string format Format columnWidths []int + lineHeights []int } tests := []struct { name string @@ -676,8 +728,17 @@ func TestTable_printData(t *testing.T) { {"i-5", "server-5", "lb-5", "-"}, {"i-6", "server-6", "-", "tg-5\ntg-6\ntg-7\ntg-8"}, }, - format: FormatText, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2", "lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3", "tg-4"}}, + {{"i-4"}, {"server-4"}, {"-"}, {"-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"-"}}, + {{"i-6"}, {"server-6"}, {"-"}, {"tg-5", "tg-6", "tg-7", "tg-8"}}, + }, + format: TextFormat, columnWidths: []int{10, 12, 10, 10}, + lineHeights: []int{1, 2, 2, 1, 1, 4}, }, want: `| i-1 | server-1 | lb-1 | tg-1 | +------------+--------------+------------+------------+ @@ -695,6 +756,44 @@ func TestTable_printData(t *testing.T) { | | | | tg-6 | | | | | tg-7 | | | | | tg-8 | +`, + }, + { + name: "text_with_compress", + fields: fields{ + data: [][]string{ + {"i-1", "server-1", "vpc-1", "sg-1", "Ingress", "tcp", "22", "22", "SecurityGroup", "sg-10"}, + {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, + {"", "", "", "sg-2", "Ingress", "tcp", "443", "443", "Ipv4", "0.0.0.0/0"}, + {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, + {"i-2", "server-2", "vpc-1", "sg-3", "Ingress", "icmp", "-1", "-1", "SecurityGroup", "sg-11"}, + {"", "", "", "", "Ingress", "tcp", "3389", "3389", "Ipv4", "10.1.0.0/16"}, + {"", "", "", "", "Ingress", "tcp", "0", "65535", "PrefixList", "pl-id/pl-name"}, + {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, + }, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"vpc-1"}, {"sg-1"}, {"Ingress"}, {"tcp"}, {"22"}, {"22"}, {"SecurityGroup"}, {"sg-10"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{""}, {""}, {""}, {"sg-2"}, {"Ingress"}, {"tcp"}, {"443"}, {"443"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + {{"i-2"}, {"server-2"}, {"vpc-1"}, {"sg-3"}, {"Ingress"}, {"icmp"}, {"-1"}, {"-1"}, {"SecurityGroup"}, {"sg-11"}}, + {{""}, {""}, {""}, {""}, {"Ingress"}, {"tcp"}, {"3389"}, {"3389"}, {"Ipv4"}, {"10.1.0.0/16"}}, + {{""}, {""}, {""}, {""}, {"Ingress"}, {"tcp"}, {"0"}, {"65535"}, {"PrefixList"}, {"pl-id/pl-name"}}, + {{""}, {""}, {""}, {""}, {"Egress"}, {"-1"}, {"0"}, {"0"}, {"Ipv4"}, {"0.0.0.0/0"}}, + }, + format: CompressedTextFormat, + columnWidths: []int{10, 12, 5, 15, 13, 10, 8, 6, 13, 13}, + lineHeights: []int{1, 1, 1, 1, 1, 1, 1, 1}, + }, + want: `| i-1 | server-1 | vpc-1 | sg-1 | Ingress | tcp | 22 | 22 | SecurityGroup | sg-10 | +| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | +| | | | sg-2 | Ingress | tcp | 443 | 443 | Ipv4 | 0.0.0.0/0 | +| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | ++------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ +| i-2 | server-2 | vpc-1 | sg-3 | Ingress | icmp | -1 | -1 | SecurityGroup | sg-11 | +| | | | | Ingress | tcp | 3389 | 3389 | Ipv4 | 10.1.0.0/16 | +| | | | | Ingress | tcp | 0 | 65535 | PrefixList | pl-id/pl-name | +| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | `, }, { @@ -708,8 +807,17 @@ func TestTable_printData(t *testing.T) { {"i-5", "server-5", "lb-5", "\\-"}, {"i-6", "server-6", "\\-", "tg-5
tg-6
tg-7
tg-8"}, }, - format: FormatMarkdown, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2
lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3
tg-4"}}, + {{"i-4"}, {"server-4"}, {"\\-"}, {"\\-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"\\-"}}, + {{"i-6"}, {"server-6"}, {"\\-"}, {"tg-5
tg-6
tg-7
tg-8"}}, + }, + format: MarkdownFormat, columnWidths: []int{10, 12, 12, 28}, + lineHeights: []int{1, 1, 1, 1, 1, 1}, }, want: `| i-1 | server-1 | lb-1 | tg-1 | | i-2 | server-2 | lb-2
lb-3 | tg-2 | @@ -730,8 +838,17 @@ func TestTable_printData(t *testing.T) { {"i-5", "server-5", "lb-5", "-"}, {"i-6", "server-6", "-", "tg-5&br;tg-6&br;tg-7&br;tg-8"}, }, - format: FormatBacklog, + splitedData: [][][]string{ + {{"i-1"}, {"server-1"}, {"lb-1"}, {"tg-1"}}, + {{"i-2"}, {"server-2"}, {"lb-2&br;lb-3"}, {"tg-2"}}, + {{"i-3"}, {"server-3"}, {"lb-4"}, {"tg-3&br;tg-4"}}, + {{"i-4"}, {"server-4"}, {"-"}, {"-"}}, + {{"i-5"}, {"server-5"}, {"lb-5"}, {"-"}}, + {{"i-6"}, {"server-6"}, {"-"}, {"tg-5&br;tg-6&br;tg-7&br;tg-8"}}, + }, + format: BacklogFormat, columnWidths: []int{10, 12, 12, 28}, + lineHeights: []int{1, 1, 1, 1, 1, 1}, }, want: `| i-1 | server-1 | lb-1 | tg-1 | | i-2 | server-2 | lb-2&br;lb-3 | tg-2 | @@ -739,33 +856,6 @@ func TestTable_printData(t *testing.T) { | i-4 | server-4 | - | - | | i-5 | server-5 | lb-5 | - | | i-6 | server-6 | - | tg-5&br;tg-6&br;tg-7&br;tg-8 | -`, - }, - { - name: "text_with_compress", - fields: fields{ - data: [][]string{ - {"i-1", "server-1", "vpc-1", "sg-1", "Ingress", "tcp", "22", "22", "SecurityGroup", "sg-10"}, - {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, - {"", "", "", "sg-2", "Ingress", "tcp", "443", "443", "Ipv4", "0.0.0.0/0"}, - {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, - {"i-2", "server-2", "vpc-1", "sg-3", "Ingress", "icmp", "-1", "-1", "SecurityGroup", "sg-11"}, - {"", "", "", "", "Ingress", "tcp", "3389", "3389", "Ipv4", "10.1.0.0/16"}, - {"", "", "", "", "Ingress", "tcp", "0", "65535", "PrefixList", "pl-id/pl-name"}, - {"", "", "", "", "Egress", "-1", "0", "0", "Ipv4", "0.0.0.0/0"}, - }, - format: FormatCompressedText, - columnWidths: []int{10, 12, 5, 15, 13, 10, 8, 6, 13, 13}, - }, - want: `| i-1 | server-1 | vpc-1 | sg-1 | Ingress | tcp | 22 | 22 | SecurityGroup | sg-10 | -| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | -| | | | sg-2 | Ingress | tcp | 443 | 443 | Ipv4 | 0.0.0.0/0 | -| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | -+------------+--------------+-------+-----------------+---------------+------------+----------+--------+---------------+---------------+ -| i-2 | server-2 | vpc-1 | sg-3 | Ingress | icmp | -1 | -1 | SecurityGroup | sg-11 | -| | | | | Ingress | tcp | 3389 | 3389 | Ipv4 | 10.1.0.0/16 | -| | | | | Ingress | tcp | 0 | 65535 | PrefixList | pl-id/pl-name | -| | | | | Egress | -1 | 0 | 0 | Ipv4 | 0.0.0.0/0 | `, }, } @@ -774,8 +864,10 @@ func TestTable_printData(t *testing.T) { buf := new(bytes.Buffer) tr := New(buf) tr.data = tt.fields.data + tr.splitedData = tt.fields.splitedData tr.format = tt.fields.format tr.columnWidths = tt.fields.columnWidths + tr.lineHeights = tt.fields.lineHeights tr.setBorder() tr.printData() if !reflect.DeepEqual(buf.String(), tt.want) { @@ -917,7 +1009,7 @@ func TestTable_printBorder(t *testing.T) { { name: "text", fields: fields{ - format: FormatText, + format: TextFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -926,7 +1018,7 @@ func TestTable_printBorder(t *testing.T) { { name: "markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -935,7 +1027,7 @@ func TestTable_printBorder(t *testing.T) { { name: "backlog", fields: fields{ - format: FormatBacklog, + format: BacklogFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -944,7 +1036,7 @@ func TestTable_printBorder(t *testing.T) { { name: "wide-margin", fields: fields{ - format: FormatText, + format: TextFormat, marginWidth: 3, columnWidths: []int{8, 12, 5}, }, @@ -983,17 +1075,17 @@ func TestTable_setAttr(t *testing.T) { { name: "text", fields: fields{ - format: FormatText, + format: TextFormat, }, want: want{ - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, }, }, { name: "markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, }, want: want{ emptyFieldPlaceholder: MarkdownDefaultEmptyFieldPlaceholder, @@ -1003,7 +1095,7 @@ func TestTable_setAttr(t *testing.T) { { name: "backlog", fields: fields{ - format: FormatBacklog, + format: BacklogFormat, }, want: want{ emptyFieldPlaceholder: BacklogDefaultEmptyFieldPlaceholder, @@ -1115,8 +1207,8 @@ func TestTable_setData(t *testing.T) { name: "text", fields: fields{ header: []string{"InstanceID", "InstanceName", "AttachedLB", "AttachedTG"}, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, mergedFields: nil, ignoredFields: nil, columnWidths: []int{0, 0, 0, 0}, @@ -1184,8 +1276,8 @@ func TestTable_setData(t *testing.T) { name: "merge", fields: fields{ header: []string{"InstanceID", "InstanceName", "SecurityGroupID", "FlowDirection", "IPProtocol", "FromPort", "ToPort", "AddressType", "CidrBlock"}, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, mergedFields: []int{0, 1, 2}, ignoredFields: nil, columnWidths: []int{0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -1209,8 +1301,8 @@ func TestTable_setData(t *testing.T) { name: "included_ptr", fields: fields{ header: []string{"InstanceID", "InstanceName", "AttachedLB", "AttachedTG"}, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, mergedFields: nil, ignoredFields: nil, columnWidths: []int{0, 0, 0, 0}, @@ -1288,7 +1380,7 @@ func TestTable_setBorder(t *testing.T) { { name: "text", fields: fields{ - format: FormatText, + format: TextFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -1297,7 +1389,7 @@ func TestTable_setBorder(t *testing.T) { { name: "markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -1306,7 +1398,7 @@ func TestTable_setBorder(t *testing.T) { { name: "backlog", fields: fields{ - format: FormatBacklog, + format: BacklogFormat, marginWidth: 1, columnWidths: []int{8, 12, 5}, }, @@ -1315,7 +1407,7 @@ func TestTable_setBorder(t *testing.T) { { name: "wide-margin", fields: fields{ - format: FormatText, + format: TextFormat, marginWidth: 3, columnWidths: []int{8, 12, 5}, }, @@ -1360,9 +1452,9 @@ func TestTable_formatField(t *testing.T) { { name: "string", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1374,23 +1466,23 @@ func TestTable_formatField(t *testing.T) { { name: "string_empty", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: "", }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "byte_slice", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1402,9 +1494,9 @@ func TestTable_formatField(t *testing.T) { { name: "escape", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: true, }, args: args{ @@ -1416,7 +1508,7 @@ func TestTable_formatField(t *testing.T) { { name: "asterisk_prefix_at_markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, emptyFieldPlaceholder: MarkdownDefaultEmptyFieldPlaceholder, wordDelimiter: MarkdownDefaultWordDelimiter, hasEscape: false, @@ -1430,9 +1522,9 @@ func TestTable_formatField(t *testing.T) { { name: "int", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1444,9 +1536,9 @@ func TestTable_formatField(t *testing.T) { { name: "int_signed", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1458,9 +1550,9 @@ func TestTable_formatField(t *testing.T) { { name: "uint", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1472,9 +1564,9 @@ func TestTable_formatField(t *testing.T) { { name: "float", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1486,9 +1578,9 @@ func TestTable_formatField(t *testing.T) { { name: "ptr", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1500,37 +1592,37 @@ func TestTable_formatField(t *testing.T) { { name: "nil_ptr", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: (*string)(nil), }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "non_nil_ptr_string", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: new(string), }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "non_nil_ptr_int", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1542,93 +1634,121 @@ func TestTable_formatField(t *testing.T) { { name: "slice_string", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []string{"a", "b"}, }, - want: "a" + DefaultWordDelimiter + "b", + want: "a" + TextDefaultWordDelimiter + "b", wantErr: false, }, { name: "slice_string_included_empty", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []string{"a", "", "b"}, }, - want: "a" + DefaultWordDelimiter + DefaultEmptyFieldPlaceholder + DefaultWordDelimiter + "b", + want: "a" + TextDefaultWordDelimiter + TextDefaultEmptyFieldPlaceholder + TextDefaultWordDelimiter + "b", wantErr: false, }, { name: "slice_int", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []int{0, 1, 2}, }, - want: "0" + DefaultWordDelimiter + "1" + DefaultWordDelimiter + "2", + want: "0" + TextDefaultWordDelimiter + "1" + TextDefaultWordDelimiter + "2", wantErr: false, }, { name: "slice_uint", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []uint{0, 1, 2}, }, - want: "0" + DefaultWordDelimiter + "1" + DefaultWordDelimiter + "2", + want: "0" + TextDefaultWordDelimiter + "1" + TextDefaultWordDelimiter + "2", + wantErr: false, + }, + { + name: "slice_float32", + fields: fields{ + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, + hasEscape: false, + }, + args: args{ + v: []float32{0.1, 1.25, 2.001}, + }, + want: "0.1" + TextDefaultWordDelimiter + "1.25" + TextDefaultWordDelimiter + "2.001", + wantErr: false, + }, + { + name: "slice_float64", + fields: fields{ + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, + hasEscape: false, + }, + args: args{ + v: []float64{0.1, 1.25, 2.001}, + }, + want: "0.1" + TextDefaultWordDelimiter + "1.25" + TextDefaultWordDelimiter + "2.001", wantErr: false, }, { name: "slice_nil", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: ([]string)(nil), }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "slice_empty", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []string{}, }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "slice_with_byte_slice", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1640,9 +1760,9 @@ func TestTable_formatField(t *testing.T) { { name: "slice_slice", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1657,9 +1777,9 @@ func TestTable_formatField(t *testing.T) { { name: "slice_struct", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1677,79 +1797,79 @@ func TestTable_formatField(t *testing.T) { { name: "slice_ptr", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: &[]string{"a", "b"}, }, - want: "a" + DefaultWordDelimiter + "b", + want: "a" + TextDefaultWordDelimiter + "b", wantErr: false, }, { name: "slice_with_ptr_to_strings", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []*string{sp(""), sp("a"), sp("b")}, }, - want: DefaultEmptyFieldPlaceholder + DefaultWordDelimiter + "a" + DefaultWordDelimiter + "b", + want: TextDefaultEmptyFieldPlaceholder + TextDefaultWordDelimiter + "a" + TextDefaultWordDelimiter + "b", wantErr: false, }, { name: "slice_with_ptr_to_string_empty", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []*string{}, }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "slice_with_nil_ptr", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []*int{nil}, }, - want: DefaultEmptyFieldPlaceholder, + want: TextDefaultEmptyFieldPlaceholder, wantErr: false, }, { name: "slice_with_ptr_mixed", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ v: []*string{nil, sp(""), sp("aaa")}, }, - want: DefaultEmptyFieldPlaceholder + DefaultWordDelimiter + DefaultEmptyFieldPlaceholder + DefaultWordDelimiter + "aaa", + want: TextDefaultEmptyFieldPlaceholder + TextDefaultWordDelimiter + TextDefaultEmptyFieldPlaceholder + TextDefaultWordDelimiter + "aaa", wantErr: false, }, { name: "stringer_duration", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1761,9 +1881,9 @@ func TestTable_formatField(t *testing.T) { { name: "stringer_ipaddress", fields: fields{ - format: FormatText, - emptyFieldPlaceholder: DefaultEmptyFieldPlaceholder, - wordDelimiter: DefaultWordDelimiter, + format: TextFormat, + emptyFieldPlaceholder: TextDefaultEmptyFieldPlaceholder, + wordDelimiter: TextDefaultWordDelimiter, hasEscape: false, }, args: args{ @@ -1811,7 +1931,7 @@ func TestTable_replaceNL(t *testing.T) { { name: "text", fields: fields{ - format: FormatText, + format: TextFormat, wordDelimiter: "\n", }, args: args{ @@ -1822,7 +1942,7 @@ func TestTable_replaceNL(t *testing.T) { { name: "markdown", fields: fields{ - format: FormatMarkdown, + format: MarkdownFormat, wordDelimiter: "
", }, args: args{ @@ -1833,7 +1953,7 @@ func TestTable_replaceNL(t *testing.T) { { name: "backlog", fields: fields{ - format: FormatBacklog, + format: BacklogFormat, wordDelimiter: "&br;", }, args: args{ @@ -1848,7 +1968,8 @@ func TestTable_replaceNL(t *testing.T) { format: tt.fields.format, wordDelimiter: tt.fields.wordDelimiter, } - got := tr.replaceNL(tt.args.s) + tr.setAttr() + got := tr.sanitize(tt.args.s) if !reflect.DeepEqual(got, tt.want) { t.Errorf("\ngot:\n%v\nwant:\n%v\n", got, tt.want) }