mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
Drafting validation for table.
This commit is contained in:
parent
d47c6cf0ff
commit
ad40f757f6
@ -18,7 +18,7 @@ package table
|
||||
// content.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
@ -34,16 +34,29 @@ type ContentOption interface {
|
||||
|
||||
// contentOptions stores options that apply to the content level.
|
||||
type contentOptions struct {
|
||||
border linestyle.LineStyle
|
||||
borderCellOpts []cell.Option
|
||||
columnWidthsPercent []int
|
||||
|
||||
border linestyle.LineStyle
|
||||
borderCellOpts []cell.Option
|
||||
columnWidthsPercent []int
|
||||
horizontalCellSpacing int
|
||||
verticalCellSpacing int
|
||||
|
||||
// hierarchical are the specified hierarchical options at the content
|
||||
// level.
|
||||
hierarchical *hierarchicalOptions
|
||||
}
|
||||
|
||||
// newContentOptions returns a new contentOptions instance with the options
|
||||
// applied.
|
||||
func newContentOptions(opts ...ContentOption) *contentOptions {
|
||||
co := &contentOptions{
|
||||
hierarchical: &hierarchicalOptions{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.set(co)
|
||||
}
|
||||
return co
|
||||
}
|
||||
|
||||
// hierarchicalOptions stores options that can be applied at multiple levels or
|
||||
// hierarchy, i.e. the Content (top level), the Row or the Cell.
|
||||
type hierarchicalOptions struct {
|
||||
@ -203,10 +216,14 @@ type Columns int
|
||||
//
|
||||
// This object is thread-safe.
|
||||
type Content struct {
|
||||
colNum Columns
|
||||
// cols is the number of columns in the content.
|
||||
cols Columns
|
||||
// header is the header row, or nil if one wasn't provided.
|
||||
header *Row
|
||||
rows []*Row
|
||||
// rows are the rows in the table.
|
||||
rows []*Row
|
||||
|
||||
// opts are the options provided to NewContent.
|
||||
opts *contentOptions
|
||||
}
|
||||
|
||||
@ -216,16 +233,50 @@ type Content struct {
|
||||
// All rows must contain the same number of columns (the same number of cells)
|
||||
// allowing for the CellColSpan option.
|
||||
func NewContent(cols Columns, rows []*Row, opts ...ContentOption) (*Content, error) {
|
||||
return nil, errors.New("unimplemented")
|
||||
c := &Content{
|
||||
cols: cols,
|
||||
opts: newContentOptions(opts...),
|
||||
}
|
||||
for _, r := range rows {
|
||||
if err := c.addRow(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// validate validates the content.
|
||||
func (c *Content) validate() error {
|
||||
return validateContent(c)
|
||||
}
|
||||
|
||||
// AddRow adds a row to the content.
|
||||
// If you need to apply options at the Row level, use AddRowWithOpts.
|
||||
func (c *Content) AddRow(cells ...*Cell) error {
|
||||
return errors.New("unimplemented")
|
||||
return c.AddRowWithOpts(cells)
|
||||
}
|
||||
|
||||
// addRow adds the row to the content.
|
||||
func (c *Content) addRow(r *Row) error {
|
||||
if r.isHeader {
|
||||
if c.header != nil {
|
||||
return fmt.Errorf("the content can only have one header row, already have: %v", c.header)
|
||||
}
|
||||
c.header = r
|
||||
} else {
|
||||
c.rows = append(c.rows, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRowWithOpts adds a row to the content and applies the options.
|
||||
func (c *Content) AddRowWithOpts(cells []*Cell, opts ...RowOption) error {
|
||||
return errors.New("unimplemented")
|
||||
if err := c.addRow(NewRowWithOpts(cells, opts...)); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.validate()
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ package table
|
||||
// content_cell.go defines a type that represents a single cell in the table.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/wrap"
|
||||
@ -133,13 +136,26 @@ func CellWrapContent() CellOption {
|
||||
|
||||
// Cell is one cell in a Row.
|
||||
type Cell struct {
|
||||
// data are the text data in the cell.
|
||||
data []*Data
|
||||
|
||||
colSpan int
|
||||
rowSpan int
|
||||
// colSpan specified how many columns does this cell span.
|
||||
colSpan int
|
||||
// rowSpan specified how many rows does this cell span.
|
||||
rowSpan int
|
||||
// hierarchical are the specified hierarchical options at the cell level.
|
||||
hierarchical *hierarchicalOptions
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (c *Cell) String() string {
|
||||
var b bytes.Buffer
|
||||
for _, d := range c.data {
|
||||
b.WriteString(d.String())
|
||||
}
|
||||
return fmt.Sprintf("| %v ", b.String())
|
||||
}
|
||||
|
||||
// NewCell returns a new Cell with the provided text.
|
||||
// If you need to apply options at the Cell or Data level use NewCellWithOpts.
|
||||
// The text contain cannot control characters (unicode.IsControl) or space
|
||||
@ -148,10 +164,19 @@ type Cell struct {
|
||||
// Any newline ('\n') characters are interpreted as newlines when displaying
|
||||
// the text.
|
||||
func NewCell(text string) *Cell {
|
||||
return nil
|
||||
return NewCellWithOpts([]*Data{NewData(text)})
|
||||
}
|
||||
|
||||
// NewCellWithOpts returns a new Cell with the provided data and options.
|
||||
func NewCellWithOpts(data []*Data, opts ...CellOption) *Cell {
|
||||
return nil
|
||||
c := &Cell{
|
||||
data: data,
|
||||
colSpan: 1,
|
||||
rowSpan: 1,
|
||||
hierarchical: &hierarchicalOptions{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.set(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ package table
|
||||
// content_data.go defines a type that represents data within a table cell.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/canvas/buffer"
|
||||
)
|
||||
@ -49,9 +51,19 @@ func DataCellOpts(cellOpts ...cell.Option) DataOption {
|
||||
|
||||
// Data is part of (or the full) the data that is displayed inside one Cell.
|
||||
type Data struct {
|
||||
// cells contain the text and its cell options.
|
||||
cells []*buffer.Cell
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (d *Data) String() string {
|
||||
var b bytes.Buffer
|
||||
for _, c := range d.cells {
|
||||
b.WriteRune(c.Rune)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// NewData creates new Data with the provided text and applies the options.
|
||||
// The text contain cannot control characters (unicode.IsControl) or space
|
||||
// character (unicode.IsSpace) other than:
|
||||
|
@ -17,6 +17,9 @@ package table
|
||||
// content_row.go defines a type that represents a single row in the table.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/terminal/terminalapi"
|
||||
@ -56,14 +59,6 @@ func (ro rowOption) set(r *Row) {
|
||||
ro(r)
|
||||
}
|
||||
|
||||
// RowHighlighted sets the row as highlighted, the user can then change which
|
||||
// row is highlighted using keyboard or mouse input.
|
||||
func RowHighlighted() RowOption {
|
||||
return rowOption(func(r *Row) {
|
||||
r.highlighted = true
|
||||
})
|
||||
}
|
||||
|
||||
// RowCallback allows this row to be activated and provides a function that
|
||||
// should be called upon each row activation by the user.
|
||||
func RowCallback(fn RowCallbackFn) RowOption {
|
||||
@ -139,20 +134,39 @@ func RowAlignVertical(v align.Vertical) RowOption {
|
||||
|
||||
// Row is one row in the table.
|
||||
type Row struct {
|
||||
// cells are the cells in this row.
|
||||
cells []*Cell
|
||||
|
||||
rowCallback RowCallbackFn
|
||||
highlighted bool
|
||||
// isHeader asserts if this row is the header of the table.
|
||||
isHeader bool
|
||||
|
||||
// rowCallback is the function to call when this row is activated.
|
||||
// Can be nil if the row cannot be activated and is always nil on the
|
||||
// header row.
|
||||
rowCallback RowCallbackFn
|
||||
// hierarchical are the specified hierarchical options at the row level.
|
||||
hierarchical *hierarchicalOptions
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (r *Row) String() string {
|
||||
if len(r.cells) == 0 {
|
||||
return "Row{}"
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for _, c := range r.cells {
|
||||
b.WriteString(c.String())
|
||||
}
|
||||
return fmt.Sprintf("Row{%v|}", b.String())
|
||||
}
|
||||
|
||||
// NewHeader returns a new Row that will be the header of the table.
|
||||
// The header remains visible while scrolling and allows for sorting of content
|
||||
// based on its values. Header row cannot be highlighted.
|
||||
// Content can only have one header Row.
|
||||
// If you need to apply options at the Row level, use NewHeaderWithOpts.
|
||||
func NewHeader(cells ...*Cell) *Row {
|
||||
return nil
|
||||
return NewHeaderWithOpts(cells)
|
||||
}
|
||||
|
||||
// NewHeaderWithOpts returns a new Row that will be the header of the table and
|
||||
@ -161,18 +175,37 @@ func NewHeader(cells ...*Cell) *Row {
|
||||
// based on its values. Header row cannot be highlighted.
|
||||
// Content can only have one header Row.
|
||||
func NewHeaderWithOpts(cells []*Cell, opts ...RowOption) *Row {
|
||||
return nil
|
||||
r := NewRowWithOpts(cells, opts...)
|
||||
r.isHeader = true
|
||||
return r
|
||||
}
|
||||
|
||||
// NewRow returns a new Row instance with the provided cells.
|
||||
// If you need to apply options at the Row level, use NewRowWithOpts.
|
||||
// If you need to add a table header Row, use NewHeader.
|
||||
func NewRow(cells ...*Cell) *Row {
|
||||
return nil
|
||||
return NewRowWithOpts(cells)
|
||||
}
|
||||
|
||||
// NewRowWithOpts returns a new Row instance with the provided cells and applies
|
||||
// the row options.
|
||||
func NewRowWithOpts(cells []*Cell, opts ...RowOption) *Row {
|
||||
return nil
|
||||
r := &Row{
|
||||
cells: cells,
|
||||
hierarchical: &hierarchicalOptions{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.set(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// effectiveColumns returns the number of columns this row effectively occupies.
|
||||
// This accounts for cells that specify colSpan > 1.
|
||||
func (r *Row) effectiveColumns() int {
|
||||
var cols int
|
||||
for _, c := range r.cells {
|
||||
cols += c.colSpan
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
@ -14,6 +14,11 @@
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ExampleContent() {
|
||||
rows := []*Row{
|
||||
NewHeader(
|
||||
@ -31,3 +36,101 @@ func ExampleContent() {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
columns Columns
|
||||
rows []*Row
|
||||
opts []ContentOption
|
||||
wantSubstr string
|
||||
}{
|
||||
{
|
||||
desc: "fails when number of columns is negative",
|
||||
columns: Columns(-1),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("0"),
|
||||
),
|
||||
},
|
||||
wantSubstr: "invalid number of columns",
|
||||
},
|
||||
{
|
||||
desc: "fails when rows doesn't have the specified number of columns",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("0"),
|
||||
),
|
||||
},
|
||||
wantSubstr: "all rows must occupy",
|
||||
},
|
||||
{
|
||||
desc: "succeeds when row columns match content",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCell("0"),
|
||||
NewCell("1"),
|
||||
),
|
||||
},
|
||||
},
|
||||
// fails on colspan <1
|
||||
// fails on rowspan <1
|
||||
{
|
||||
desc: "succeeds when row has a column with a colSpan",
|
||||
columns: Columns(2),
|
||||
rows: []*Row{
|
||||
NewRow(
|
||||
NewCellWithOpts(
|
||||
[]*Data{NewData("0")},
|
||||
CellColSpan(2),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
// Content:
|
||||
// rows with zero columns
|
||||
// rows with varying number of columns
|
||||
// rows with columns that have valid colspan
|
||||
// rows with columns that have valid rowspan
|
||||
// rows with columns that have valid colspan and rowspan
|
||||
// too many column widths
|
||||
// not enough column widths
|
||||
// zero column width
|
||||
// negative column width
|
||||
// zero row height
|
||||
// negative row height
|
||||
// zero and negative padding
|
||||
// zero and negative spacing
|
||||
|
||||
// Row:
|
||||
// too many header rows
|
||||
// nil row callback
|
||||
// zero and negative row height
|
||||
// zero and negative padding
|
||||
|
||||
// cell:
|
||||
// zero and negative colspan
|
||||
// zero and negative rowspan
|
||||
// zero and negative cell height
|
||||
// zero and negative padding
|
||||
|
||||
// data:
|
||||
// invalid space characters in data
|
||||
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
_, err := NewContent(tc.columns, tc.rows, tc.opts...)
|
||||
if (err != nil) != (tc.wantSubstr != "") {
|
||||
t.Errorf("NewContent => unexpected error: %v, wantSubstr: %q", err, tc.wantSubstr)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), tc.wantSubstr) {
|
||||
t.Errorf("NewContent => unexpected error: %v, wantSubstr: %q", err, tc.wantSubstr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
33
widgets/table/content_validate.go
Normal file
33
widgets/table/content_validate.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package table
|
||||
|
||||
// contant_validate.go contains code that validates the user provided Content.
|
||||
|
||||
import "fmt"
|
||||
|
||||
// validateContent validates the content instance.
|
||||
func validateContent(c *Content) error {
|
||||
if min := 1; int(c.cols) < min {
|
||||
return fmt.Errorf("invalid number of columns %d, must be a value in range %d <= v", c.cols, min)
|
||||
}
|
||||
|
||||
for _, r := range c.rows {
|
||||
if got, want := r.effectiveColumns(), int(c.cols); got != want {
|
||||
return fmt.Errorf("content has %d columns, but row %v has %d, all rows must occupy the same amount of columns", want, r, got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -119,7 +119,7 @@ func HighlightDelay(d time.Duration) Option {
|
||||
// HighlightCellOpts sets the cell options on cells that are part of a
|
||||
// highlighted row.
|
||||
// Defaults to DefaultHighlightColor.
|
||||
func HighlightColor(cellOpts ...cell.Option) Option {
|
||||
func HighlightCellOpts(cellOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.highlightCellOpts = cellOpts
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user