internal/formatter/yaml.go (94 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package formatter import ( "bytes" "fmt" "strings" "gopkg.in/yaml.v3" ) // YAMLFormatter is responsible for formatting the given YAML input. type YAMLFormatter struct { keysWithDotsAction int } func NewYAMLFormatter(keysWithDotsAction int) *YAMLFormatter { return &YAMLFormatter{ keysWithDotsAction: keysWithDotsAction, } } func (f *YAMLFormatter) Format(content []byte) ([]byte, bool, error) { // yaml.Unmarshal() requires `yaml.Node` to be passed instead of generic `interface{}`. // Otherwise it can't detect any comments and fields are considered as normal map. var node yaml.Node err := yaml.Unmarshal(content, &node) if err != nil { return nil, false, fmt.Errorf("unmarshalling YAML file failed: %w", err) } applyActionOnKeysWithDots(&node, f.keysWithDotsAction) var b bytes.Buffer encoder := yaml.NewEncoder(&b) encoder.SetIndent(2) err = encoder.Encode(&node) if err != nil { return nil, false, fmt.Errorf("marshalling YAML node failed: %w", err) } formatted := b.Bytes() prefix := []byte("---\n") // required to preserve yaml files starting with "---" as yaml.Encoding strips them if bytes.HasPrefix(content, prefix) && !bytes.HasPrefix(formatted, prefix) { formatted = append(prefix, formatted...) } return formatted, string(content) == string(formatted), nil } func applyActionOnKeysWithDots(node *yaml.Node, action int) { switch action { case KeysWithDotActionNested: extendNestedObjects(node) case KeysWithDotActionNone: // Nothing to do. } } func extendNestedObjects(node *yaml.Node) { if node.Kind == yaml.MappingNode { extendMapNode(node) } for _, child := range node.Content { extendNestedObjects(child) } } func extendMapNode(node *yaml.Node) { for i := 0; i < len(node.Content); i += 2 { key := node.Content[i] value := node.Content[i+1] base, rest, found := strings.Cut(key.Value, ".") // Insert nested objects only when the key has a dot, and is not quoted. if found && key.Style == 0 { // Copy key to create the new parent with the first part of the path. newKey := *key newKey.Value = base newKey.FootComment = "" newKey.HeadComment = "" newKey.LineComment = "" // Copy key also to create the key of the child value. newChildKey := *key newChildKey.Value = rest // Copy the parent node to create the nested object, that contains the new // child key and the original value. newNode := *node newNode.Content = []*yaml.Node{ &newChildKey, value, } // Replace current key and value. node.Content[i] = &newKey node.Content[i+1] = &newNode } // Recurse on the current value. extendNestedObjects(node.Content[i+1]) } mergeNodes(node) } // mergeNodes merges the contents of keys with the same name. func mergeNodes(node *yaml.Node) { keys := make(map[string]*yaml.Node) k := 0 for i := 0; i < len(node.Content); i += 2 { key := node.Content[i] value := node.Content[i+1] merged, found := keys[key.Value] if !found { keys[key.Value] = value node.Content[k] = key node.Content[k+1] = value k += 2 continue } merged.Content = append(merged.Content, value.Content...) } node.Content = node.Content[:k] }