mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-11 18:48:32 +00:00
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
260 lines
6.3 KiB
Go
260 lines
6.3 KiB
Go
package internal
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/fatih/color"
|
||
|
||
"github.com/ivuorinen/gh-action-readme/internal/errors"
|
||
)
|
||
|
||
// ColoredOutput provides methods for colored terminal output.
|
||
// It implements all the focused interfaces for backward compatibility.
|
||
type ColoredOutput struct {
|
||
NoColor bool
|
||
Quiet bool
|
||
}
|
||
|
||
// Compile-time interface checks.
|
||
var (
|
||
_ MessageLogger = (*ColoredOutput)(nil)
|
||
_ ErrorReporter = (*ColoredOutput)(nil)
|
||
_ ErrorFormatter = (*ColoredOutput)(nil)
|
||
_ ProgressReporter = (*ColoredOutput)(nil)
|
||
_ OutputConfig = (*ColoredOutput)(nil)
|
||
_ CompleteOutput = (*ColoredOutput)(nil)
|
||
)
|
||
|
||
// NewColoredOutput creates a new colored output instance.
|
||
func NewColoredOutput(quiet bool) *ColoredOutput {
|
||
return &ColoredOutput{
|
||
NoColor: color.NoColor || os.Getenv("NO_COLOR") != "",
|
||
Quiet: quiet,
|
||
}
|
||
}
|
||
|
||
// IsQuiet returns whether the output is in quiet mode.
|
||
func (co *ColoredOutput) IsQuiet() bool {
|
||
return co.Quiet
|
||
}
|
||
|
||
// Success prints a success message in green.
|
||
func (co *ColoredOutput) Success(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
if co.NoColor {
|
||
fmt.Printf("✅ "+format+"\n", args...)
|
||
} else {
|
||
color.Green("✅ "+format, args...)
|
||
}
|
||
}
|
||
|
||
// Error prints an error message in red to stderr.
|
||
func (co *ColoredOutput) Error(format string, args ...any) {
|
||
if co.NoColor {
|
||
fmt.Fprintf(os.Stderr, "❌ "+format+"\n", args...)
|
||
} else {
|
||
_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "❌ "+format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// Warning prints a warning message in yellow.
|
||
func (co *ColoredOutput) Warning(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
if co.NoColor {
|
||
fmt.Printf("⚠️ "+format+"\n", args...)
|
||
} else {
|
||
color.Yellow("⚠️ "+format, args...)
|
||
}
|
||
}
|
||
|
||
// Info prints an info message in blue.
|
||
func (co *ColoredOutput) Info(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
if co.NoColor {
|
||
fmt.Printf("ℹ️ "+format+"\n", args...)
|
||
} else {
|
||
color.Blue("ℹ️ "+format, args...)
|
||
}
|
||
}
|
||
|
||
// Progress prints a progress message in cyan.
|
||
func (co *ColoredOutput) Progress(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
if co.NoColor {
|
||
fmt.Printf("🔄 "+format+"\n", args...)
|
||
} else {
|
||
color.Cyan("🔄 "+format, args...)
|
||
}
|
||
}
|
||
|
||
// Bold prints text in bold.
|
||
func (co *ColoredOutput) Bold(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
if co.NoColor {
|
||
fmt.Printf(format+"\n", args...)
|
||
} else {
|
||
_, _ = color.New(color.Bold).Printf(format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// Printf prints without color formatting (respects quiet mode).
|
||
func (co *ColoredOutput) Printf(format string, args ...any) {
|
||
if co.Quiet {
|
||
return
|
||
}
|
||
fmt.Printf(format, args...)
|
||
}
|
||
|
||
// Fprintf prints to specified writer without color formatting.
|
||
func (co *ColoredOutput) Fprintf(w *os.File, format string, args ...any) {
|
||
_, _ = fmt.Fprintf(w, format, args...)
|
||
}
|
||
|
||
// ErrorWithSuggestions prints a ContextualError with suggestions and help.
|
||
func (co *ColoredOutput) ErrorWithSuggestions(err *errors.ContextualError) {
|
||
if err == nil {
|
||
return
|
||
}
|
||
|
||
// Print main error message
|
||
if co.NoColor {
|
||
fmt.Fprintf(os.Stderr, "❌ %s\n", err.Error())
|
||
} else {
|
||
color.Red("❌ %s", err.Error())
|
||
}
|
||
}
|
||
|
||
// ErrorWithContext creates and prints a contextual error with suggestions.
|
||
func (co *ColoredOutput) ErrorWithContext(
|
||
code errors.ErrorCode,
|
||
message string,
|
||
context map[string]string,
|
||
) {
|
||
suggestions := errors.GetSuggestions(code, context)
|
||
helpURL := errors.GetHelpURL(code)
|
||
|
||
contextualErr := errors.New(code, message).
|
||
WithSuggestions(suggestions...).
|
||
WithHelpURL(helpURL)
|
||
|
||
if len(context) > 0 {
|
||
contextualErr = contextualErr.WithDetails(context)
|
||
}
|
||
|
||
co.ErrorWithSuggestions(contextualErr)
|
||
}
|
||
|
||
// ErrorWithSimpleFix prints an error with a simple suggestion.
|
||
func (co *ColoredOutput) ErrorWithSimpleFix(message, suggestion string) {
|
||
contextualErr := errors.New(errors.ErrCodeUnknown, message).
|
||
WithSuggestions(suggestion)
|
||
|
||
co.ErrorWithSuggestions(contextualErr)
|
||
}
|
||
|
||
// FormatContextualError formats a ContextualError for display.
|
||
func (co *ColoredOutput) FormatContextualError(err *errors.ContextualError) string {
|
||
if err == nil {
|
||
return ""
|
||
}
|
||
|
||
var parts []string
|
||
|
||
// Add main error message
|
||
parts = append(parts, co.formatMainError(err))
|
||
|
||
// Add details section
|
||
if len(err.Details) > 0 {
|
||
parts = append(parts, co.formatDetailsSection(err.Details)...)
|
||
}
|
||
|
||
// Add suggestions section
|
||
if len(err.Suggestions) > 0 {
|
||
parts = append(parts, co.formatSuggestionsSection(err.Suggestions)...)
|
||
}
|
||
|
||
// Add help URL section
|
||
if err.HelpURL != "" {
|
||
parts = append(parts, co.formatHelpURLSection(err.HelpURL))
|
||
}
|
||
|
||
return strings.Join(parts, "\n")
|
||
}
|
||
|
||
// formatMainError formats the main error message with code.
|
||
func (co *ColoredOutput) formatMainError(err *errors.ContextualError) string {
|
||
mainMsg := fmt.Sprintf("%s [%s]", err.Error(), err.Code)
|
||
if co.NoColor {
|
||
return "❌ " + mainMsg
|
||
}
|
||
return color.RedString("❌ ") + mainMsg
|
||
}
|
||
|
||
// formatDetailsSection formats the details section.
|
||
func (co *ColoredOutput) formatDetailsSection(details map[string]string) []string {
|
||
var parts []string
|
||
|
||
if co.NoColor {
|
||
parts = append(parts, "\nDetails:")
|
||
} else {
|
||
parts = append(parts, color.New(color.Bold).Sprint("\nDetails:"))
|
||
}
|
||
|
||
for key, value := range details {
|
||
if co.NoColor {
|
||
parts = append(parts, fmt.Sprintf(" %s: %s", key, value))
|
||
} else {
|
||
parts = append(parts, fmt.Sprintf(" %s: %s",
|
||
color.CyanString(key),
|
||
color.WhiteString(value)))
|
||
}
|
||
}
|
||
|
||
return parts
|
||
}
|
||
|
||
// formatSuggestionsSection formats the suggestions section.
|
||
func (co *ColoredOutput) formatSuggestionsSection(suggestions []string) []string {
|
||
var parts []string
|
||
|
||
if co.NoColor {
|
||
parts = append(parts, "\nSuggestions:")
|
||
} else {
|
||
parts = append(parts, color.New(color.Bold).Sprint("\nSuggestions:"))
|
||
}
|
||
|
||
for _, suggestion := range suggestions {
|
||
if co.NoColor {
|
||
parts = append(parts, fmt.Sprintf(" • %s", suggestion))
|
||
} else {
|
||
parts = append(parts, fmt.Sprintf(" %s %s",
|
||
color.YellowString("•"),
|
||
color.WhiteString(suggestion)))
|
||
}
|
||
}
|
||
|
||
return parts
|
||
}
|
||
|
||
// formatHelpURLSection formats the help URL section.
|
||
func (co *ColoredOutput) formatHelpURLSection(helpURL string) string {
|
||
if co.NoColor {
|
||
return fmt.Sprintf("\nFor more help: %s", helpURL)
|
||
}
|
||
return fmt.Sprintf("\n%s: %s",
|
||
color.New(color.Bold).Sprint("For more help"),
|
||
color.BlueString(helpURL))
|
||
}
|