1
0
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:
Jakub Sobon 2019-03-12 00:30:44 -04:00
parent d47c6cf0ff
commit ad40f757f6
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
7 changed files with 286 additions and 29 deletions

View File

@ -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()
}

View File

@ -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
}

View File

@ -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:

View File

@ -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
}

View File

@ -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)
}
})
}
}

View 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
}

View File

@ -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
})