Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func NewText(content string) *TextBuilder {
return &TextBuilder{
text: Text{
Content: content,
Children: make([]Text, 0),
Children: make([]Textable, 0),
},
}
}
Expand Down
4 changes: 2 additions & 2 deletions api/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func TestChildBuilder(t *testing.T) {
t.Errorf("expected 1 child, got %d", len(result.Children))
}

if result.Children[0].Content != "Child" {
t.Errorf("expected child content %q, got %q", "Child", result.Children[0].Content)
if result.Children[0].String() != "Child" {
t.Errorf("expected child content %q, got %q", "Child", result.Children[0].String())
}
}
87 changes: 87 additions & 0 deletions api/icons/icons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package icons

import "fmt"

type Icon struct {
Unicode string
Iconify string
Style string
}

// String returns the Unicode representation of the icon
func (i Icon) String() string {
return i.Unicode
}

// ANSI returns the Unicode representation (same as String for icons)
func (i Icon) ANSI() string {
return i.Unicode
}

// HTML returns an HTML representation using Iconify classes or Unicode fallback
func (i Icon) HTML() string {
if i.Iconify != "" {
return fmt.Sprintf(`<i class="iconify" data-icon="%s">%s</i>`, i.Iconify, i.Unicode)
}
return i.Unicode
}

// Markdown returns the Unicode representation (same as String for icons)
func (i Icon) Markdown() string {
return i.Unicode
}

var (
Config = "⚙️"
Success Icon = Icon{Unicode: "✓", Iconify: "check", Style: "success"}
Error = Icon{Unicode: "✗", Iconify: "close", Style: "error"}
Fail = Icon{Unicode: "✗", Iconify: "close", Style: "error"}
Pass = Icon{Unicode: "✓", Iconify: "check", Style: "success"}
Skip = Icon{Unicode: "→", Iconify: "arrow-right", Style: "warning"}
Unknown = Icon{Unicode: "?", Iconify: "help", Style: "muted"}
Info = Icon{Unicode: "•", Iconify: "bullet", Style: "info"}
Warning = Icon{Unicode: "!", Iconify: "alert-circle", Style: "warning"}
Circle = Icon{Unicode: "○", Iconify: "circle", Style: "muted"}
ArrowUp = Icon{Unicode: "↑", Iconify: "arrow-up", Style: "muted"}
ArrowDown = Icon{Unicode: "↓", Iconify: "arrow-down", Style: "muted"}
ArrowLeft = Icon{Unicode: "←", Iconify: "arrow-left", Style: "muted"}
ArrowUpRight = Icon{Unicode: "↗", Iconify: "arrow-up-right", Style: "muted"}
ArrowDownRight = Icon{Unicode: "↘", Iconify: "arrow-down-right", Style: "muted"}
ArrowDownLeft = Icon{Unicode: "↙", Iconify: "arrow-down-left", Style: "muted"}
ArrowUpLeft = Icon{Unicode: "↖", Iconify: "arrow-up-left", Style: "muted"}
ArrowDoubleUpDown = Icon{Unicode: "⇕", Iconify: "arrows-up-down", Style: "muted"}
ArrowUpDown = Icon{Unicode: "⇵", Iconify: "arrow-up-down", Style: "muted"}
ArrowLeftRight = Icon{Unicode: "⇄", Iconify: "arrows-left-right", Style: "muted"}
ArrowoubleLeftRight = Icon{Unicode: "⇔", Iconify: "arrows-left-right", Style: "muted"}
ArrowDoubleRight = Icon{Unicode: "⇒", Iconify: "arrow-right", Style: "muted"}
ArrowDoubleLeft = Icon{Unicode: "⇐", Iconify: "arrow-left", Style: "muted"}
ArrowRight = Icon{Unicode: "→", Iconify: "arrow-right", Style: "muted"}
ChevronUp = Icon{Unicode: "▲", Iconify: "chevron-up", Style: "muted"}
ChevronDown = Icon{Unicode: "▼", Iconify: "chevron-down", Style: "muted"}
ChevronLeft = Icon{Unicode: "◀", Iconify: "chevron-left", Style: "muted"}
ChevronRight = Icon{Unicode: "▶", Iconify: "chevron-right", Style: "muted"}
InfoAlt = Icon{Unicode: "ℹ️", Iconify: "info", Style: "info"}
Star = Icon{Unicode: "★", Iconify: "star", Style: "muted"}
Heart = Icon{Unicode: "❤️", Iconify: "heart", Style: "muted"}
Link = Icon{Unicode: "🔗", Iconify: "link", Style: "muted"}
Golang = Icon{Unicode: "🐹", Iconify: "go", Style: "muted"}
Python = Icon{Unicode: "🐍", Iconify: "python", Style: "muted"}
JS = Icon{Unicode: "🟨", Iconify: "javascript", Style: "muted"}
Java = Icon{Unicode: "☕", Iconify: "java", Style: "muted"}
TS = Icon{Unicode: "🟦", Iconify: "typescript", Style: "muted"}
MD = Icon{Unicode: "📝", Iconify: "markdown", Style: "muted"}
File = Icon{Unicode: "📄", Iconify: "file", Style: "muted"}
Folder = Icon{Unicode: "📁", Iconify: "folder", Style: "muted"}
Search = Icon{Unicode: "🔍", Iconify: "search", Style: "muted"}
Cloud = Icon{Unicode: "☁️", Iconify: "cloud", Style: "muted"}
Package = Icon{Unicode: "📦", Iconify: "package", Style: "muted"}
Lambda = Icon{Unicode: "λ", Iconify: "lambda", Style: "muted"}
Method = Icon{Unicode: "ƒ", Iconify: "function", Style: "muted"}
Variable = Icon{Unicode: "𝑣", Iconify: "variable", Style: "muted"}
Type = Icon{Unicode: "🏷️", Iconify: "tag", Style: "muted"}
Interface = Icon{Unicode: "🔗", Iconify: "link", Style: "muted"}
Constant = Icon{Unicode: "π", Iconify: "constant", Style: "muted"}
Http = Icon{Unicode: "🌐", Iconify: "globe", Style: "muted"}
Queue = Icon{Unicode: "📥", Iconify: "inbox", Style: "muted"}
DB = Icon{Unicode: "🗄️", Iconify: "database", Style: "muted"}
)
49 changes: 49 additions & 0 deletions api/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/flanksource/commons/logger"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -852,6 +853,54 @@ func (p *StructParser) GetTableFields(val reflect.Value) ([]PrettyField, error)
return fields, nil
}

