Files
commandkit/report.go

122 lines
3.0 KiB
Go
Raw Normal View History

2026-11-07 03:40:09 +00:00
package commandkit
import (
"encoding/json"
"fmt"
"io"
"text/template"
"go.yaml.in/yaml/v4"
)
// Format selects a report representation. The zero value selects automatically.
type Format string
const (
// FormatText renders a text template or the default text representation.
FormatText Format = "text"
// FormatYAML renders the report as YAML.
FormatYAML Format = "yaml"
// FormatJSON renders the report as JSON.
FormatJSON Format = "json"
)
// Report contains application-selected content and an optional text presentation.
// Templates receive Value itself; structured formats use its JSON representation.
type Report struct {
Value any
TextTemplate *template.Template
}
// ParseTextTemplate prepares a reusable standard text template for a report.
func ParseTextTemplate(name, source string, funcs template.FuncMap) (*template.Template, error) {
return template.New(name).Funcs(funcs).Parse(source)
}
// RenderReport writes a report in the selected format. An automatic format uses
// a supplied text template, otherwise it selects text for scalars and nonempty
// string lists and YAML for other values. Text without a template uses YAML for
// structured values. Rendering and writer errors are returned to the caller.
func RenderReport(w io.Writer, report Report, format Format) error {
if format != "" && format != FormatText && format != FormatYAML && format != FormatJSON {
return fmt.Errorf("unknown report format %q", format)
}
writer := reportWriter{w}
if report.TextTemplate != nil && (format == "" || format == FormatText) {
return report.TextTemplate.Execute(writer, report.Value)
}
value, err := normalizeReport(report.Value)
if err != nil {
return err
}
if format == "" {
if reportIsPlainText(value) {
format = FormatText
} else {
format = FormatYAML
}
}
if format == FormatText {
if reportIsPlainText(value) {
switch v := value.(type) {
case []any:
for _, item := range v {
if _, err := fmt.Fprintln(writer, item); err != nil {
return err
}
}
return nil
default:
_, err := fmt.Fprintln(writer, value)
return err
}
}
format = FormatYAML
}
if format == FormatJSON {
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
return encoder.Encode(value)
}
node, err := reportYAMLNode(value)
if err != nil {
return err
}
encoder := yaml.NewEncoder(writer)
encoder.SetIndent(2)
if err := encoder.Encode(node); err != nil {
return err
}
return encoder.Close()
}
func reportIsPlainText(value any) bool {
switch v := value.(type) {
case string, bool, json.Number:
return true
case []any:
if len(v) == 0 {
return false
}
for _, item := range v {
if _, ok := item.(string); !ok {
return false
}
}
return true
default:
return false
}
}
// reportWriter makes short writes observable even for writers that return no error.
type reportWriter struct{ io.Writer }
func (w reportWriter) Write(p []byte) (int, error) {
n, err := w.Writer.Write(p)
if n < len(p) && err == nil {
return n, io.ErrShortWrite
}
return n, err
}