122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
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
|
|
}
|