1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
|
package report
import (
"bytes"
"encoding/json"
"reflect"
"regexp"
"strings"
"text/template"
"github.com/containers/common/pkg/report/camelcase"
)
// Template embeds template.Template to add functionality to methods
type Template struct {
*template.Template
isTable bool
}
// FuncMap is aliased from template.FuncMap
type FuncMap template.FuncMap
// tableReplacer will remove 'table ' prefix and clean up tabs
var tableReplacer = strings.NewReplacer(
"table ", "",
`\t`, "\t",
" ", "\t",
`\n`, "\n",
)
// escapedReplacer will clean up escaped characters from CLI
var escapedReplacer = strings.NewReplacer(
`\t`, "\t",
`\n`, "\n",
)
var DefaultFuncs = FuncMap{
"join": strings.Join,
"json": func(v interface{}) string {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
_ = enc.Encode(v)
// Remove the trailing new line added by the encoder
return strings.TrimSpace(buf.String())
},
"lower": strings.ToLower,
"pad": padWithSpace,
"split": strings.Split,
"title": strings.Title, //nolint:staticcheck
"truncate": truncateWithLength,
"upper": strings.ToUpper,
}
// NormalizeFormat reads given go template format provided by CLI and munges it into what we need
func NormalizeFormat(format string) string {
var f string
// two replacers used so we only remove the prefix keyword `table`
if strings.HasPrefix(format, "table ") {
f = tableReplacer.Replace(format)
} else {
f = escapedReplacer.Replace(format)
}
if !strings.HasSuffix(f, "\n") {
f += "\n"
}
return f
}
// padWithSpace adds spaces*prefix and spaces*suffix to the input when it is non-empty
func padWithSpace(source string, prefix, suffix int) string {
if source == "" {
return source
}
return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
}
// truncateWithLength truncates the source string up to the length provided by the input
func truncateWithLength(source string, length int) string {
if len(source) < length {
return source
}
return source[:length]
}
// Headers queries the interface for field names.
// Array of map is returned to support range templates
// Note: unexported fields can be supported by adding field to overrides
// Note: It is left to the developer to write out said headers
// Podman commands use the general rules of:
// 1) unchanged --format includes headers
// 2) --format '{{.ID}" # no headers
// 3) --format 'table {{.ID}}' # includes headers
func Headers(object interface{}, overrides map[string]string) []map[string]string {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// Column header will be field name upper-cased.
headers := make(map[string]string, value.NumField())
for i := 0; i < value.Type().NumField(); i++ {
field := value.Type().Field(i)
// Recurse to find field names from promoted structs
if field.Type.Kind() == reflect.Struct && field.Anonymous {
h := Headers(reflect.New(field.Type).Interface(), nil)
for k, v := range h[0] {
headers[k] = v
}
continue
}
name := strings.Join(camelcase.Split(field.Name), " ")
headers[field.Name] = strings.ToUpper(name)
}
if len(overrides) > 0 {
// Override column header as provided
for k, v := range overrides {
headers[k] = strings.ToUpper(v)
}
}
return []map[string]string{headers}
}
// NewTemplate creates a new template object
func NewTemplate(name string) *Template {
return &Template{Template: template.New(name).Funcs(template.FuncMap(DefaultFuncs))}
}
// Parse parses text as a template body for t
func (t *Template) Parse(text string) (*Template, error) {
if strings.HasPrefix(text, "table ") {
t.isTable = true
text = "{{range .}}" + NormalizeFormat(text) + "{{end -}}"
} else {
text = NormalizeFormat(text)
}
tt, err := t.Template.Funcs(template.FuncMap(DefaultFuncs)).Parse(text)
return &Template{tt, t.isTable}, err
}
// Funcs adds the elements of the argument map to the template's function map.
// A default template function will be replace if there is a key collision.
func (t *Template) Funcs(funcMap FuncMap) *Template {
m := make(FuncMap)
for k, v := range DefaultFuncs {
m[k] = v
}
for k, v := range funcMap {
m[k] = v
}
return &Template{Template: t.Template.Funcs(template.FuncMap(m)), isTable: t.isTable}
}
// IsTable returns true if format string defines a "table"
func (t *Template) IsTable() bool {
return t.isTable
}
var rangeRegex = regexp.MustCompile(`(?s){{\s*range\s*\.\s*}}.*{{\s*end\s*-?\s*}}`)
// EnforceRange ensures that the format string contains a range
func EnforceRange(format string) string {
if !rangeRegex.MatchString(format) {
return "{{range .}}" + format + "{{end -}}"
}
return format
}
// HasTable returns whether the format is a table
func HasTable(format string) bool {
return strings.HasPrefix(format, "table ")
}
|