230 lines
5.7 KiB
Go
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
|
|
}
|