Skip to content

Commit 53a0a13

Browse files
committed
Adopt goccy/go-yaml
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent 6f5d914 commit 53a0a13

File tree

22 files changed

+206
-202
lines changed

22 files changed

+206
-202
lines changed

cli/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"strings"
2626

2727
"github.com/sirupsen/logrus"
28-
"gopkg.in/yaml.v3"
28+
"github.com/goccy/go-yaml"
2929

3030
"github.com/compose-spec/compose-go/v2/consts"
3131
"github.com/compose-spec/compose-go/v2/dotenv"

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"os"
2525

2626
"github.com/compose-spec/compose-go/v2/cli"
27-
"gopkg.in/yaml.v3"
27+
"github.com/goccy/go-yaml"
2828
)
2929

3030
func main() {

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/docker/go-connections v0.4.0
88
github.com/docker/go-units v0.5.0
99
github.com/go-viper/mapstructure/v2 v2.0.0
10+
github.com/goccy/go-yaml v1.17.1
1011
github.com/google/go-cmp v0.5.9
1112
github.com/mattn/go-shellwords v1.0.12
1213
github.com/opencontainers/go-digest v1.0.0
@@ -15,7 +16,6 @@ require (
1516
github.com/xeipuuv/gojsonschema v1.2.0
1617
github.com/xhit/go-str2duration/v2 v2.1.0
1718
golang.org/x/sync v0.3.0
18-
gopkg.in/yaml.v3 v3.0.1
1919
gotest.tools/v3 v3.4.0
2020
)
2121

@@ -25,4 +25,5 @@ require (
2525
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
2626
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
2727
golang.org/x/sys v0.1.0 // indirect
28+
gopkg.in/yaml.v3 v3.0.1 // indirect
2829
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
99
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
1010
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
1111
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
12+
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
13+
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
1214
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1315
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1416
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

loader/loader.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ import (
4242
"github.com/compose-spec/compose-go/v2/types"
4343
"github.com/compose-spec/compose-go/v2/validation"
4444
"github.com/go-viper/mapstructure/v2"
45+
"github.com/goccy/go-yaml"
46+
"github.com/goccy/go-yaml/parser"
4547
"github.com/sirupsen/logrus"
46-
"gopkg.in/yaml.v3"
4748
)
4849

4950
// Options supported by Load
@@ -260,8 +261,6 @@ func WithProfiles(profiles []string) func(*Options) {
260261
// PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase
261262
// that hardly can be implemented using go-yaml and mapstructure
262263
type PostProcessor interface {
263-
yaml.Unmarshaler
264-
265264
// Apply changes to compose model based on recorder metadata
266265
Apply(interface{}) error
267266
}
@@ -501,17 +500,25 @@ func loadYamlFile(ctx context.Context,
501500
if file.Config == nil {
502501
r := bytes.NewReader(file.Content)
503502
decoder := yaml.NewDecoder(r)
504-
for {
505-
var raw interface{}
506-
reset := &ResetProcessor{target: &raw}
507-
err := decoder.Decode(reset)
508-
if err != nil && errors.Is(err, io.EOF) {
509-
break
503+
all, err := io.ReadAll(r)
504+
if err != nil {
505+
return nil, nil, err
506+
}
507+
y, err := parser.ParseBytes(all, parser.ParseComments)
508+
if err != nil {
509+
return nil, nil, err
510+
}
511+
for _, doc := range y.Docs {
512+
if doc.Body == nil {
513+
continue // empty file
510514
}
515+
processor = NewResetProcessor(doc)
516+
517+
var raw interface{}
518+
err := decoder.DecodeFromNodeContext(ctx, doc.Body, &raw)
511519
if err != nil {
512520
return nil, nil, err
513521
}
514-
processor = reset
515522
if err := processRawYaml(raw, processor); err != nil {
516523
return nil, nil, err
517524
}

loader/loader_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ func TestNonStringKeys(t *testing.T) {
468468
foo:
469469
image: busybox
470470
`)
471-
assert.ErrorContains(t, err, "non-string key at top level: 123")
471+
assert.ErrorContains(t, err, "(root): Additional property 123 is not allowed")
472472

473473
_, err = loadYAML(`
474474
services:
@@ -477,7 +477,7 @@ services:
477477
123:
478478
image: busybox
479479
`)
480-
assert.ErrorContains(t, err, "non-string key in services: 123")
480+
assert.ErrorContains(t, err, "services: Additional property 123 is not allowed")
481481

482482
_, err = loadYAML(`
483483
services:
@@ -489,7 +489,7 @@ networks:
489489
config:
490490
- 123: oh dear
491491
`)
492-
assert.ErrorContains(t, err, "non-string key in networks.default.ipam.config[0]: 123")
492+
assert.ErrorContains(t, err, "networks.default.ipam.config.0: Additional property 123 is not allowed")
493493

494494
_, err = loadYAML(`
495495
services:
@@ -990,7 +990,7 @@ func TestDecodeErrors(t *testing.T) {
990990

991991
configDetails := buildConfigDetails(dict, nil)
992992
_, err := LoadWithContext(context.TODO(), configDetails)
993-
assert.Error(t, err, "yaml: line 4: found a tab character that violates indentation")
993+
assert.ErrorContains(t, err, "found character '\t' that cannot start any token")
994994
}
995995

996996
func TestBuildProperties(t *testing.T) {
@@ -3729,7 +3729,7 @@ services:
37293729
environment:
37303730
- DEBUG = true
37313731
`)
3732-
assert.Check(t, strings.Contains(err.Error(), "'services[test].environment': environment variable DEBUG is declared with a trailing space"))
3732+
assert.ErrorContains(t, err, "'services[test].environment': environment variable DEBUG is declared with a trailing space")
37333733
}
37343734

37353735
func TestFileModeNumber(t *testing.T) {

loader/mapstructure.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,21 @@ func cast(from reflect.Value, to reflect.Value) (interface{}, error) {
6565
return toInt(from.String())
6666
case reflect.Int64:
6767
return toInt64(from.String())
68+
case reflect.Uint64:
69+
return toInt64(from.String())
6870
case reflect.Float32:
6971
return toFloat32(from.String())
7072
case reflect.Float64:
7173
return toFloat(from.String())
7274
}
73-
case reflect.Int:
75+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
7476
if to.Kind() == reflect.String {
7577
return strconv.FormatInt(from.Int(), 10), nil
7678
}
79+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
80+
if to.Kind() == reflect.String {
81+
return strconv.FormatUint(from.Uint(), 10), nil
82+
}
7783
}
7884
return from.Interface(), nil
7985
}

loader/merge_reset_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ func Test_LoadWithReset(t *testing.T) {
4545
services:
4646
foo:
4747
image: foo
48-
build: !reset
48+
build: !reset {}
4949
environment:
50-
FOO: !reset
50+
FOO: !reset {}
5151
`),
5252
},
5353
},

loader/normalize_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"testing"
2121

2222
"github.com/compose-spec/compose-go/v2/types"
23-
"gopkg.in/yaml.v3"
23+
"github.com/goccy/go-yaml"
2424
"gotest.tools/v3/assert"
2525
)
2626

loader/reset.go

Lines changed: 36 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -18,94 +18,58 @@ package loader
1818

1919
import (
2020
"fmt"
21-
"strconv"
2221
"strings"
2322

2423
"github.com/compose-spec/compose-go/v2/tree"
25-
"gopkg.in/yaml.v3"
24+
"github.com/goccy/go-yaml/ast"
2625
)
2726

2827
type ResetProcessor struct {
29-
target interface{}
30-
paths []tree.Path
31-
visitedNodes map[*yaml.Node][]string
28+
paths []tree.Path
3229
}
3330

34-
// UnmarshalYAML implement yaml.Unmarshaler
35-
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
36-
p.visitedNodes = make(map[*yaml.Node][]string)
37-
resolved, err := p.resolveReset(value, tree.NewPath())
38-
p.visitedNodes = nil
39-
if err != nil {
40-
return err
41-
}
42-
return resolved.Decode(p.target)
31+
func NewResetProcessor(doc *ast.DocumentNode) PostProcessor {
32+
r := &ResetProcessor{}
33+
r.parse(doc.Body)
34+
return r
4335
}
4436

45-
// resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree
46-
func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) {
47-
pathStr := path.String()
48-
// If the path contains "<<", removing the "<<" element and merging the path
49-
if strings.Contains(pathStr, ".<<") {
50-
path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 1))
51-
}
52-
53-
// If the node is an alias, We need to process the alias field in order to consider the !override and !reset tags
54-
if node.Kind == yaml.AliasNode {
55-
if err := p.checkForCycle(node.Alias, path); err != nil {
56-
return nil, err
37+
func (p *ResetProcessor) parse(n ast.Node) bool {
38+
switch n.Type() {
39+
case ast.TagType:
40+
t := n.(*ast.TagNode)
41+
tag := t.Start.Value
42+
if tag == "!reset" {
43+
p.paths = append(p.paths, tree.Path(strings.TrimPrefix(n.GetPath(), "$.")))
44+
return true
45+
}
46+
if tag == "!override" {
47+
p.paths = append(p.paths, tree.Path(strings.TrimPrefix(n.GetPath(), "$.")))
48+
return false
49+
}
50+
case ast.MappingType:
51+
node := n.(*ast.MappingNode)
52+
for _, value := range node.Values {
53+
if p.parse(value.Value) {
54+
node.Values = removeMapping(node.Values, value.Key.String())
55+
}
56+
}
57+
case ast.SequenceType:
58+
for _, value := range n.(*ast.SequenceNode).Values {
59+
p.parse(value)
5760
}
58-
59-
return p.resolveReset(node.Alias, path)
6061
}
6162

62-
if node.Tag == "!reset" {
63-
p.paths = append(p.paths, path)
64-
return nil, nil
65-
}
66-
if node.Tag == "!override" {
67-
p.paths = append(p.paths, path)
68-
return node, nil
69-
}
63+
return false
64+
}
7065

71-
keys := map[string]int{}
72-
switch node.Kind {
73-
case yaml.SequenceNode:
74-
var nodes []*yaml.Node
75-
for idx, v := range node.Content {
76-
next := path.Next(strconv.Itoa(idx))
77-
resolved, err := p.resolveReset(v, next)
78-
if err != nil {
79-
return nil, err
80-
}
81-
if resolved != nil {
82-
nodes = append(nodes, resolved)
83-
}
66+
func removeMapping(nodes []*ast.MappingValueNode, key string) []*ast.MappingValueNode {
67+
for i, node := range nodes {
68+
if node.Key.String() == key {
69+
return append(nodes[:i], nodes[i+1:]...)
8470
}
85-
node.Content = nodes
86-
case yaml.MappingNode:
87-
var key string
88-
var nodes []*yaml.Node
89-
for idx, v := range node.Content {
90-
if idx%2 == 0 {
91-
key = v.Value
92-
if line, seen := keys[key]; seen {
93-
return nil, fmt.Errorf("line %d: mapping key %#v already defined at line %d", v.Line, key, line)
94-
}
95-
keys[key] = v.Line
96-
} else {
97-
resolved, err := p.resolveReset(v, path.Next(key))
98-
if err != nil {
99-
return nil, err
100-
}
101-
if resolved != nil {
102-
nodes = append(nodes, node.Content[idx-1], resolved)
103-
}
104-
}
105-
}
106-
node.Content = nodes
10771
}
108-
return node, nil
72+
return nodes
10973
}
11074

11175
// Apply finds the go attributes matching recorded paths and reset them to zero value
@@ -149,48 +113,3 @@ func (p *ResetProcessor) applyNullOverrides(target any, path tree.Path) error {
149113
}
150114
return nil
151115
}
152-
153-
func (p *ResetProcessor) checkForCycle(node *yaml.Node, path tree.Path) error {
154-
paths := p.visitedNodes[node]
155-
pathStr := path.String()
156-
157-
for _, prevPath := range paths {
158-
// If we're visiting the exact same path, it's not a cycle
159-
if pathStr == prevPath {
160-
continue
161-
}
162-
163-
// If either path is using a merge key, it's legitimate YAML merging
164-
if strings.Contains(prevPath, "<<") || strings.Contains(pathStr, "<<") {
165-
continue
166-
}
167-
168-
// Only consider it a cycle if one path is contained within the other
169-
// and they're not in different service definitions
170-
if (strings.HasPrefix(pathStr, prevPath+".") ||
171-
strings.HasPrefix(prevPath, pathStr+".")) &&
172-
!areInDifferentServices(pathStr, prevPath) {
173-
return fmt.Errorf("cycle detected: node at path %s references node at path %s", pathStr, prevPath)
174-
}
175-
}
176-
177-
p.visitedNodes[node] = append(paths, pathStr)
178-
return nil
179-
}
180-
181-
// areInDifferentServices checks if two paths are in different service definitions
182-
func areInDifferentServices(path1, path2 string) bool {
183-
// Split paths into components
184-
parts1 := strings.Split(path1, ".")
185-
parts2 := strings.Split(path2, ".")
186-
187-
// Look for the services component and compare the service names
188-
for i := 0; i < len(parts1) && i < len(parts2); i++ {
189-
if parts1[i] == "services" && i+1 < len(parts1) &&
190-
parts2[i] == "services" && i+1 < len(parts2) {
191-
// If they're different services, it's not a cycle
192-
return parts1[i+1] != parts2[i+1]
193-
}
194-
}
195-
return false
196-
}

0 commit comments

Comments
 (0)