Skip to content

Commit

Permalink
Refactor report package to be more compatible
Browse files Browse the repository at this point in the history
* Add additional replacer for ('\', 'n') -> "\n"
* New type Formatter embeds Template and writer/tabwriter handling
* tabwriter.Init() is exposed to allow updating the tabwriter settings

Note: If template origin is OriginPodman or has "table" keyword prefix
output will be filtered through tabwriter. Otherwise, output will be
rendered using given writer.

Note: Once all podman commands have been updated a follow on PR will
remove the old report.Template and report.Writer code.

See containers/podman#10974

Signed-off-by: Jhon Honce <[email protected]>
  • Loading branch information
jwhonce committed Nov 19, 2021
1 parent 1373ca3 commit cb82b46
Show file tree
Hide file tree
Showing 5 changed files with 410 additions and 20 deletions.
49 changes: 33 additions & 16 deletions pkg/report/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,65 @@ Package report provides helper structs/methods/funcs for formatting output
To format output for an array of structs:
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
ExamplePodman:
headers := report.Headers(struct {
ID string
}{}, nil)
t, _ := report.NewTemplate("command name").Parse("{{range .}}{{.ID}}{{end}}")
t.Execute(t, headers)
t.Execute(t, map[string]string{
f := report.New(os.Stdout, "Command Name")
f, _ := f.Parse(report.OriginPodman, "{{range .}}{{.ID}}{{end}}")
defer f.Flush()
if f.RenderHeaders {
f.Execute(headers)
}
f.Execute( map[string]string{
"ID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == false
or
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
ExampleUser:
headers := report.Headers(struct {
CID string
}{}, map[string]string{
"CID":"ID"})
t, _ := report.NewTemplate("command name").Parse("table {{.CID}}")
t.Execute(t, headers)
}{}, map[string]string{"CID":"ID"})
f, _ := report.New(os.Stdout, "Command Name").Parse(report.OriginUser, "table {{.CID}}")
defer f.Flush()
if f.RenderHeaders {
t.Execute(t, headers)
}
t.Execute(t,map[string]string{
"CID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == true
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
Helpers:
if report.IsJSON(cmd.Flag("format").Value.String()) {
... process JSON and output
}
if report.HasTable(cmd.Flag("format").Value.String()) {
... "table" keyword prefix in format text
}
Template Functions:
The following template functions are added to the template when parsed:
- join strings.Join, {{join .Field separator}}
- json encode field as JSON {{ json .Field }}
- lower strings.ToLower {{ .Field | lower }}
- pad add spaces as prefix and suffix {{ pad . 2 2 }}
- split strings.Split {{ .Field | split }}
- title strings.Title {{ .Field | title }}
- truncate limit field length {{ truncate . 10 }}
- upper strings.ToUpper {{ .Field | upper }}
report.Funcs() may be used to add additional template functions.
Expand Down
151 changes: 151 additions & 0 deletions pkg/report/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package report

import (
"io"
"strings"
"text/tabwriter"
"text/template"
)

// Flusher is the interface that wraps the Flush method.
type Flusher interface {
Flush() error
}

// NopFlusher represents a type which flush operation is nop.
type NopFlusher struct{}

// Flush is a nop operation.
func (f *NopFlusher) Flush() (err error) { return }

type Origin int

const (
OriginUnknown Origin = iota
OriginPodman
OriginUser
)

func (o Origin) String() string {
switch o {
case OriginPodman:
return "OriginPodman"
case OriginUser:
return "OriginUser"
default:
return "OriginUnknown"
}
}

// Formatter holds the configured Writer and parsed Template, additional state fields are
// maintained to assist in the podman command report writing.
type Formatter struct {
Origin Origin // Source of go template. OriginUser or OriginPodman
RenderHeaders bool // Hint, default behavior for given template is to include headers
RenderTable bool // Does template have "table" keyword
flusher Flusher // Flush any buffered formatted output
template *template.Template // Go text/template for formatting output
text string // value of canonical template after processing
writer io.Writer // Destination for formatted output
}

// Parse parses golang template returning a formatter
//
// - OriginPodman implies text is a template from podman code. Output will
// be filtered through a tabwriter.
//
// - OriginUser implies text is a template from a user. If template includes
// keyword "table" output will be filtered through a tabwriter.
func (f *Formatter) Parse(origin Origin, text string) (*Formatter, error) {
f.Origin = origin

switch {
case strings.HasPrefix(text, "table "):
f.RenderTable = true
text = "{{range .}}" + NormalizeFormat(text) + "{{end -}}"
case OriginUser == origin:
text = EnforceRange(NormalizeFormat(text))
default:
text = NormalizeFormat(text)
}
f.text = text

if f.RenderTable || origin == OriginPodman {
tw := tabwriter.NewWriter(f.writer, 12, 2, 2, ' ', tabwriter.StripEscape)
f.writer = tw
f.flusher = tw
f.RenderHeaders = true
}

tmpl, err := f.template.Funcs(template.FuncMap(DefaultFuncs)).Parse(text)
if err != nil {
return f, err
}
f.template = tmpl
return f, nil
}

// Funcs adds the elements of the argument map to the template's function map.
// A default template function will be replaced if there is a key collision.
func (f *Formatter) Funcs(funcMap template.FuncMap) *Formatter {
m := make(template.FuncMap, len(DefaultFuncs)+len(funcMap))
for k, v := range DefaultFuncs {
m[k] = v
}
for k, v := range funcMap {
m[k] = v
}
f.template = f.template.Funcs(funcMap)
return f
}

// Init either resets the given tabwriter with new values or wraps w in tabwriter with given values
func (f *Formatter) Init(w io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Formatter {
flags |= tabwriter.StripEscape

if tw, ok := f.writer.(*tabwriter.Writer); ok {
tw = tw.Init(w, minwidth, tabwidth, padding, padchar, flags)
f.writer = tw
f.flusher = tw
} else {
tw = tabwriter.NewWriter(w, minwidth, tabwidth, padding, padchar, flags)
f.writer = tw
f.flusher = tw
}
return f
}

// Execute applies a parsed template to the specified data object,
// and writes the output to Formatter.Writer.
func (f *Formatter) Execute(data interface{}) error {
return f.template.Execute(f.writer, data)
}

// Flush should be called after the last call to Write to ensure
// that any data buffered in the Formatter is written to output. Any
// incomplete escape sequence at the end is considered
// complete for formatting purposes.
func (f *Formatter) Flush() error {
// Indirection is required here to prevent caller from having to know when
// value of Flusher may be changed.
return f.flusher.Flush()
}

// Writer returns the embedded io.Writer from Formatter
func (f *Formatter) Writer() io.Writer {
return f.writer
}

// New allocates a new, undefined Formatter with the given name and Writer
func New(output io.Writer, name string) *Formatter {
f := new(Formatter)

f.flusher = new(NopFlusher)
if flusher, ok := output.(Flusher); ok {
f.flusher = flusher
}

f.template = template.New(name)
f.writer = output
return f
}
Loading

0 comments on commit cb82b46

Please sign in to comment.