// StructToRowWithOptions converts a struct to a PrettyDataRow, checking for PrettyRow interface first
func (p *StructParser) StructToRowWithOptions(val reflect.Value, opts interface{}) (PrettyDataRow, error) {
// Dereference pointer if needed
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, fmt.Errorf("cannot convert nil pointer to row")
}
val = val.Elem()
}

if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct, got %s", val.Kind())
}

structType := val.Type()
logger.V(4).Infof("Processing struct type %s with %d fields for PrettyRow conversion", structType.Name(), val.NumField())

// Check if the struct implements PrettyRow interface
if val.CanInterface() {
if prettyRowInterface, ok := val.Interface().(PrettyRow); ok {
logger.Debugf("Struct %s implements PrettyRow interface - using custom implementation", structType.Name())

// Use the custom PrettyRow implementation
prettyRowMap := prettyRowInterface.PrettyRow(opts)
logger.V(4).Infof("PrettyRow() returned %d columns for struct %s", len(prettyRowMap), structType.Name())

// Convert map[string]Text to PrettyDataRow
row := make(PrettyDataRow)
for key, text := range prettyRowMap {
row[key] = FieldValue{
Value: text.Content,
Text: &text,
Field: PrettyField{Name: key},
}
}
return row, nil
} else {
logger.V(4).Infof("Struct %s does not implement PrettyRow interface - checking CanInterface capability", structType.Name())
}
} else {
logger.V(4).Infof("Struct %s cannot interface - skipping PrettyRow check", structType.Name())
}

// Fall back to reflection-based approach
logger.Debugf("Falling back to reflection-based parsing for struct %s (no PrettyRow interface)", structType.Name())
return p.StructToRow(val)
}

