// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.index) > length {
fields = fields[:i]
break
}
if f.tag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
var fieldCache struct {
sync.RWMutex
m map[reflect.Type][]field
}
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
fieldCache.RLock()
f := fieldCache.m[t]
fieldCache.RUnlock()
if f != nil {
return f
}
// Compute fields without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)
if f == nil {
f = []field{}
}
fieldCache.Lock()
if fieldCache.m == nil {
fieldCache.m = map[reflect.Type][]field{}
}
fieldCache.m[t] = f
fieldCache.Unlock()
return f
}
func isValidTag(s string) bool {
if s == "" {
return false
}
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
default:
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
return false
}
}
}
return true
}
const (
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
kelvin = 'K'
smallLongEss = 'ſ'
)
// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
// - S maps to s and to U+017F 'ſ' Latin small letter long s
// - k maps to K and to U+212A 'K' Kelvin sign
//
// See http://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
nonLetter := false
special := false // special letter
for _, b := range s {
if b >= utf8.RuneSelf {
return bytes.EqualFold
}
upper := b & caseMask
if upper < 'A' || upper > 'Z' {
nonLetter = true
} else if upper == 'K' || upper == 'S' {
// See above for why these letters are special.
special = true
}
}
if special {
return equalFoldRight
}
if nonLetter {
return asciiEqualFold
}
return simpleLetterEqualFold
}
// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
for _, sb := range s {
if len(t) == 0 {
return false
}
tb := t[0]
if tb < utf8.RuneSelf {
if sb != tb {
sbUpper := sb & caseMask
if 'A' <= sbUpper && sbUpper <= 'Z' {
if sbUpper != tb&caseMask {
return false
}
} else {
return false
}
}
t = t[1:]
continue
}
// sb is ASCII and t is not. t must be either kelvin
// sign or long s; sb must be s, S, k, or K.
tr, size := utf8.DecodeRune(t)
switch sb {
case 's', 'S':
if tr != smallLongEss {
return false
}
case 'k', 'K':
if tr != kelvin {
return false
}
default:
return false
}
t = t[size:]
}
return len(t) <= 0
}
// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, sb := range s {
tb := t[i]
if sb == tb {
continue
}
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
if sb&caseMask != tb&caseMask {
return false
}
} else {
return false
}
}
return true
}
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, b := range s {
if b&caseMask != t[i]&caseMask {
return false
}
}
return true
}
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}
package kyaml
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
yaml "go.yaml.in/yaml/v3"
)
// Encoder formats objects or YAML data (JSON is valid YAML) into KYAML. KYAML
// is halfway between YAML and JSON, but is a strict subset of YAML, so it
// should should be readable by any YAML parser. It is designed to be explicit
// and unambiguous, and eschews significant whitespace.
type Encoder struct {
// Compact tells the encoder to use compact formatting. This puts all the
// data on one line, with no extra newlines, no comments, and no multi-line
// formatting.
Compact bool
}
// FromYAML renders a KYAML (multi-)document from YAML bytes (JSON is YAML),
// including the KYAML header. The result always has a trailing newline.
func (ky *Encoder) FromYAML(in io.Reader, out io.Writer) error {
// We need a YAML decoder to handle multi-document streams.
dec := yaml.NewDecoder(in)
// Process each document in the stream.
for {
var doc yaml.Node
err := dec.Decode(&doc)
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("error decoding: %v", err)
}
if doc.Kind != yaml.DocumentNode {
return fmt.Errorf("kyaml internal error: line %d: expected a document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
}
// Always emit a document separator, which helps disambiguate between YAML
// and JSON.
if _, err := fmt.Fprintln(out, "---"); err != nil {
return err
}
if err := ky.renderDocument(&doc, 0, ky.flags(), out); err != nil {
return err
}
fmt.Fprintf(out, "
")
}
return nil
}
// FromObject renders a KYAML document from a Go object, including the KYAML
// header. The result always has a trailing newline.
func (ky *Encoder) FromObject(obj any, out io.Writer) error {
jb, err := json.Marshal(obj)
if err != nil {
return fmt.Errorf("error marshaling to JSON: %v", err)
}
// JSON is YAML.
return ky.FromYAML(bytes.NewReader(jb), out)
}
// Marshal renders a single Go object as KYAML, without the header or trailing
// newline.
func (ky *Encoder) Marshal(obj any) ([]byte, error) {
// Convert the object to JSON bytes to take advantage of all the JSON tag
// handling and things like that.
jb, err := json.Marshal(obj)
if err != nil {
return nil, fmt.Errorf("error marshaling to JSON: %v", err)
}
buf := &bytes.Buffer{}
// JSON is YAML.
if err := ky.fromObjectYAML(bytes.NewReader(jb), buf); err != nil {
return nil, fmt.Errorf("error rendering object: %v", err)
}
return buf.Bytes(), nil
}
func (ky *Encoder) fromObjectYAML(in io.Reader, out io.Writer) error {
yb, err := io.ReadAll(in)
if err != nil {
return err
}
var doc yaml.Node
if err := yaml.Unmarshal(yb, &doc); err != nil {
return fmt.Errorf("error decoding: %v", err)
}
if doc.Kind != yaml.DocumentNode {
return fmt.Errorf("kyaml internal error: line %d: expected document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
}
if err := ky.renderNode(&doc, 0, ky.flags(), out); err != nil {
return fmt.Errorf("error rendering document: %v", err)
}
return nil
}
// From the YAML spec.
const (
intTag = "!!int"
floatTag = "!!float"
boolTag = "!!bool"
strTag = "!!str"
timestampTag = "!!timestamp"
seqTag = "!!seq"
mapTag = "!!map"
nullTag = "!!null"
binaryTag = "!!binary"
mergeTag = "!!merge"
)
type flagMask uint64
const (
flagsNone flagMask = 0
flagLazyQuote flagMask = 0x01
flagCompact flagMask = 0x02
)
// flags returns a flagMask representing the current encoding options. It can
// be used directly or OR'ed with another mask.
func (ky *Encoder) flags() flagMask {
flags := flagsNone
if ky.Compact {
flags |= flagCompact
}
return flags
}
// renderNode processes a YAML node, calling the appropriate render function
// for its type. Each render function should assume that the output "cursor"
// is positioned at the start of the node and should not emit a final newline.
// If a render function needs to linewrap or indent (e.g. a struct), it should
// assume the indent level is currently correct for the node type itself, and
// may need to indent more.
func (ky *Encoder) renderNode(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
if node == nil {
return nil
}
switch node.Kind {
case yaml.DocumentNode:
return ky.renderDocument(node, indent, flags, out)
case yaml.ScalarNode:
return ky.renderScalar(node, indent, flags, out)
case yaml.SequenceNode:
return ky.renderSequence(node, indent, flags, out)
case yaml.MappingNode:
return ky.renderMapping(node, indent, flags, out)
case yaml.AliasNode:
return ky.renderAlias(node, indent, flags, out)
}
return fmt.Errorf("kyaml internal error: line %d: unknown node kind %v", node.Line, node.Kind)
}
// renderDocument processes a YAML document node, rendering it to the output.
// This function assumes that the output "cursor" is positioned at the start of
// the document. This does not emit a final newline.
func (ky *Encoder) renderDocument(doc *yaml.Node, indent int, flags flagMask, out io.Writer) error {
if len(doc.Content) == 0 {
return fmt.Errorf("kyaml internal error: line %d: document has no content node (%d)", doc.Line, len(doc.Content))
}
if len(doc.Content) > 1 {
return fmt.Errorf("kyaml internal error: line %d: document has more than one content node (%d)", doc.Line, len(doc.Content))
}
if indent != 0 {
return fmt.Errorf("kyaml internal error: line %d: document non-zero indent (%d)", doc.Line, indent)
}
compact := flags&flagCompact != 0
// For document nodes, the cursor is assumed to be ready to render.
child := doc.Content[0]
if !compact {
if len(doc.HeadComment) > 0 {
ky.renderComments(doc.HeadComment, indent, out)
fmt.Fprint(out, "
")
}
if len(child.HeadComment) > 0 {
ky.renderComments(child.HeadComment, indent, out)
fmt.Fprint(out, "
")
}
}
if err := ky.renderNode(child, indent, flags, out); err != nil {
return err
}
if !compact {
if len(child.LineComment) > 0 {
ky.renderComments(" "+child.LineComment, 0, out)
}
if len(child.FootComment) > 0 {
fmt.Fprint(out, "
")
ky.renderComments(child.FootComment, indent, out)
}
if len(doc.LineComment) > 0 {
fmt.Fprint(out, "
")
ky.renderComments(" "+doc.LineComment, 0, out)
}
if len(doc.FootComment) > 0 {
fmt.Fprint(out, "
")
ky.renderComments(doc.FootComment, indent, out)
}
}
return nil
}
// renderScalar processes a YAML scalar node, rendering it to the output. This
// DOES NOT render a trailing newline or head/line/foot comments, as those
// require the parent context.
func (ky *Encoder) renderScalar(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
switch node.Tag {
case intTag, floatTag, boolTag, nullTag:
fmt.Fprint(out, node.Value)
case strTag, timestampTag:
return ky.renderString(node.Value, indent+1, flags, out)
default:
return fmt.Errorf("kyaml internal error: line %d: unknown tag %q on scalar node %q", node.Line, node.Tag, node.Value)
}
return nil
}
const kyamlFoldStr = "\
"
var regularEscapeMap = map[rune]string{
'
': "\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable
' ': " ", // literal tab
}
var compactEscapeMap = map[rune]string{
'
': "\n",
' ': "\t",
}