Add shared report rendering
This commit is contained in:
121
report.go
Normal file
121
report.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user