Add redacted effective rendering
This commit is contained in:
103
internal/yaml/write.go
Normal file
103
internal/yaml/write.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
var errWrite = errors.New("configkit: effective YAML rendering failed")
|
||||
|
||||
// Write serializes one owned document tree with fully specified stable formatting.
|
||||
func Write(output io.Writer, root *document.Node) error {
|
||||
if output == nil || root == nil {
|
||||
return errWrite
|
||||
}
|
||||
dumper, err := yaml.NewDumper(strictWriter{output},
|
||||
yaml.WithIndent(2),
|
||||
yaml.WithCompactSeqIndent(false),
|
||||
yaml.WithLineWidth(-1),
|
||||
yaml.WithUnicode(true),
|
||||
yaml.WithUniqueKeys(true),
|
||||
yaml.WithCanonical(false),
|
||||
yaml.WithLineBreak(yaml.LineBreakLN),
|
||||
yaml.WithExplicitStart(false),
|
||||
yaml.WithExplicitEnd(false),
|
||||
yaml.WithFlowSimpleCollections(false),
|
||||
yaml.WithQuotePreference(yaml.QuoteSingle),
|
||||
yaml.WithAllDocuments(false),
|
||||
)
|
||||
if err != nil {
|
||||
return errWrite
|
||||
}
|
||||
if err := dumper.Dump(adapterNode(root)); err != nil {
|
||||
return safeWriteError(err)
|
||||
}
|
||||
if err := dumper.Close(); err != nil {
|
||||
return safeWriteError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func adapterNode(node *document.Node) *yaml.Node {
|
||||
result := &yaml.Node{}
|
||||
switch node.Kind {
|
||||
case document.Mapping:
|
||||
result.Kind, result.Tag = yaml.MappingNode, "!!map"
|
||||
result.Content = make([]*yaml.Node, 0, len(node.Mapping)*2)
|
||||
for _, entry := range node.Mapping {
|
||||
result.Content = append(result.Content,
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: entry.Key},
|
||||
adapterNode(entry.Value),
|
||||
)
|
||||
}
|
||||
case document.Sequence:
|
||||
result.Kind, result.Tag = yaml.SequenceNode, "!!seq"
|
||||
result.Content = make([]*yaml.Node, len(node.Sequence))
|
||||
for index, child := range node.Sequence {
|
||||
result.Content[index] = adapterNode(child)
|
||||
}
|
||||
case document.String:
|
||||
result.Kind, result.Tag, result.Value = yaml.ScalarNode, "!!str", node.Text
|
||||
if node.Text == "[REDACTED]" {
|
||||
result.Style = yaml.SingleQuotedStyle
|
||||
}
|
||||
case document.Boolean:
|
||||
result.Kind, result.Tag, result.Value = yaml.ScalarNode, "!!bool", node.Text
|
||||
case document.Number:
|
||||
result.Kind, result.Value = yaml.ScalarNode, node.Text
|
||||
result.Tag = "!!int"
|
||||
if strings.ContainsAny(node.Text, ".eE") {
|
||||
result.Tag = "!!float"
|
||||
}
|
||||
case document.Null:
|
||||
result.Kind, result.Tag, result.Value = yaml.ScalarNode, "!!null", "null"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func safeWriteError(err error) error {
|
||||
var dumpError *yaml.DumpError
|
||||
if errors.As(err, &dumpError) && dumpError.Stage == yaml.WriterStage {
|
||||
if cause := errors.Unwrap(dumpError); cause != nil {
|
||||
return cause
|
||||
}
|
||||
}
|
||||
return errWrite
|
||||
}
|
||||
|
||||
type strictWriter struct {
|
||||
output io.Writer
|
||||
}
|
||||
|
||||
func (writer strictWriter) Write(data []byte) (int, error) {
|
||||
written, err := writer.output.Write(data)
|
||||
if err == nil && written != len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
59
internal/yaml/write_test.go
Normal file
59
internal/yaml/write_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func TestWriteUsesStableExplicitFormatting(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
|
||||
{Key: "empty_map", Value: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{}}},
|
||||
{Key: "empty_sequence", Value: &document.Node{Kind: document.Sequence, Sequence: []*document.Node{}}},
|
||||
{Key: "number", Value: &document.Node{Kind: document.Number, Text: "1.25e+2"}},
|
||||
{Key: "redacted", Value: &document.Node{Kind: document.String, Text: "[REDACTED]"}},
|
||||
{Key: "sequence", Value: &document.Node{Kind: document.Sequence, Sequence: []*document.Node{
|
||||
{Kind: document.String, Text: "first"},
|
||||
{Kind: document.String, Text: "true"},
|
||||
}}},
|
||||
{Key: "nil_value", Value: &document.Node{Kind: document.Null}},
|
||||
}}
|
||||
var output bytes.Buffer
|
||||
if err := Write(&output, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "empty_map: {}\nempty_sequence: []\nnumber: 1.25e+2\nredacted: '[REDACTED]'\nsequence:\n - first\n - 'true'\nnil_value: null\n"
|
||||
if output.String() != want {
|
||||
t.Fatalf("output:\n%s\nwant:\n%s", output.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePropagatesWriterErrorsAndShortWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "value", Value: &document.Node{Kind: document.String, Text: "safe"}}}}
|
||||
want := errors.New("write failed")
|
||||
if err := Write(failingWriter{err: want}, root); !errors.Is(err, want) {
|
||||
t.Fatalf("writer error = %v", err)
|
||||
}
|
||||
if err := Write(shortOutput{}, root); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type failingWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (writer failingWriter) Write(data []byte) (int, error) {
|
||||
return min(2, len(data)), writer.err
|
||||
}
|
||||
|
||||
type shortOutput struct{}
|
||||
|
||||
func (shortOutput) Write(data []byte) (int, error) {
|
||||
return max(0, len(data)-1), nil
|
||||
}
|
||||
Reference in New Issue
Block a user