Files
relspecgo/pkg/inspector/report.go
Hein 97a57f5dc8
Some checks failed
CI / Test (1.24) (push) Successful in -25m44s
CI / Test (1.25) (push) Successful in -25m40s
CI / Build (push) Successful in -25m53s
CI / Lint (push) Successful in -25m45s
Integration Tests / Integration Tests (push) Failing after -26m2s
feature: Inspector Gadget
2025-12-31 01:40:08 +02:00

230 lines
5.7 KiB
Go

package inspector
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"
)
// ANSI color codes
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorYellow = "\033[33m"
colorGreen = "\033[32m"
colorBold = "\033[1m"
)
// ReportFormatter defines the interface for report formatters
type ReportFormatter interface {
Format(report *InspectorReport) (string, error)
}
// MarkdownFormatter formats reports as markdown
type MarkdownFormatter struct {
UseColors bool
}
// JSONFormatter formats reports as JSON
type JSONFormatter struct{}
// NewMarkdownFormatter creates a markdown formatter with color support detection
func NewMarkdownFormatter(writer io.Writer) *MarkdownFormatter {
return &MarkdownFormatter{
UseColors: isTerminal(writer),
}
}
// NewJSONFormatter creates a JSON formatter
func NewJSONFormatter() *JSONFormatter {
return &JSONFormatter{}
}
// Format generates a markdown report
func (f *MarkdownFormatter) Format(report *InspectorReport) (string, error) {
var sb strings.Builder
// Header
sb.WriteString(f.formatHeader("RelSpec Inspector Report"))
sb.WriteString("\n\n")
// Metadata
sb.WriteString(f.formatBold("Database:") + " " + report.Database + "\n")
sb.WriteString(f.formatBold("Source Format:") + " " + report.SourceFormat + "\n")
sb.WriteString(f.formatBold("Generated:") + " " + report.GeneratedAt.Format(time.RFC3339) + "\n")
sb.WriteString("\n")
// Summary
sb.WriteString(f.formatHeader("Summary"))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("- Rules Checked: %d\n", report.Summary.RulesChecked))
// Color-code error and warning counts
if report.Summary.ErrorCount > 0 {
sb.WriteString(f.colorize(fmt.Sprintf("- Errors: %d\n", report.Summary.ErrorCount), colorRed))
} else {
sb.WriteString(fmt.Sprintf("- Errors: %d\n", report.Summary.ErrorCount))
}
if report.Summary.WarningCount > 0 {
sb.WriteString(f.colorize(fmt.Sprintf("- Warnings: %d\n", report.Summary.WarningCount), colorYellow))
} else {
sb.WriteString(fmt.Sprintf("- Warnings: %d\n", report.Summary.WarningCount))
}
if report.Summary.PassedCount > 0 {
sb.WriteString(f.colorize(fmt.Sprintf("- Passed: %d\n", report.Summary.PassedCount), colorGreen))
}
sb.WriteString("\n")
// Group violations by level
errors := []ValidationResult{}
warnings := []ValidationResult{}
for _, v := range report.Violations {
if !v.Passed {
if v.Level == "error" {
errors = append(errors, v)
} else {
warnings = append(warnings, v)
}
}
}
// Report violations
if len(errors) > 0 || len(warnings) > 0 {
sb.WriteString(f.formatHeader("Violations"))
sb.WriteString("\n")
// Errors
if len(errors) > 0 {
sb.WriteString(f.formatSubheader(fmt.Sprintf("Errors (%d)", len(errors)), colorRed))
sb.WriteString("\n")
for _, violation := range errors {
sb.WriteString(f.formatViolation(violation, colorRed))
}
sb.WriteString("\n")
}
// Warnings
if len(warnings) > 0 {
sb.WriteString(f.formatSubheader(fmt.Sprintf("Warnings (%d)", len(warnings)), colorYellow))
sb.WriteString("\n")
for _, violation := range warnings {
sb.WriteString(f.formatViolation(violation, colorYellow))
}
}
} else {
sb.WriteString(f.colorize("✓ No violations found!\n", colorGreen))
}
return sb.String(), nil
}
// Format generates a JSON report
func (f *JSONFormatter) Format(report *InspectorReport) (string, error) {
data, err := json.MarshalIndent(report, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal report to JSON: %w", err)
}
return string(data), nil
}
// Helper methods for MarkdownFormatter
func (f *MarkdownFormatter) formatHeader(text string) string {
return f.formatBold("# " + text)
}
func (f *MarkdownFormatter) formatSubheader(text string, color string) string {
header := "### " + text
if f.UseColors {
return color + colorBold + header + colorReset
}
return header
}
func (f *MarkdownFormatter) formatBold(text string) string {
if f.UseColors {
return colorBold + text + colorReset
}
return "**" + text + "**"
}
func (f *MarkdownFormatter) colorize(text string, color string) string {
if f.UseColors {
return color + text + colorReset
}
return text
}
func (f *MarkdownFormatter) formatViolation(v ValidationResult, color string) string {
var sb strings.Builder
// Rule name as header
if f.UseColors {
sb.WriteString(color + "#### " + v.RuleName + colorReset + "\n")
} else {
sb.WriteString("#### " + v.RuleName + "\n")
}
// Location and message
sb.WriteString(f.formatBold("Location:") + " " + v.Location + "\n")
sb.WriteString(f.formatBold("Message:") + " " + v.Message + "\n")
// Context details (optional, only show interesting ones)
if len(v.Context) > 0 {
contextStr := f.formatContext(v.Context)
if contextStr != "" {
sb.WriteString(f.formatBold("Details:") + " " + contextStr + "\n")
}
}
sb.WriteString("\n")
return sb.String()
}
func (f *MarkdownFormatter) formatContext(context map[string]interface{}) string {
// Extract relevant context information
var parts []string
// Skip schema, table, column as they're in location
skipKeys := map[string]bool{
"schema": true,
"table": true,
"column": true,
}
for key, value := range context {
if skipKeys[key] {
continue
}
parts = append(parts, fmt.Sprintf("%s=%v", key, value))
}
return strings.Join(parts, ", ")
}
// isTerminal checks if the writer is a terminal (supports ANSI colors)
func isTerminal(w io.Writer) bool {
file, ok := w.(*os.File)
if !ok {
return false
}
// Check if the file descriptor is a terminal
stat, err := file.Stat()
if err != nil {
return false
}
// Check if it's a character device (terminal)
// This works on Unix-like systems
return (stat.Mode() & os.ModeCharDevice) != 0
}