prettyprinters/human/human.go
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package human
import (
"bytes"
"fmt"
"strings"
"sync"
"text/tabwriter"
"text/template"
"github.com/asteris-llc/converge/graph"
pp "github.com/asteris-llc/converge/prettyprinters"
"github.com/pkg/errors"
)
// Printer for human-readable output
type Printer struct {
Color bool // color output
Filter FilterFunc
}
var (
funcs = map[string]interface{}{}
funcsMu sync.Mutex
)
// New returns a base version of Printer
func New() *Printer {
return NewFiltered(ShowEverything)
}
// NewFiltered returns a version of Printer that will filter according to the
// specified func
func NewFiltered(f FilterFunc) *Printer {
return &Printer{Filter: f}
}
// InitColors initializes the colors used by the human printer
func (p *Printer) InitColors() {
reset := "\x1b[0m"
p.funcsMapWrite("bold", p.styled(func(in string) string { return "\x1b[1m" + in + reset }))
p.funcsMapWrite("black", p.styled(func(in string) string { return "\x1b[30m" + in + reset }))
p.funcsMapWrite("red", p.styled(func(in string) string { return "\x1b[31m" + in + reset }))
p.funcsMapWrite("green", p.styled(func(in string) string { return "\x1b[32m" + in + reset }))
p.funcsMapWrite("yellow", p.styled(func(in string) string { return "\x1b[33m" + in + reset }))
p.funcsMapWrite("blue", p.styled(func(in string) string { return "\x1b[34m" + in + reset }))
p.funcsMapWrite("magenta", p.styled(func(in string) string { return "\x1b[35m" + in + reset }))
p.funcsMapWrite("cyan", p.styled(func(in string) string { return "\x1b[36m" + in + reset }))
p.funcsMapWrite("white", p.styled(func(in string) string { return "\x1b[37m" + in + reset }))
}
// StartPP does nothing, but is required to satisfy the GraphPrinter interface
func (p *Printer) StartPP(g *graph.Graph) (pp.Renderable, error) {
return pp.HiddenString(), nil
}
// FinishPP provides summary statistics about the printed graph
func (p *Printer) FinishPP(g *graph.Graph) (pp.Renderable, error) {
tmpl, err := p.template(`{{if .Errors}}Errors:
{{range .Errors}} * {{.}}
{{end}}
{{end}}
{{- if .DependencyErrors}}Failed due to failing dependency:
{{range .DependencyErrors}} * {{.}}
{{end}}
{{end}}
{{- if gt (len .Errors) 0}}{{red "Summary"}}
{{- else}}{{green "Summary"}}
{{- end}}: {{len .Errors}} errors, {{.ChangesCount}} changes
{{- if .DependencyErrors}}, {{len .DependencyErrors}} dependency errors
{{- end}}
`)
if err != nil {
return pp.HiddenString(), err
}
counts := struct {
ChangesCount int
Errors []error
DependencyErrors []error
}{}
for _, id := range g.Vertices() {
meta, ok := g.Get(id)
if !ok {
continue
}
printable, ok := meta.Value().(Printable)
if !ok {
continue
}
if err = printable.Error(); err != nil {
if id != "root" {
if strings.Contains(err.Error(), "error in dependency") {
counts.DependencyErrors = append(
counts.DependencyErrors,
errors.Wrap(err, id),
)
} else {
counts.Errors = append(
counts.Errors,
errors.Wrap(err, id),
)
}
}
} else if printable.HasChanges() && id != "root" {
counts.ChangesCount++
}
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, counts)
return &buf, err
}
// DrawNode containing a result
func (p *Printer) DrawNode(g *graph.Graph, id string) (pp.Renderable, error) {
meta, ok := g.Get(id)
if !ok {
return pp.HiddenString(), nil
}
printable, ok := meta.Value().(Printable)
if !ok {
return pp.HiddenString(), errors.New("cannot print values that don't implement Printable")
}
if !p.Filter(id, printable) {
return pp.HiddenString(), nil
}
tmpl, err := p.template(`{{if .Error}}{{red .ID}}{{else if .HasChanges}}{{yellow .ID}}{{else}}{{.ID}}{{end}}:
{{- if .Error}}
{{red "Error"}}: {{.Error}}
{{- end}}
{{- if .Warning}}
{{yellow "Warning"}}: {{.Warning}}
{{- end}}
Messages:
{{- range $msg := .Messages}}
{{indent $msg}}
{{- end}}
Has Changes: {{if .HasChanges}}{{yellow "yes"}}{{else}}no{{end}}
Changes:
{{- range $key, $values := .Changes}}
{{cyan $key}}: {{diff ($values.Original) ($values.Current)}}
{{- else}} No changes {{- end}}
`)
if err != nil {
return pp.HiddenString(), err
}
var intermediate, out bytes.Buffer
err = tmpl.Execute(&intermediate, &printerNode{ID: id, Printable: printable})
if err != nil {
return pp.HiddenString(), err
}
tabWriter := tabwriter.NewWriter(&out, 1, 1, 1, ' ', 0)
_, err = tabWriter.Write(intermediate.Bytes())
return &out, err
}
func (p *Printer) getFunc(key string) func(string) string {
funcsMu.Lock()
defer funcsMu.Unlock()
return funcs[key].(func(string) string)
}
func (p *Printer) funcsMapWrite(key string, value interface{}) {
funcsMu.Lock()
defer funcsMu.Unlock()
funcs[key] = value
}
func (p *Printer) template(source string) (*template.Template, error) {
p.funcsMapWrite("diff", p.diff)
p.funcsMapWrite("indent", p.indent)
p.funcsMapWrite("empty", p.empty)
funcsMu.Lock()
defer funcsMu.Unlock()
return template.New("").Funcs(funcs).Parse(source)
}
func (p *Printer) styled(style func(string) string) func(string) string {
if !p.Color {
return func(in string) string { return in }
}
return style
}
func (p *Printer) diff(before, after string) (string, error) {
// remember when modifying these that diff is responsible for leading
// whitespace
if !strings.Contains(strings.TrimSpace(before), "\n") && !strings.Contains(strings.TrimSpace(after), "\n") {
return p.getFunc("bold")(
fmt.Sprintf("%q\t=>\t%q", strings.TrimSpace(before), strings.TrimSpace(after)),
), nil
}
tmpl, err := p.template(`before:
{{.Before}}
after:
{{.After}}`)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, struct{ Before, After string }{before, after})
return "\n" + p.indent(p.indent(buf.String())), err
}
func (p *Printer) indent(in string) string {
return "\t" + strings.Replace(in, "\n", "\n\t", -1)
}
func (p *Printer) empty(s string) bool {
return s == ""
}