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 }