diff --git a/go.mod b/go.mod index 43a43f1..a17c97a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/continusec/objecthash v0.0.0-20250728010430-211b145fc905 github.com/continusec/verifiabledatastructures v0.0.0-20250728001347-b313577a08fe github.com/gorilla/mux v1.8.1 - github.com/olekukonko/tablewriter v1.0.8 + github.com/olekukonko/tablewriter v1.0.9 github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli v1.22.17 golang.org/x/crypto v0.40.0 diff --git a/go.sum b/go.sum index 3707f05..6d2736f 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5 github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= -github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ= -github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/vendor/github.com/continusec/verifiabledatastructures/storage/memory/storage_memory.go b/vendor/github.com/continusec/verifiabledatastructures/storage/memory/storage_memory.go deleted file mode 100644 index a13dc54..0000000 --- a/vendor/github.com/continusec/verifiabledatastructures/storage/memory/storage_memory.go +++ /dev/null @@ -1,104 +0,0 @@ -/* - -Copyright 2017 Continusec Pty Ltd - -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 memory - -import ( - "encoding/hex" - "sync" - - "golang.org/x/net/context" - - "github.com/continusec/verifiabledatastructures/verifiable" - "google.golang.org/protobuf/proto" -) - -// TransientStorage gives a service that does inefficiently locking, keeps everything in memory, and doesn't -// exit clean from update transactions if there's an error. -type TransientStorage struct { - dbLock sync.RWMutex - data map[string]map[string][]byte -} - -// ExecuteReadOnly executes a read only query -func (bbs *TransientStorage) ExecuteReadOnly(ctx context.Context, namespace []byte, f func(ctx context.Context, db verifiable.KeyReader) error) error { - key := hex.EncodeToString(namespace) - - bbs.dbLock.RLock() - defer bbs.dbLock.RUnlock() - - var db map[string][]byte - if bbs.data != nil { - x, ok := bbs.data[key] - if ok { - db = x - } - } - - return f(ctx, &memoryThing{Data: db}) -} - -// ExecuteUpdate executes an update query -func (bbs *TransientStorage) ExecuteUpdate(ctx context.Context, namespace []byte, f func(ctx context.Context, db verifiable.KeyWriter) error) error { - key := hex.EncodeToString(namespace) - - bbs.dbLock.Lock() - defer bbs.dbLock.Unlock() - - if bbs.data == nil { - bbs.data = make(map[string]map[string][]byte) - } - db, ok := bbs.data[key] - if !ok { - db = make(map[string][]byte) - bbs.data[key] = db - } - return f(ctx, &memoryThing{Data: db}) -} - -type memoryThing struct { - Data map[string][]byte -} - -func (db *memoryThing) Get(ctx context.Context, key []byte, value proto.Message) error { - if db.Data == nil { - return verifiable.ErrNoSuchKey - } - rv, ok := db.Data[string(key)] - if !ok { // as distinct from 0 length - return verifiable.ErrNoSuchKey - } - return proto.Unmarshal(rv, value) -} - -func (db *memoryThing) Set(ctx context.Context, key []byte, value proto.Message) error { - if db.Data == nil { - return verifiable.ErrNotImplemented - } - actKey := string(key) - if value == nil { - delete(db.Data, actKey) - return nil - } - bb, err := proto.Marshal(value) - if err != nil { - return err - } - db.Data[actKey] = bb - return nil -} diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md index 70480d6..2ce0785 100644 --- a/vendor/github.com/olekukonko/tablewriter/README.md +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5 #### Latest Version The latest stable version ```bash -go get github.com/olekukonko/tablewriter@v1.0.7 +go get github.com/olekukonko/tablewriter@v1.0.9 ``` **Warning:** Version `v1.0.0` contains missing functionality and should not be used. @@ -62,7 +62,7 @@ func main() { data := [][]string{ {"Package", "Version", "Status"}, {"tablewriter", "v0.0.5", "legacy"}, - {"tablewriter", "v1.0.7", "latest"}, + {"tablewriter", "v1.0.9", "latest"}, } table := tablewriter.NewWriter(os.Stdout) @@ -77,7 +77,7 @@ func main() { │ PACKAGE │ VERSION │ STATUS │ ├─────────────┼─────────┼────────┤ │ tablewriter │ v0.0.5 │ legacy │ -│ tablewriter │ v1.0.7 │ latest │ +│ tablewriter │ v1.0.9 │ latest │ └─────────────┴─────────┴────────┘ ``` diff --git a/vendor/github.com/olekukonko/tablewriter/config.go b/vendor/github.com/olekukonko/tablewriter/config.go index 94094f1..93f7fa3 100644 --- a/vendor/github.com/olekukonko/tablewriter/config.go +++ b/vendor/github.com/olekukonko/tablewriter/config.go @@ -688,6 +688,12 @@ func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfi return bb } +// WithAutoHeader enables/disables automatic header extraction for structs in Bulk. +func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder { + bb.config.Structs.AutoHeader = state + return bb +} + // ColumnConfigBuilder configures column-specific settings type ColumnConfigBuilder struct { parent *ConfigBuilder diff --git a/vendor/github.com/olekukonko/tablewriter/option.go b/vendor/github.com/olekukonko/tablewriter/option.go index 7270b76..f1ea70b 100644 --- a/vendor/github.com/olekukonko/tablewriter/option.go +++ b/vendor/github.com/olekukonko/tablewriter/option.go @@ -717,6 +717,10 @@ func defaultConfig() Config { Behavior: tw.Behavior{ AutoHide: tw.Off, TrimSpace: tw.On, + Structs: tw.Struct{ + AutoHeader: tw.Off, + Tags: []string{"json", "db"}, + }, }, } } @@ -844,6 +848,14 @@ func mergeConfig(dst, src Config) Config { dst.Behavior.Compact = src.Behavior.Compact dst.Behavior.Header = src.Behavior.Header dst.Behavior.Footer = src.Behavior.Footer + dst.Behavior.Footer = src.Behavior.Footer + + dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader + + // check lent of tags + if len(src.Behavior.Structs.Tags) > 0 { + dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags + } if src.Widths.Global != 0 { dst.Widths.Global = src.Widths.Global diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go index 42966eb..8cfb2a1 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go @@ -523,9 +523,22 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) { isTotalPattern := false + // Case-insensitive check for "total" + if isHMergeStart && colIndex > 0 { + if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok { + if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") { + isTotalPattern = true + f.logger.Debugf("renderLine: total pattern in row in %d", colIndex) + } + } + } + + // Get the alignment from the configuration + align = cellCtx.Align + // Override alignment for footer merged cells if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { - if align != tw.AlignRight { + if align == tw.AlignNone { f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align) align = tw.AlignRight } diff --git a/vendor/github.com/olekukonko/tablewriter/stream.go b/vendor/github.com/olekukonko/tablewriter/stream.go index 7467e9c..d1c6e99 100644 --- a/vendor/github.com/olekukonko/tablewriter/stream.go +++ b/vendor/github.com/olekukonko/tablewriter/stream.go @@ -1,7 +1,6 @@ package tablewriter import ( - "fmt" "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" @@ -90,7 +89,7 @@ func (t *Table) Start() error { if !t.renderer.Config().Streaming { // Check if the configured renderer actually supports streaming. t.logger.Error("Configured renderer does not support streaming.") - return fmt.Errorf("renderer does not support streaming") + return errors.Newf("renderer does not support streaming") } //t.renderer.Start(t.writer) @@ -208,7 +207,7 @@ func (t *Table) streamAppendRow(row interface{}) error { rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row) if err != nil { t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err) - return fmt.Errorf("failed to convert row to strings: %w", err) + return errors.Newf("failed to convert row to strings").Wrap(err) } if len(rawCellsSlice) == 0 { @@ -221,7 +220,7 @@ func (t *Table) streamAppendRow(row interface{}) error { } if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil { - return fmt.Errorf("failed to establish stream column count/widths: %w", err) + return errors.New("failed to establish stream column count/widths").Wrap(err) } // Now, check for column mismatch if a column count has been established. diff --git a/vendor/github.com/olekukonko/tablewriter/tablewriter.go b/vendor/github.com/olekukonko/tablewriter/tablewriter.go index 4aed1a6..e96cc93 100644 --- a/vendor/github.com/olekukonko/tablewriter/tablewriter.go +++ b/vendor/github.com/olekukonko/tablewriter/tablewriter.go @@ -2,7 +2,6 @@ package tablewriter import ( "bytes" - "fmt" "github.com/olekukonko/errors" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" @@ -180,65 +179,87 @@ func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modif // This method always contributes to a single logical row in the table. // To add multiple distinct rows, call Append multiple times (once for each row's data) // or use the Bulk() method if providing a slice where each element is a row. -func (t *Table) Append(rows ...interface{}) error { // rows is already []interface{} +func (t *Table) Append(rows ...interface{}) error { t.ensureInitialized() if t.config.Stream.Enable && t.hasPrinted { + // Streaming logic remains unchanged, as AutoHeader is a batch-mode concept. t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows)) var rowItemForStream interface{} if len(rows) == 1 { rowItemForStream = rows[0] } else { - rowItemForStream = rows // Pass the slice of items if multiple args + rowItemForStream = rows } if err := t.streamAppendRow(rowItemForStream); err != nil { t.logger.Errorf("Error rendering streaming row: %v", err) - return fmt.Errorf("failed to stream append row: %w", err) + return errors.Newf("failed to stream append row").Wrap(err) } return nil } - //Batch Mode Logic + // Batch Mode Logic t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows) var cellsSource interface{} if len(rows) == 1 { cellsSource = rows[0] - t.logger.Debug("Append (Batch): Single argument provided. Treating it as the source for row cells.") } else { - cellsSource = rows // 'rows' is []interface{} containing all arguments - t.logger.Debug("Append (Batch): Multiple arguments provided. Treating them directly as cells for one row.") + cellsSource = rows + } + // Check if we should attempt to auto-generate headers from this append operation. + // Conditions: AutoHeader is on, no headers are set yet, and this is the first data row. + isFirstRow := len(t.rows) == 0 + if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow { + t.logger.Debug("Append: Triggering AutoHeader for the first row.") + headers := t.extractHeadersFromStruct(cellsSource) + if len(headers) > 0 { + // Set the extracted headers. The Header() method handles the rest. + t.Header(headers) + } } - if err := t.appendSingle(cellsSource); err != nil { + // The rest of the function proceeds as before, converting the data to string lines. + lines, err := t.toStringLines(cellsSource, t.config.Row) + if err != nil { t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err) return err } + t.rows = append(t.rows, lines) t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows)) return nil } -// Bulk adds multiple rows from a slice to the table (legacy method). -// Parameter rows must be a slice compatible with stringer or []string. -// Returns an error if the input is invalid or appending fails. +// Bulk adds multiple rows from a slice to the table. +// If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs, +// automatically extracts/sets headers from the first struct. func (t *Table) Bulk(rows interface{}) error { - t.logger.Debug("Starting Bulk operation") rv := reflect.ValueOf(rows) if rv.Kind() != reflect.Slice { - err := errors.Newf("Bulk expects a slice, got %T", rows) - t.logger.Debugf("Bulk error: %v", err) - return err + return errors.Newf("Bulk expects a slice, got %T", rows) + } + if rv.Len() == 0 { + return nil } + + // AutoHeader logic remains here, as it's a "Bulk" operation concept. + if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 { + first := rv.Index(0).Interface() + // We can now correctly get headers from pointers or embedded structs + headers := t.extractHeadersFromStruct(first) + if len(headers) > 0 { + t.Header(headers) + } + } + + // The rest of the logic is now just a loop over Append. for i := 0; i < rv.Len(); i++ { row := rv.Index(i).Interface() - t.logger.Debugf("Processing bulk row %d: %v", i, row) - if err := t.appendSingle(row); err != nil { - t.logger.Debugf("Bulk append failed at index %d: %v", i, err) + if err := t.Append(row); err != nil { // Use Append return err } } - t.logger.Debugf("Bulk completed, processed %d rows", rv.Len()) return nil } @@ -1383,13 +1404,13 @@ func (t *Table) render() error { if err != nil { t.writer = originalWriter t.logger.Errorf("prepareContexts failed: %v", err) - return fmt.Errorf("failed to prepare table contexts: %w", err) + return errors.Newf("failed to prepare table contexts").Wrap(err) } if err := ctx.renderer.Start(t.writer); err != nil { t.writer = originalWriter t.logger.Errorf("Renderer Start() error: %v", err) - return fmt.Errorf("renderer start failed: %w", err) + return errors.Newf("renderer start failed").Wrap(err) } renderError := false @@ -1404,7 +1425,7 @@ func (t *Table) render() error { if renderErr := renderFn(ctx, mctx); renderErr != nil { t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr) if !renderError { - firstRenderErr = fmt.Errorf("failed to render %s section: %w", sectionName, renderErr) + firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr) } renderError = true break @@ -1414,7 +1435,7 @@ func (t *Table) render() error { if closeErr := ctx.renderer.Close(); closeErr != nil { t.logger.Errorf("Renderer Close() error: %v", closeErr) if !renderError { - firstRenderErr = fmt.Errorf("renderer close failed: %w", closeErr) + firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr) } renderError = true } diff --git a/vendor/github.com/olekukonko/tablewriter/tw/types.go b/vendor/github.com/olekukonko/tablewriter/tw/types.go index 29a1862..c3cef66 100644 --- a/vendor/github.com/olekukonko/tablewriter/tw/types.go +++ b/vendor/github.com/olekukonko/tablewriter/tw/types.go @@ -141,6 +141,18 @@ type Compact struct { Merge State // Merge enables compact width calculation during cell merging, optimizing space allocation. } +// Struct holds settings for struct-based operations like AutoHeader. +type Struct struct { + // AutoHeader automatically extracts and sets headers from struct fields when Bulk is called with a slice of structs. + // Uses JSON tags if present, falls back to field names (title-cased). Skips unexported or json:"-" fields. + // Enabled by default for convenience. + AutoHeader State + + // Tags is a priority-ordered list of struct tag keys to check for header names. + // The first tag found on a field will be used. Defaults to ["json", "db"]. + Tags []string +} + // Behavior defines settings that control table rendering behaviors, such as column visibility and content formatting. type Behavior struct { AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode. @@ -152,6 +164,9 @@ type Behavior struct { // Compact enables optimized width calculation for merged cells, such as in horizontal merges, // by systematically determining the most efficient width instead of scaling by the number of columns. Compact Compact + + // Structs contains settings for how struct data is processed. + Structs Struct } // Padding defines the spacing characters around cell content in all four directions. diff --git a/vendor/github.com/olekukonko/tablewriter/zoo.go b/vendor/github.com/olekukonko/tablewriter/zoo.go index b24f230..e0fee2c 100644 --- a/vendor/github.com/olekukonko/tablewriter/zoo.go +++ b/vendor/github.com/olekukonko/tablewriter/zoo.go @@ -1197,6 +1197,10 @@ func (t *Table) convertToString(value interface{}) string { // convertItemToCells is responsible for converting a single input item (which could be // a struct, a basic type, or an item implementing Stringer/Formatter) into a slice // of strings, where each string represents a cell for the table row. +// zoo.go + +// convertItemToCells is responsible for converting a single input item into a slice of strings. +// It now uses the unified struct parser for structs. func (t *Table) convertItemToCells(item interface{}) ([]string, error) { t.logger.Debugf("convertItemToCells: Converting item of type %T", item) @@ -1204,10 +1208,10 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) { if t.stringer != nil { res, err := t.convertToStringer(item) if err == nil { - t.logger.Debugf("convertItemToCells: Used custom table stringer (t.stringer) for type %T. Produced %d cells: %v", item, len(res), res) + t.logger.Debugf("convertItemToCells: Used custom table stringer for type %T. Produced %d cells: %v", item, len(res), res) return res, nil } - t.logger.Warnf("convertItemToCells: Custom table stringer (t.stringer) was set but incompatible or errored for type %T: %v. Will attempt other conversion methods.", item, err) + t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err) } // 2. Handle untyped nil directly. @@ -1216,85 +1220,26 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) { return []string{""}, nil } - itemValue := reflect.ValueOf(item) - itemType := itemValue.Type() - - // 3. Handle pointers: Dereference pointers to get to the underlying struct or value. - if itemType.Kind() == reflect.Ptr { - if itemValue.IsNil() { - t.logger.Debugf("convertItemToCells: Item is a nil pointer of type %s. Returning single empty cell.", itemType.String()) - return []string{""}, nil - } - itemValue = itemValue.Elem() - itemType = itemValue.Type() - t.logger.Debugf("convertItemToCells: Dereferenced pointer, now processing type %s.", itemType.String()) + // 3. Use the new unified struct parser. It handles pointers and embedding. + // We only care about the values it returns. + _, values := t.extractFieldsAndValuesFromStruct(item) + if values != nil { + t.logger.Debugf("convertItemToCells: Structs %T reflected into %d cells: %v", item, len(values), values) + return values, nil } - // 4. Special handling for structs: - if itemType.Kind() == reflect.Struct { - // Check if the original item (before potential dereference) implements Formatter or Stringer. - if formatter, ok := item.(tw.Formatter); ok { - t.logger.Debugf("convertItemToCells: Struct item (type %s) is tw.Formatter. Using Format(). Resulting in 1 cell.", itemType.Name()) - return []string{formatter.Format()}, nil - } - if stringer, ok := item.(fmt.Stringer); ok { - t.logger.Debugf("convertItemToCells: Struct item (type %s) is fmt.Stringer. Using String(). Resulting in 1 cell.", itemType.Name()) - return []string{stringer.String()}, nil - } - - t.logger.Debugf("convertItemToCells: Item is a struct (type %s). Attempting generic field reflection to expand into multiple cells.", itemType.Name()) - numFields := itemValue.NumField() - structCells := make([]string, 0, numFields) - hasProcessableFields := false - - for i := 0; i < numFields; i++ { - fieldMeta := itemType.Field(i) - if fieldMeta.PkgPath != "" { - t.logger.Debugf("convertItemToCells: Skipping unexported field %s in struct %s", fieldMeta.Name, itemType.Name()) - continue - } - hasProcessableFields = true // Mark true if we encounter any exported field - - jsonTag := fieldMeta.Tag.Get("json") - if jsonTag == "-" { - t.logger.Debugf("convertItemToCells: Skipping field %s in struct %s due to json:\"-\" tag", fieldMeta.Name, itemType.Name()) - continue - } - - fieldReflectedValue := itemValue.Field(i) - if strings.Contains(jsonTag, ",omitempty") && fieldReflectedValue.IsZero() { - t.logger.Debugf("convertItemToCells: Omitting zero value for field %s in struct %s due to omitempty tag", fieldMeta.Name, itemType.Name()) - structCells = append(structCells, "") - continue - } - structCells = append(structCells, t.convertToString(fieldReflectedValue.Interface())) - } - - // Only return expanded cells if there were processable fields. - // If a struct has no exported fields, or all were skipped via json:"-", - // it should still produce output (e.g. fmt.Sprintf of the struct) rather than an empty row. - if hasProcessableFields { - t.logger.Debugf("convertItemToCells: Struct %s reflected into %d cells: %v", itemType.Name(), len(structCells), structCells) - return structCells, nil - } - - t.logger.Warnf("convertItemToCells: Struct %s has no processable exported fields. Falling back to Sprintf for the whole item (resulting in 1 cell).", itemType.Name()) - return []string{t.convertToString(item)}, nil // 'item' is the original potentially pointer type - } - - // 5. Item is NOT a struct. It might be a basic type or a non-struct type implementing Formatter/Stringer. - // These should all result in a single cell. + // 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter). + // This code path is now for non-struct types. if formatter, ok := item.(tw.Formatter); ok { - t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format(). Resulting in 1 cell.", item) + t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item) return []string{formatter.Format()}, nil } if stringer, ok := item.(fmt.Stringer); ok { - t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String(). Resulting in 1 cell.", item) + t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String().", item) return []string{stringer.String()}, nil } - // 6. Fallback for any other single item (e.g., basic types like int, string, bool): - t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type or unhandled by other mechanisms. Treating as single cell via convertToString.", item) + t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type. Treating as single cell via convertToString.", item) return []string{t.convertToString(item)}, nil } @@ -1694,3 +1639,92 @@ func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding t } } } + +// extractHeadersFromStruct is now a thin wrapper around the new unified function. +// It only cares about the header names. +func (t *Table) extractHeadersFromStruct(sample interface{}) []string { + headers, _ := t.extractFieldsAndValuesFromStruct(sample) + return headers +} + +// extractFieldsAndValuesFromStruct is the new single source of truth for struct reflection. +// It recursively processes a struct, handling pointers and embedded structs, +// and returns two slices: one for header names and one for string-converted values. +func (t *Table) extractFieldsAndValuesFromStruct(sample interface{}) ([]string, []string) { + v := reflect.ValueOf(sample) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, nil + } + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil, nil + } + + typ := v.Type() + headers := make([]string, 0, typ.NumField()) + values := make([]string, 0, typ.NumField()) + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldValue := v.Field(i) + + // Skip unexported fields + if field.PkgPath != "" { + continue + } + + // Handle embedded structs recursively + if field.Anonymous { + h, val := t.extractFieldsAndValuesFromStruct(fieldValue.Interface()) + if h != nil { + headers = append(headers, h...) + values = append(values, val...) + } + continue + } + + var tagName string + skipField := false + + // Loop through the priority list of configured tags (e.g., ["json", "db"]) + for _, tagKey := range t.config.Behavior.Structs.Tags { + tagValue := field.Tag.Get(tagKey) + + // If a tag is found... + if tagValue != "" { + // If the tag is "-", this field should be skipped entirely. + if tagValue == "-" { + skipField = true + break // Stop processing tags for this field. + } + // Otherwise, we've found our highest-priority tag. Store it and stop. + tagName = tagValue + break // Stop processing tags for this field. + } + } + + // If the field was marked for skipping, continue to the next field. + if skipField { + continue + } + + // Determine header name from the tag or fallback to the field name + headerName := field.Name + if tagName != "" { + headerName = strings.Split(tagName, ",")[0] + } + headers = append(headers, tw.Title(headerName)) + + // Determine value, respecting omitempty from the found tag + value := "" + if !strings.Contains(tagName, ",omitempty") || !fieldValue.IsZero() { + value = t.convertToString(fieldValue.Interface()) + } + values = append(values, value) + } + + return headers, values +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 131d1fc..e0f3886 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -20,7 +20,6 @@ github.com/continusec/verifiabledatastructures/oracle/policy github.com/continusec/verifiabledatastructures/pb github.com/continusec/verifiabledatastructures/server/httprest github.com/continusec/verifiabledatastructures/storage/bolt -github.com/continusec/verifiabledatastructures/storage/memory github.com/continusec/verifiabledatastructures/verifiable # github.com/cpuguy83/go-md2man/v2 v2.0.7 ## explicit; go 1.12 @@ -54,7 +53,7 @@ github.com/olekukonko/errors github.com/olekukonko/ll github.com/olekukonko/ll/lh github.com/olekukonko/ll/lx -# github.com/olekukonko/tablewriter v1.0.8 +# github.com/olekukonko/tablewriter v1.0.9 ## explicit; go 1.21 github.com/olekukonko/tablewriter github.com/olekukonko/tablewriter/pkg/twwarp