Dmitriy Matrenichev e26d0043e0
chore: code cleanup
More usage of slices package, less usage of package sort.

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
2024-11-14 12:25:56 +03:00

366 lines
7.5 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package encoder
import (
"cmp"
"encoding"
"reflect"
"slices"
"strings"
yaml "gopkg.in/yaml.v3"
)
// Encoder implements config encoder.
type Encoder struct {
value any
options *Options
}
// NewEncoder initializes and returns an `Encoder`.
func NewEncoder(value any, opts ...Option) *Encoder {
return &Encoder{
value: value,
options: newOptions(opts...),
}
}
// Marshal converts value to YAML-serializable value (suitable for MarshalYAML).
func (e *Encoder) Marshal() (*yaml.Node, error) {
node, err := toYamlNode(e.value, e.options)
if err != nil {
return nil, err
}
if e.options.Comments.enabled(CommentsDocs) {
addComments(node, getDoc(e.value), HeadComment, LineComment)
}
return node, nil
}
// Encode converts value to yaml.
//
//nolint:gocyclo
func (e *Encoder) Encode() ([]byte, error) {
if e.options.Comments == CommentsDisabled {
return yaml.Marshal(e.value)
}
node, err := e.Marshal()
if err != nil {
return nil, err
}
// special handling for case when we get an empty output
if node.Kind == yaml.MappingNode && len(node.Content) == 0 && node.FootComment != "" && e.options.Comments.enabled(CommentsExamples) {
res := ""
if node.HeadComment != "" {
res += node.HeadComment + "\n"
}
lines := strings.Split(res+node.FootComment, "\n")
for i, line := range lines {
if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
continue
}
lines[i] = "# " + line
}
return []byte(strings.Join(lines, "\n")), nil
}
return yaml.Marshal(node)
}
func isEmpty(value reflect.Value) bool {
if !value.IsValid() {
return true
}
//nolint:exhaustive
switch value.Kind() {
case reflect.Ptr:
return value.IsNil()
case reflect.Map:
return len(value.MapKeys()) == 0
case reflect.Slice:
return value.Len() == 0
default:
return value.IsZero()
}
}
func isNil(value reflect.Value) bool {
if !value.IsValid() {
return true
}
//nolint:exhaustive
switch value.Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice:
return value.IsNil()
default:
return false
}
}
//nolint:gocyclo,cyclop
func toYamlNode(in any, options *Options) (*yaml.Node, error) {
node := &yaml.Node{}
flags := options.Comments
// do not wrap yaml.Node into yaml.Node
if n, ok := in.(*yaml.Node); ok {
return n, nil
}
// if input implements yaml.Marshaler we should use that marshaller instead
// same way as regular yaml marshal does
if m, ok := in.(yaml.Marshaler); ok && !isNil(reflect.ValueOf(in)) {
res, err := m.MarshalYAML()
if err != nil {
return nil, err
}
if n, ok := res.(*yaml.Node); ok {
return n, nil
}
in = res
}
if _, ok := in.(encoding.TextMarshaler); ok && !isNil(reflect.ValueOf(in)) {
return node, node.Encode(in)
}
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
doc := getDoc(in)
//nolint:exhaustive
switch v.Kind() {
case reflect.Struct:
node.Kind = yaml.MappingNode
t := v.Type()
var examples []string
for i := range v.NumField() {
// skip unexported fields
if !v.Field(i).CanInterface() {
continue
}
tag := t.Field(i).Tag.Get("yaml")
parts := strings.Split(tag, ",")
fieldName := parts[0]
parts = parts[1:]
tag = t.Field(i).Tag.Get("talos")
if tag != "" {
parts = append(parts, strings.Split(tag, ",")...)
}
if fieldName == "" {
fieldName = strings.ToLower(t.Field(i).Name)
}
if fieldName == "-" {
continue
}
var (
empty = isEmpty(v.Field(i))
null = isNil(v.Field(i))
skip bool
inline bool
flow bool
)
for _, part := range parts {
if part == "omitempty" && empty && options.OmitEmpty {
skip = true
}
if part == "omitonlyifnil" && !null {
skip = false
}
if part == "inline" {
inline = true
}
if part == "flow" {
flow = true
}
}
var value any
if v.Field(i).CanInterface() {
value = v.Field(i).Interface()
}
// get documentation data either from field, or from type
var fieldDoc *Doc
if doc != nil {
fieldDoc = mergeDoc(getDoc(value), doc.Field(i))
} else {
fieldDoc = getDoc(value)
}
// inlineExample is rendered after the value
var inlineExample string
if empty && flags.enabled(CommentsExamples) && fieldDoc != nil {
if skip {
// render example to be appended to the end of the rendered struct
example := renderExample(fieldName, fieldDoc, options)
if example != "" {
examples = append(examples, example)
}
} else {
// render example to be appended to the empty field
fieldDocCopy := *fieldDoc
fieldDocCopy.Comments = [3]string{}
inlineExample = renderExample("", &fieldDocCopy, options)
}
}
if skip {
continue
}
var style yaml.Style
if flow {
style |= yaml.FlowStyle
}
if inline {
child, err := toYamlNode(value, options)
if err != nil {
return nil, err
}
if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode {
appendNodes(node, child.Content...)
}
} else if err := addToMap(node, fieldDoc, fieldName, value, style, options); err != nil {
return nil, err
}
if inlineExample != "" {
nodeToAttach := node.Content[len(node.Content)-1]
if nodeToAttach.FootComment != "" {
nodeToAttach.FootComment += "\n"
}
nodeToAttach.FootComment += inlineExample
}
}
if len(examples) > 0 {
comment := strings.Join(examples, "\n")
// add rendered example to the foot comment of the last node
// or to the foot comment of parent node
if len(node.Content) > 0 {
node.Content[len(node.Content)-2].FootComment += "\n" + comment
} else {
node.FootComment += comment
}
}
case reflect.Map:
node.Kind = yaml.MappingNode
keys := v.MapKeys()
// always interate keys in alphabetical order to preserve the same output for maps
slices.SortFunc(keys, func(a, b reflect.Value) int { return cmp.Compare(a.String(), b.String()) })
for _, k := range keys {
element := v.MapIndex(k)
value := element.Interface()
if err := addToMap(node, nil, k.Interface(), value, 0, options); err != nil {
return nil, err
}
}
case reflect.Slice:
node.Kind = yaml.SequenceNode
nodes := make([]*yaml.Node, v.Len())
for i := range v.Len() {
element := v.Index(i)
var err error
nodes[i], err = toYamlNode(element.Interface(), options)
if err != nil {
return nil, err
}
}
appendNodes(node, nodes...)
default:
if err := node.Encode(in); err != nil {
return nil, err
}
}
return node, nil
}
func appendNodes(dest *yaml.Node, nodes ...*yaml.Node) {
if dest.Content == nil {
dest.Content = []*yaml.Node{}
}
dest.Content = append(dest.Content, nodes...)
}
func addToMap(dest *yaml.Node, doc *Doc, fieldName, in any, style yaml.Style, options *Options) error {
key, err := toYamlNode(fieldName, options)
if err != nil {
return err
}
value, err := toYamlNode(in, options)
if err != nil {
return err
}
value.Style = style
if options.Comments.enabled(CommentsDocs) {
addComments(key, doc, HeadComment, FootComment)
addComments(value, doc, LineComment)
}
// override head comment with line comment for non-scalar nodes
if value.Kind != yaml.ScalarNode {
if key.HeadComment == "" {
key.HeadComment = value.LineComment
}
value.LineComment = ""
}
appendNodes(dest, key, value)
return nil
}