Skip to content

Commit 8a56a7a

Browse files
authored
rename yamlpatch -> csyaml; new functions (#32)
* move package yamlpatch -> csyaml * csyaml/GetDocumentKeys() * csyaml.SplitDocuments() * format * deprecate yamlpatch.NewPatcher() * re-implement Merge with goccy * tests * typo * csyaml.IsEmptyYAML() * SplitDocuments: preserve comments * dependencies * update null merge behavior
1 parent deee40c commit 8a56a7a

18 files changed

+1444
-8
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717
steps:
1818

1919
- name: Check out code into the Go module directory
20-
uses: actions/checkout@v4
20+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2121

2222
- name: Set up Go
23-
uses: actions/setup-go@v5
23+
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
2424
with:
2525
go-version-file: go.mod
2626

@@ -32,7 +32,7 @@ jobs:
3232
RICHGO_FORCE_COLOR: 1
3333

3434
- name: golangci-lint
35-
uses: golangci/golangci-lint-action@v7
35+
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
3636
with:
3737
version: v2.1
3838
args: --issues-exit-code=1 --timeout 10m

.golangci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ linters:
1111
- inamedparam # reports interfaces with unnamed method parameters
1212
- wrapcheck # Checks that errors returned from external packages are wrapped
1313
- err113 # Go linter to check the errors handling expressions
14+
#- noinlineerr
1415
- paralleltest # Detects missing usage of t.Parallel() method in your Go test
1516
- testpackage # linter that makes you use a separate _test package
1617
- exhaustruct # Checks if all structure fields are initialized
@@ -27,21 +28,22 @@ linters:
2728
- gocognit # revive
2829
- gocyclo # revive
2930
- lll # revive
31+
- wsl # wsl_v5
3032

3133
#
3234
# Formatting only, useful in IDE but should not be forced on CI?
3335
#
3436

3537
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
36-
- wsl # add or remove empty lines
38+
#- wsl_v5 # add or remove empty lines
3739

3840
settings:
3941

4042
depguard:
4143
rules:
4244
yaml:
4345
files:
44-
- 'yamlpatch/patcher.go'
46+
- '!**/yamlpatch/patcher.go'
4547
deny:
4648
- pkg: gopkg.in/yaml.v2
4749
desc: yaml.v2 is deprecated for new code in favor of yaml.v3
@@ -102,6 +104,8 @@ linters:
102104
- 43
103105
- name: defer
104106
disabled: true
107+
#- name: enforce-switch-style
108+
# disabled: true
105109
- name: flag-parameter
106110
disabled: true
107111
- name: function-length

csyaml/empty.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package csyaml
2+
3+
import (
4+
"errors"
5+
"io"
6+
7+
yaml "github.com/goccy/go-yaml"
8+
"github.com/goccy/go-yaml/parser"
9+
)
10+
11+
// IsEmptyYAML reads one or more YAML documents from r and returns true
12+
// if they are all empty or contain only comments.
13+
// It will reports errors if the input is not valid YAML.
14+
func IsEmptyYAML(r io.Reader) (bool, error) {
15+
src, err := io.ReadAll(r)
16+
if err != nil {
17+
return false, err
18+
}
19+
20+
if len(src) == 0 {
21+
return true, nil
22+
}
23+
24+
file, err := parser.ParseBytes(src, 0)
25+
if err != nil {
26+
if errors.Is(err, io.EOF) {
27+
return true, nil
28+
}
29+
30+
return false, errors.New(yaml.FormatError(err, false, false))
31+
}
32+
33+
if file == nil || len(file.Docs) == 0 {
34+
return true, nil
35+
}
36+
37+
for _, doc := range file.Docs {
38+
if doc.Body != nil {
39+
return false, nil
40+
}
41+
}
42+
43+
return true, nil
44+
}

csyaml/empty_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package csyaml
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/crowdsecurity/go-cs-lib/cstest" // adjust this import to your package
10+
)
11+
12+
func TestIsEmptyYAML(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
input string
16+
want bool
17+
wantErr string
18+
}{
19+
{
20+
name: "empty document",
21+
input: ``,
22+
want: true,
23+
},
24+
{
25+
name: "just a key",
26+
input: "foo:",
27+
want: false,
28+
},
29+
{
30+
name: "just newline",
31+
input: "\n",
32+
want: true,
33+
},
34+
{
35+
name: "just comment",
36+
input: "# only a comment",
37+
want: true,
38+
},
39+
{
40+
name: "comments and empty lines",
41+
input: "# only a comment\n\n# another one\n\n",
42+
want: true,
43+
},
44+
{
45+
name: "empty doc with separator",
46+
input: "---",
47+
want: true,
48+
},
49+
{
50+
name: "empty mapping",
51+
input: "{}",
52+
want: false,
53+
},
54+
{
55+
name: "empty sequence",
56+
input: "[]",
57+
want: false,
58+
},
59+
{
60+
name: "non-empty mapping",
61+
input: "foo: bar",
62+
want: false,
63+
},
64+
{
65+
name: "non-empty sequence",
66+
input: "- 1\n- 2",
67+
want: false,
68+
},
69+
{
70+
name: "non-empty scalar",
71+
input: "hello",
72+
want: false,
73+
},
74+
{
75+
name: "empty scalar",
76+
input: "''",
77+
want: false,
78+
},
79+
{
80+
name: "explicit nil",
81+
input: "null",
82+
want: false,
83+
},
84+
{
85+
name: "malformed YAML",
86+
input: "foo: [1,",
87+
wantErr: "[1:6] sequence end token ']' not found",
88+
},
89+
{
90+
name: "multiple empty documents",
91+
input: "---\n---\n---\n#comment",
92+
want: true,
93+
},
94+
{
95+
name: "second document is not empty",
96+
input: "---\nfoo: bar\n---\n#comment",
97+
want: false,
98+
},
99+
}
100+
101+
for _, tc := range tests {
102+
t.Run(tc.name, func(t *testing.T) {
103+
got, err := IsEmptyYAML(strings.NewReader(tc.input))
104+
105+
cstest.RequireErrorContains(t, err, tc.wantErr)
106+
107+
if tc.wantErr != "" {
108+
return
109+
}
110+
111+
assert.Equal(t, tc.want, got)
112+
})
113+
}
114+
}

