I have the current setup to make it easy for a developer to output structured data in many formats. It currently uses a middleware schema that can transform a full Table.
❯ go run ./cmd/cli rum ls-actions --action filters-search --from 2022/11/10 --fields name,query.q,query.,pagination.total_hits --filter query.category --count 50 --table-format markdown
| name | query.q | query.hardiness_zone | query.is_on_sale | query.pp | pagination.total_hits |
| -------------- | ------------------ | -------------------- | ---------------- | -------- | --------------------- |
| filters-search | | | | 2 | 688 |
| filters-search | Japanese red maple | | | 3 | 67 |
| filters-search | Japanese red maple | | | 4 | 67 |
| filters-search | Japanese red maple | | | 5 | 67 |
| filters-search | Japanese red maple | | | 4 | 67 |
| filters-search | Japanese red maple | | | 3 | 67 |
| filters-search | Japanese red maple | | | 2 | 67 |
| filters-search | | | On sale | 2 | 72 |
| filters-search | | 5 | | 26 | 432 |
| filters-search | | 5 | | 25 | 432 |
| filters-search | | 5 | | 24 | 432 |
| filters-search | | 5 | | 23 | 432 |
| filters-search | | 5 | | 22 | 432 |
| filters-search | | 5 | | 21 | 432 |
| filters-search | | 5 | | 20 | 432 |
| filters-search | | 5 | | 19 | 432 |
| filters-search | | 5 | | 18 | 432 |
| filters-search | | 5 | | 17 | 432 |
| filters-search | | 5 | | 16 | 432 |
| filters-search | | 5 | | 15 | 432 |
| filters-search | | 5 | | 14 | 432 |
| filters-search | | 5 | | 13 | 432 |
| filters-search | | 5 | | 12 | 432 |
| filters-search | | 5 | | 13 | 432 |
| filters-search | | 5 | | 12 | 432 |
| filters-search | | 5 | | 11 | 432 |
| filters-search | | 5 | | 10 | 432 |
| filters-search | | 5 | | 9 | 432 |
| filters-search | | 5 | | 8 | 432 |
| filters-search | | 5 | | 7 | 432 |
| filters-search | | 5 | | 6 | 432 |
| filters-search | | 5 | | 5 | 432 |
| filters-search | | 5 | | 4 | 432 |
| filters-search | | 5 | | 3 | 432 |
| filters-search | | 5 | | 2 | 432 |
| filters-search | | | On sale | 4 | 72 |
| filters-search | | | On sale | 3 | 72 |
| filters-search | | | On sale | 2 | 72 |
| filters-search | burning bush | | | 2 | 30 |
| filters-search | | | On sale | 5 | 72 |
| filters-search | | | On sale | 4 | 72 |
| filters-search | | | On sale | 3 | 72 |
| filters-search | | | On sale | 2 | 72 |
| filters-search | | | On sale | 4 | 72 |
| filters-search | | | On sale | 5 | 72 |
| filters-search | | | On sale | 4 | 72 |
| filters-search | | | On sale | 3 | 72 |
| filters-search | | | On sale | 2 | 72 |
| filters-search | | | | 2 | 151 |
| filters-search | | | | 3 | 151 |
type TableName = string
type FieldName = string
type GenericCellValue = interface{}
type MapRow = map[FieldName]GenericCellValue
type Row interface {
GetFields() []FieldName
GetValues() MapRow
}
type TableMiddleware interface {
// Process transform a single row into potential multiple rows split across multiple tables
Process(table *Table) (*Table, error)
}
type Table struct {
Columns []FieldName
Rows []Row
}type TableOutputFormatter struct {
table *Table
middlewares []TableMiddleware
tableFormat string
}
func (tof *TableOutputFormatter) Output() (string, error) {
for _, middleware := range tof.middlewares {
newTable, err := middleware.Process(tof.table)
if err != nil {
return "", err
}
tof.table = newTable
}
...
} of := cli.NewTableOutputFormatter(tableFormat)
of.AddMiddleware(cli.NewFlattenObjectMiddleware())
of.AddMiddleware(cli.NewFieldsFilterMiddleware(fields, filters))
of.AddMiddleware(cli.NewSortColumnsMiddleware())
if len(fields) == 0 {
of.AddMiddleware(cli.NewReorderColumnOrderMiddleware([]cli.FieldName{"name"}))
} else {
of.AddMiddleware(cli.NewReorderColumnOrderMiddleware(fields))
}
flattenedActions := flattenActions(actions)
for _, action := range flattenedActions {
of.AddRow(&cli.SimpleRow{Hash: action})
}
s, err := of.Output()I would like to make it possible to also pass in middlewares that only transform a single row.
Should I make middleware a interface{} and do type dispatching?
Of course, I wonder if row processors should be called in front of table processors anyway. It's easy to transform a row processor into a table processor, so that if we need to "interleave" table processors with row processors, we can easily do that.
Row processors are useful for streaming data, where we want to transform rows as they come in, which means that while they can take a table, they only need to return a single row.
I guess I just answered my own question.