build(deps): bump github.com/olekukonko/tablewriter from 1.0.8 to 1.0.9
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.8 to 1.0.9. - [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.8...v1.0.9) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
+3
-3
@@ -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 │
|
||||
└─────────────┴─────────┴────────┘
|
||||
```
|
||||
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
+12
@@ -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
|
||||
|
||||
+14
-1
@@ -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
|
||||
}
|
||||
|
||||
+3
-4
@@ -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.
|
||||
|
||||
+45
-24
@@ -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
|
||||
}
|
||||
|
||||
+15
@@ -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.
|
||||
|
||||
+106
-72
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user