// StructToRow converts a struct to a PrettyDataRow
func (p *StructParser) StructToRow(val reflect.Value) (PrettyDataRow, error) {
// Dereference pointer if needed
Expand Down
148 changes: 148 additions & 0 deletions api/pretty_row_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package api

import (
"fmt"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

// TestStruct implements PrettyRow interface for testing
type TestStruct struct {
Name string
Count int
Status string
}

// PrettyRow implements the PrettyRow interface
func (t TestStruct) PrettyRow(opts interface{}) map[string]Text {
result := make(map[string]Text)

// Custom column name and content
result["Name"] = Text{Content: t.Name, Style: "font-bold"}

// Conditional styling based on format options
countStyle := "text-blue-600"
if opts != nil {
// Simple check for NoColor - in real implementation, you'd type assert to FormatOptions
if s := fmt.Sprintf("%+v", opts); s != "" && fmt.Sprintf("%v", opts) != "<nil>" {
// For testing, assume NoColor is passed in a simple struct
if noColorOpts, ok := opts.(struct{ NoColor bool }); ok && noColorOpts.NoColor {
countStyle = ""
}
}
}
result["Count"] = Text{Content: fmt.Sprintf("%d", t.Count), Style: countStyle}

// Status with conditional coloring
statusStyle := "text-green-600"
if t.Status == "error" {
statusStyle = "text-red-600"
}
if opts != nil {
if noColorOpts, ok := opts.(struct{ NoColor bool }); ok && noColorOpts.NoColor {
statusStyle = ""
}
}
result["Status"] = Text{Content: t.Status, Style: statusStyle}

return result
}

func TestPrettyRowInterface(t *testing.T) {
// Create a test struct that implements PrettyRow
testStruct := TestStruct{
Name: "Test Item",
Count: 5,
Status: "success",
}

// Test basic PrettyRow functionality
prettyRow := testStruct.PrettyRow(nil)
assert.Equal(t, 3, len(prettyRow))
assert.Equal(t, "Test Item", prettyRow["Name"].Content)
assert.Equal(t, "font-bold", prettyRow["Name"].Style)
assert.Equal(t, "5", prettyRow["Count"].Content)
assert.Equal(t, "text-blue-600", prettyRow["Count"].Style)
assert.Equal(t, "success", prettyRow["Status"].Content)
assert.Equal(t, "text-green-600", prettyRow["Status"].Style)
}

func TestPrettyRowWithFormatOptions(t *testing.T) {
testStruct := TestStruct{
Name: "Test Item",
Count: 3,
Status: "error",
}

// Mock FormatOptions with NoColor
opts := struct{ NoColor bool }{NoColor: true}

prettyRow := testStruct.PrettyRow(opts)

// Verify that styles are disabled when NoColor is true
assert.Equal(t, "", prettyRow["Count"].Style)
assert.Equal(t, "", prettyRow["Status"].Style)
assert.Equal(t, "font-bold", prettyRow["Name"].Style) // Name should still have style
}

func TestStructToRowWithOptionsUsesInterface(t *testing.T) {
parser := NewStructParser()
testStruct := TestStruct{
Name: "Interface Test",
Count: 7,
Status: "active",
}

val := reflect.ValueOf(testStruct)
opts := struct{ NoColor bool }{NoColor: false}

// Call StructToRowWithOptions which should detect and use the PrettyRow interface
row, err := parser.StructToRowWithOptions(val, opts)
assert.NoError(t, err)
assert.NotNil(t, row)

// Verify that the PrettyRow interface was used
assert.Equal(t, 3, len(row))

// Check that the custom implementation was used
nameField, exists := row["Name"]
assert.True(t, exists)
assert.Equal(t, "Interface Test", nameField.Value)
assert.NotNil(t, nameField.Text)
assert.Equal(t, "Interface Test", nameField.Text.Content)
assert.Equal(t, "font-bold", nameField.Text.Style)

countField, exists := row["Count"]
assert.True(t, exists)
assert.Equal(t, "7", countField.Value)
assert.NotNil(t, countField.Text)
assert.Equal(t, "text-blue-600", countField.Text.Style)
}

func TestStructToRowFallbackWithoutInterface(t *testing.T) {
parser := NewStructParser()

// Regular struct without PrettyRow interface
regularStruct := struct {
Name string
Value int
}{
Name: "Regular Struct",
Value: 42,
}

val := reflect.ValueOf(regularStruct)
opts := struct{ NoColor bool }{NoColor: false}

// Should fall back to reflection-based approach
row, err := parser.StructToRowWithOptions(val, opts)
assert.NoError(t, err)
assert.NotNil(t, row)

// Verify fallback behavior
nameField, exists := row["Name"]
assert.True(t, exists)
assert.Equal(t, "Regular Struct", nameField.Value)
}
Loading
Loading