@@ -7,13 +7,24 @@ package formatter
77import (
88 "bytes"
99 "fmt"
10+ "strings"
1011
12+ "github.com/Masterminds/semver/v3"
1113 "gopkg.in/yaml.v3"
1214)
1315
14- // YAMLFormatter function is responsible for formatting the given YAML input.
15- // The function is exposed, so it can be used by other internal packages.
16- func YAMLFormatter (content []byte ) ([]byte , bool , error ) {
16+ // YAMLFormatter is responsible for formatting the given YAML input.
17+ type YAMLFormatter struct {
18+ specVersion semver.Version
19+ }
20+
21+ func NewYAMLFormatter (specVersion semver.Version ) * YAMLFormatter {
22+ return & YAMLFormatter {
23+ specVersion : specVersion ,
24+ }
25+ }
26+
27+ func (f * YAMLFormatter ) Format (content []byte ) ([]byte , bool , error ) {
1728 // yaml.Unmarshal() requires `yaml.Node` to be passed instead of generic `interface{}`.
1829 // Otherwise it can't detect any comments and fields are considered as normal map.
1930 var node yaml.Node
@@ -22,6 +33,10 @@ func YAMLFormatter(content []byte) ([]byte, bool, error) {
2233 return nil , false , fmt .Errorf ("unmarshalling YAML file failed: %w" , err )
2334 }
2435
36+ if ! f .specVersion .LessThan (semver .MustParse ("3.0.0" )) {
37+ extendNestedObjects (& node )
38+ }
39+
2540 var b bytes.Buffer
2641 encoder := yaml .NewEncoder (& b )
2742 encoder .SetIndent (2 )
@@ -39,3 +54,75 @@ func YAMLFormatter(content []byte) ([]byte, bool, error) {
3954
4055 return formatted , string (content ) == string (formatted ), nil
4156}
57+
58+ func extendNestedObjects (node * yaml.Node ) {
59+ if node .Kind == yaml .MappingNode {
60+ extendMapNode (node )
61+ }
62+ for _ , child := range node .Content {
63+ extendNestedObjects (child )
64+ }
65+ }
66+
67+ func extendMapNode (node * yaml.Node ) {
68+ for i := 0 ; i < len (node .Content ); i += 2 {
69+ key := node .Content [i ]
70+ value := node .Content [i + 1 ]
71+
72+ base , rest , found := strings .Cut (key .Value , "." )
73+
74+ // Insert nested objects only when the key has a dot, and is not quoted.
75+ if found && key .Style == 0 {
76+ // Copy key to create the new parent with the first part of the path.
77+ newKey := * key
78+ newKey .Value = base
79+ newKey .FootComment = ""
80+ newKey .HeadComment = ""
81+ newKey .LineComment = ""
82+
83+ // Copy key also to create the key of the child value.
84+ newChildKey := * key
85+ newChildKey .Value = rest
86+
87+ // Copy the parent node to create the nested object, that contains the new
88+ // child key and the original value.
89+ newNode := * node
90+ newNode .Content = []* yaml.Node {
91+ & newChildKey ,
92+ value ,
93+ }
94+
95+ // Replace current key and value.
96+ node .Content [i ] = & newKey
97+ node .Content [i + 1 ] = & newNode
98+ }
99+
100+ // Recurse on the current value.
101+ extendNestedObjects (node .Content [i + 1 ])
102+ }
103+
104+ mergeNodes (node )
105+ }
106+
107+ // mergeNodes merges the contents of keys with the same name.
108+ func mergeNodes (node * yaml.Node ) {
109+ keys := make (map [string ]* yaml.Node )
110+ k := 0
111+ for i := 0 ; i < len (node .Content ); i += 2 {
112+ key := node .Content [i ]
113+ value := node .Content [i + 1 ]
114+
115+ merged , found := keys [key .Value ]
116+ if ! found {
117+ keys [key .Value ] = value
118+ node .Content [k ] = key
119+ node .Content [k + 1 ] = value
120+ k += 2
121+ continue
122+ }
123+
124+ merged .Content = append (merged .Content , value .Content ... )
125+ }
126+
127+ node .Content = node .Content [:k ]
128+ }
0 commit comments