Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 150 additions & 97 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,17 @@ func (e *Encoder) Encode() ([]byte, error) {
}

// special handling for case when we get an empty output
if node.Kind == yaml.MappingNode && len(node.Content) == 0 && node.FootComment != "" && e.options.CommentsFlag.enabled() {
if handled, res, err := handleEmptyNodeComment(node, e.options); handled {
return res, err
}

return yaml.Marshal(node)
}

// handleEmptyNodeComment handles the special case for empty mapping nodes with foot comments.
// It returns true if the case was handled, along with the marshalled bytes and any error.
func handleEmptyNodeComment(node *yaml.Node, options *Options) (bool, []byte, error) {
if node.Kind == yaml.MappingNode && len(node.Content) == 0 && node.FootComment != "" && options.CommentsFlag.enabled() {
res := ""

if node.HeadComment != "" {
Expand All @@ -121,10 +131,9 @@ func (e *Encoder) Encode() ([]byte, error) {
lines[i] = "# " + line
}

return []byte(strings.Join(lines, "\n")), nil
return true, []byte(strings.Join(lines, "\n")), nil
}

return yaml.Marshal(node)
return false, nil, nil
}

func isEmpty(value reflect.Value) bool {
Expand Down Expand Up @@ -195,114 +204,153 @@ func toYamlNode(in interface{}, options *Options) (*yaml.Node, error) {
case reflect.Struct:
node.Kind = yaml.MappingNode

node.Kind = yaml.MappingNode
t := v.Type()

for i := 0; i < v.NumField(); i++ {
// skip unexported fields
if !v.Field(i).CanInterface() {
continue
if err := processStructField(node, v, t, i, options); err != nil {
return nil, err
}
}
case reflect.Map:
node.Kind = yaml.MappingNode
if err := processMap(v, node, options); err != nil {
return nil, err
}
case reflect.Slice:
node.Kind = yaml.SequenceNode
if err := processSlice(v, node, options); err != nil {
return nil, err
}
default:
if err := node.Encode(in); err != nil {
return nil, err
}
}

comment := t.Field(i).Tag.Get(options.CommentTagName)
tag := t.Field(i).Tag.Get("yaml")
parts := strings.Split(tag, ",")
fieldName := parts[0]
parts = parts[1:]

if fieldName == "" {
fieldName = strings.ToLower(t.Field(i).Name)
}
return node, nil
}

if fieldName == "-" {
continue
}
// processStructField handles processing of a single struct field.
func processStructField(node *yaml.Node, v reflect.Value, t reflect.Type, i int, options *Options) error {
// skip unexported fields
if !v.Field(i).CanInterface() {
return nil
}

var (
empty = isEmpty(v.Field(i))
skip bool
inline bool
flow bool
)

for _, part := range parts {
if part == "omitempty" && empty && options.OmitEmpty {
skip = true
}

if part == "inline" {
inline = true
}

if part == "flow" {
flow = true
}
}
field := t.Field(i)
comment := field.Tag.Get(options.CommentTagName)
tag := field.Tag.Get("yaml")
parts := strings.Split(tag, ",")
fieldName := parts[0]
parts = parts[1:]

var value interface{}
if v.Field(i).CanInterface() {
value = v.Field(i).Interface()
}
if fieldName == "" {
fieldName = strings.ToLower(field.Name)
}

if skip {
continue
}
if fieldName == "-" {
return nil
}

var style yaml.Style
if flow {
style |= yaml.FlowStyle
}
var (
fieldIsEmpty = isEmpty(v.Field(i)) // Determine if field is empty
skipField = false // Default to not skipping the field
inline = false
flow = false
)

// Tag parsing logic
hasOmitemptyTagInStruct := false
for _, part := range parts {
switch part {
case "omitempty":
hasOmitemptyTagInStruct = true
case "inline":
inline = true
case "flow":
flow = true
}
}

if inline {
child, err := toYamlNode(value, options)
if err != nil {
return nil, err
}
// Decision logic for skipping the field
performSkip := false // Initialize

if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode {
appendNodes(node, child.Content...)
}
} else if err := addToMap(node, fieldName, value, comment, style, options); err != nil {
return nil, err
if fieldName == "-" {
performSkip = true // Always skip if field name is "-"
} else {
// If options.OmitEmpty is true, then standard omitempty logic applies
if options.OmitEmpty {
if hasOmitemptyTagInStruct && fieldIsEmpty {
performSkip = true
}
}
case reflect.Map:
node.Kind = yaml.MappingNode
keys := v.MapKeys()
// always interate keys in alphabetical order to preserve the same output for maps
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
// If options.OmitEmpty is false, performSkip remains false (unless fieldName was "-"),
// meaning the omitempty tag is completely ignored because the condition
// `hasOmitemptyTagInStruct && fieldIsEmpty` is only checked if options.OmitEmpty is true.
}

for _, k := range keys {
element := v.MapIndex(k)
value := element.Interface()
// This is the crucial assignment
skipField = performSkip

if err := addToMap(node, k.Interface(), value, "", 0, options); err != nil {
return nil, err
}
}
case reflect.Slice:
node.Kind = yaml.SequenceNode
nodes := make([]*yaml.Node, v.Len())
var value interface{}
if v.Field(i).CanInterface() {
value = v.Field(i).Interface()
}

for i := 0; i < v.Len(); i++ {
element := v.Index(i)
if skipField {
return nil
}

var err error
var style yaml.Style
if flow {
style |= yaml.FlowStyle
}

nodes[i], err = toYamlNode(element.Interface(), options)
if err != nil {
return nil, err
}
if inline {
child, err := toYamlNode(value, options)
if err != nil {
return err
}
appendNodes(node, nodes...)
default:
if err := node.Encode(in); err != nil {
return nil, err

if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode {
appendNodes(node, child.Content...)
}
} else if err := addToMap(node, fieldName, value, comment, style, options); err != nil {
return err
}
return nil
}

return node, nil
func processMap(v reflect.Value, node *yaml.Node, options *Options) error {
keys := v.MapKeys()
// always interate keys in alphabetical order to preserve the same output for maps
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})

for _, k := range keys {
element := v.MapIndex(k)
value := element.Interface()

if err := addToMap(node, k.Interface(), value, "", 0, options); err != nil {
return err
}
}
return nil
}

func processSlice(v reflect.Value, node *yaml.Node, options *Options) error {
nodes := make([]*yaml.Node, v.Len())
for i := 0; i < v.Len(); i++ {
element := v.Index(i)
var err error
nodes[i], err = toYamlNode(element.Interface(), options)
if err != nil {
return err
}
}
appendNodes(node, nodes...)
return nil
}

func appendNodes(dest *yaml.Node, nodes ...*yaml.Node) {
Expand Down Expand Up @@ -334,12 +382,17 @@ func addToMap(dest *yaml.Node, fieldName, in interface{}, fieldComent string, st
}

func addComment(node *yaml.Node, comment string, flag CommentsFlag) {
if flag.enabled() {
dest := []*string{
&node.HeadComment,
&node.LineComment,
&node.FootComment,
}
*dest[int(flag)-1] = comment
if !flag.enabled() || comment == "" { // Also explicitly skip if comment is empty
return
}

switch flag {
case CommentsOnHead:
node.HeadComment = comment
case CommentsInLine:
node.LineComment = comment
case CommentsOnFoot:
node.FootComment = comment
// No default needed as flag.enabled() already filters for valid comment placement flags.
}
}
Loading