Separate explanation replay snapshots and rendering
This commit is contained in:
123
explain_render.go
Normal file
123
explain_render.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errExplanationUnavailable = errors.New("configkit: configuration explanation is unavailable")
|
||||
errExplanationUnknownPath = errors.New("configkit: configuration explanation path is unknown")
|
||||
)
|
||||
|
||||
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
||||
// Unknown paths produce no output. Writer failures may leave partial output.
|
||||
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||
if r == nil || output == nil {
|
||||
return errExplanationUnavailable
|
||||
}
|
||||
explanation, ok := r.Explain(path)
|
||||
if !ok {
|
||||
return errExplanationUnknownPath
|
||||
}
|
||||
var rendered strings.Builder
|
||||
rendered.WriteString("path: ")
|
||||
rendered.WriteString(strconv.Quote(explanation.Path.String()))
|
||||
rendered.WriteByte('\n')
|
||||
writeExplanationOrigin(&rendered, "effective source", explanation.Origin, r.sources)
|
||||
writeExplanationSnapshot(&rendered, "effective (canonical typed)", explanation.Effective, 0)
|
||||
if explanation.SecretReference != nil {
|
||||
rendered.WriteString("secret reference: ")
|
||||
rendered.WriteString(strconv.Quote(explanation.SecretReference.Name))
|
||||
rendered.WriteString(" (resolved from process environment)\n")
|
||||
}
|
||||
rendered.WriteString("history:\n")
|
||||
for _, change := range explanation.History {
|
||||
rendered.WriteString(" - representation: ")
|
||||
if change.Origin.Operation == ChangeDecoding {
|
||||
rendered.WriteString("input to canonical typed\n")
|
||||
} else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerProfile || change.Origin.Layer == LayerCLI {
|
||||
rendered.WriteString("policy-validated input\n")
|
||||
} else {
|
||||
rendered.WriteString("canonical typed\n")
|
||||
}
|
||||
writeExplanationOrigin(&rendered, " source", change.Origin, r.sources)
|
||||
writeExplanationSnapshot(&rendered, " before", change.Before, 4)
|
||||
writeExplanationSnapshot(&rendered, " after", change.After, 4)
|
||||
for _, reason := range change.Reasons {
|
||||
rendered.WriteString(" reason: ")
|
||||
rendered.WriteString(strconv.Quote(reason))
|
||||
rendered.WriteByte('\n')
|
||||
}
|
||||
for _, dependency := range change.Dependencies {
|
||||
rendered.WriteString(" dependency: ")
|
||||
rendered.WriteString(strconv.Quote(dependency.String()))
|
||||
rendered.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
data := []byte(rendered.String())
|
||||
written, err := output.Write(data)
|
||||
if err == nil && written != len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func writeExplanationOrigin(output *strings.Builder, label string, origin Origin, sources []SourceInfo) {
|
||||
output.WriteString(label)
|
||||
output.WriteString(": layer=")
|
||||
output.WriteString(strconv.Quote(string(origin.Layer)))
|
||||
if origin.Operation != "" {
|
||||
output.WriteString(" operation=")
|
||||
output.WriteString(strconv.Quote(string(origin.Operation)))
|
||||
}
|
||||
output.WriteByte('\n')
|
||||
for _, location := range origin.Locations {
|
||||
output.WriteString(" location: source=")
|
||||
output.WriteString(strconv.Quote(location.SourceID))
|
||||
if label := sourceLabel(sources, location.SourceID); label != "" {
|
||||
output.WriteString(" label=")
|
||||
output.WriteString(strconv.Quote(label))
|
||||
}
|
||||
output.WriteString(" line=")
|
||||
output.WriteString(strconv.Itoa(location.Line))
|
||||
output.WriteString(" column=")
|
||||
output.WriteString(strconv.Itoa(location.Column))
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func sourceLabel(sources []SourceInfo, sourceID string) string {
|
||||
for _, source := range sources {
|
||||
if source.ID == sourceID {
|
||||
return source.Label
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) {
|
||||
if !snapshot.Present {
|
||||
output.WriteString(label)
|
||||
output.WriteString(": absent\n")
|
||||
return
|
||||
}
|
||||
output.WriteString(label)
|
||||
if snapshot.Redacted {
|
||||
output.WriteString(" (redacted)")
|
||||
}
|
||||
output.WriteString(":\n")
|
||||
prefix := strings.Repeat(" ", indent+2)
|
||||
for _, line := range strings.SplitAfter(snapshot.YAML, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
output.WriteString(prefix)
|
||||
output.WriteString(line)
|
||||
if !strings.HasSuffix(line, "\n") {
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user