csyaml/keys.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package csyaml
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
8+
"github.com/goccy/go-yaml"
9+
)
10+
11+
// GetDocumentKeys reads all YAML documents from r and for each one
12+
// returns a slice of its top-level keys, in order.
13+
//
14+
// Non-mapping documents yield an empty slice. Duplicate keys
15+
// are not allowed and return an error.
16+
func GetDocumentKeys(r io.Reader) ([][]string, error) {
17+
// Decode into Go types, but force mappings into MapSlice
18+
dec := yaml.NewDecoder(r, yaml.UseOrderedMap())
19+
20+
allKeys := make([][]string, 0)
21+
22+
idx := -1
23+
24+
for {
25+
var raw any
26+
27+
idx++
28+
29+
if err := dec.Decode(&raw); err != nil {
30+
if errors.Is(err, io.EOF) {
31+
break
32+
}
33+
return nil, fmt.Errorf("position %d: %s", idx, yaml.FormatError(err, false, false))
34+
}
35+
keys := []string{}
36+
37+
// Only mapping nodes become MapSlice with UseOrderedMap()
38+
if ms, ok := raw.(yaml.MapSlice); ok {
39+
for _, item := range ms {
40+
// Key is interface{}—here we expect strings
41+
if ks, ok := item.Key.(string); ok {
42+
keys = append(keys, ks)
43+
} else {
44+
// fallback to string form of whatever it is
45+
keys = append(keys, fmt.Sprint(item.Key))
46+
}
47+
}
48+
}
49+
50+
allKeys = append(allKeys, keys)
51+
}
52+
53+
return allKeys, nil
54+
}

csyaml/keys_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package csyaml_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/crowdsecurity/go-cs-lib/cstest"
10+
"github.com/crowdsecurity/go-cs-lib/csyaml"
11+
)
12+
13+
func TestCollectTopLevelKeys(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
input string
17+
want [][]string
18+
wantErr string
19+
}{
20+
{
21+
name: "single mapping",
22+
input: "a: 1\nb: 2\n",
23+
want: [][]string{{"a", "b"}},
24+
},
25+
{
26+
name: "duplicate keys mapping",
27+
input: "a: 1\nb: 2\na: 3\n",
28+
want: nil,
29+
wantErr: `position 0: [3:1] mapping key "a" already defined at [1:1]`,
30+
},
31+
{
32+
name: "multiple documents",
33+
input: `---
34+
a: 1
35+
b: 2
36+
---
37+
- 1
38+
---
39+
c: 1
40+
b: 2
41+
---
42+
"scalar"
43+
`,
44+
want: [][]string{{"a", "b"}, {}, {"c", "b"}, {}},
45+
},
46+
{
47+
name: "empty input",
48+
input: "",
49+
want: [][]string{},
50+
},
51+
{
52+
name: "invalid YAML",
53+
input: "list: [1, 2,",
54+
want: nil,
55+
wantErr: "position 0: [1:7] sequence end token ']' not found",
56+
},
57+
}
58+
59+
for _, tc := range tests {
60+
t.Run(tc.name, func(t *testing.T) {
61+
r := strings.NewReader(tc.input)
62+
got, err := csyaml.GetDocumentKeys(r)
63+
cstest.RequireErrorContains(t, err, tc.wantErr)
64+
assert.Equal(t, tc.want, got)
65+
})
66+
}
67+
}

0 commit comments

Comments
 (0)