Skip to content

Commit 2e13d30

Browse files
authored
fix: support nested arrays in json matchers (#145)
1 parent 9137b42 commit 2e13d30

File tree

10 files changed

+996
-44
lines changed

10 files changed

+996
-44
lines changed

examples/__snapshots__/matchJSON_test.snap

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,59 @@
116116
"email": "mock@email.com"
117117
}
118118
---
119+
120+
[TestMatchers/Any_matcher/should_handle_nested_json_arrays - 1]
121+
{
122+
"repositories": [
123+
{
124+
"commits": [
125+
{
126+
"files": [
127+
{
128+
"checksum": "<Any value>",
129+
"path": "a.js"
130+
},
131+
{
132+
"checksum": "<Any value>",
133+
"path": "b.js"
134+
}
135+
],
136+
"sha": "abc123"
137+
},
138+
{
139+
"files": [
140+
{
141+
"checksum": "<Any value>",
142+
"path": "c.js"
143+
}
144+
],
145+
"sha": "def456"
146+
}
147+
],
148+
"name": "repo1"
149+
},
150+
{
151+
"commits": [
152+
{
153+
"files": [
154+
{
155+
"checksum": "<Any value>",
156+
"path": "d.js"
157+
},
158+
{
159+
"checksum": "<Any value>",
160+
"path": "e.js"
161+
},
162+
{
163+
"checksum": "<Any value>",
164+
"path": "f.js"
165+
}
166+
],
167+
"sha": "ghi789"
168+
}
169+
],
170+
"name": "repo2"
171+
}
172+
]
173+
}
174+
---

examples/matchJSON_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,35 @@ func TestMatchers(t *testing.T) {
179179

180180
snaps.MatchJSON(t, body, match.Any("data.createdAt"))
181181
})
182+
183+
t.Run("should handle nested json arrays", func(t *testing.T) {
184+
j := []byte(`{
185+
"repositories": [
186+
{
187+
"name": "repo1",
188+
"commits": [
189+
{"sha": "abc123", "files": [{"path": "a.js", "checksum": "c1"}, {"path": "b.js", "checksum": "c2"}]},
190+
{"sha": "def456", "files": [{"path": "c.js", "checksum": "c3"}]}
191+
]
192+
},
193+
{
194+
"name": "repo2",
195+
"commits": [
196+
{
197+
"sha": "ghi789",
198+
"files": [
199+
{"path": "d.js", "checksum": "c4"},
200+
{"path": "e.js", "checksum": "c5"},
201+
{"path": "f.js", "checksum": "c6"}
202+
]
203+
}
204+
]
205+
}
206+
]
207+
}`)
208+
209+
snaps.MatchJSON(t, j, match.Any("repositories.#.commits.#.files.#.checksum"))
210+
})
182211
})
183212

184213
t.Run("my matcher", func(t *testing.T) {

match/any.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,34 @@ func (a anyMatcher) JSON(b []byte) ([]byte, []MatcherError) {
9595

9696
json := b
9797
for _, path := range a.paths {
98-
r := gjson.GetBytes(json, path)
99-
if !r.Exists() {
100-
if a.errOnMissingPath {
101-
errs = append(errs, a.matcherError(errPathNotFound, path))
98+
for _, ep := range expandArrayPaths(json, path) {
99+
j, err := a.processPathJSON(json, ep)
100+
if err != nil {
101+
errs = append(errs, a.matcherError(err, path))
102+
continue
102103
}
103104

104-
continue
105+
json = j
105106
}
107+
}
106108

107-
j, err := sjson.SetBytesOptions(json, path, a.placeholder, setJSONOptions)
108-
if err != nil {
109-
errs = append(errs, a.matcherError(err, path))
109+
return json, errs
110+
}
110111

111-
continue
112+
func (a anyMatcher) processPathJSON(json []byte, path string) ([]byte, error) {
113+
r := gjson.GetBytes(json, path)
114+
if !r.Exists() {
115+
if a.errOnMissingPath {
116+
return nil, errPathNotFound
112117
}
113118

114-
json = j
119+
return json, nil
115120
}
116121

117-
return json, errs
122+
j, err := sjson.SetBytesOptions(json, path, a.placeholder, setJSONOptions)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
return j, nil
118128
}

match/any_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestAnyMatcher(t *testing.T) {
1717
test.Equal(t, "Any", a.name)
1818
})
1919

20-
t.Run("should allow overriding values", func(t *testing.T) {
20+
t.Run("should allow overriding config values", func(t *testing.T) {
2121
p := []string{"test.1", "test.2"}
2222
a := Any(p...).ErrOnMissingPath(false).Placeholder("hello")
2323

@@ -98,6 +98,104 @@ func TestAnyMatcher(t *testing.T) {
9898
test.Equal(t, expected, string(res))
9999
},
100100
)
101+
102+
t.Run("nested json arrays", func(t *testing.T) {
103+
t.Run("should replace values with root level nested arrays", func(t *testing.T) {
104+
j := []byte(`[
105+
{
106+
"results": ["mock-data-1", "mock-data-2" ],
107+
},
108+
{
109+
"results": ["mock-data-1", "mock-data-2" ],
110+
},
111+
{
112+
"results": ["mock-data-1", "mock-data-2" ],
113+
},
114+
]`)
115+
116+
a := Any("#.results.#")
117+
118+
res, errs := a.JSON(j)
119+
120+
expected := `[
121+
{
122+
"results": ["<Any value>", "<Any value>" ],
123+
},
124+
{
125+
"results": ["<Any value>", "<Any value>" ],
126+
},
127+
{
128+
"results": ["<Any value>", "<Any value>" ],
129+
},
130+
]`
131+
132+
test.Equal(t, 0, len(errs))
133+
test.Equal(t, expected, string(res))
134+
})
135+
136+
t.Run("should replace value and return new json", func(t *testing.T) {
137+
j := []byte(`{
138+
"results": [
139+
{
140+
"packages": [
141+
{"vulnerabilities": "mock-data-1", "name": "mock-name-1", "id": 12},
142+
{"vulnerabilities": "mock-data-1", "name": "mock-name-1", "id": 15},
143+
{"vulnerabilities": "mock-data-1", "name": "mock-name-1", "id": 17},
144+
],
145+
},
146+
{
147+
"packages": [
148+
{"vulnerabilities": "mock-data-2", "name": "mock-name-2", "id": 22},
149+
{"vulnerabilities": "mock-data-2", "name": "mock-name-2", "id": 25},
150+
{"vulnerabilities": "mock-data-2", "name": "mock-name-2", "id": 27},
151+
],
152+
},
153+
{
154+
"packages": [
155+
{"vulnerabilities": "mock-data-3", "name": "mock-name-3", "id": 32},
156+
{"vulnerabilities": "mock-data-3", "name": "mock-name-3", "id": 35},
157+
{"vulnerabilities": "mock-data-3", "name": "mock-name-3", "id": 37},
158+
],
159+
},
160+
]
161+
}`)
162+
a := Any(
163+
"results.#.packages.#.vulnerabilities",
164+
"results.#.packages.#.name",
165+
"missing.key",
166+
).ErrOnMissingPath(false)
167+
res, errs := a.JSON(j)
168+
169+
expected := `{
170+
"results": [
171+
{
172+
"packages": [
173+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 12},
174+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 15},
175+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 17},
176+
],
177+
},
178+
{
179+
"packages": [
180+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 22},
181+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 25},
182+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 27},
183+
],
184+
},
185+
{
186+
"packages": [
187+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 32},
188+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 35},
189+
{"vulnerabilities": "<Any value>", "name": "<Any value>", "id": 37},
190+
],
191+
},
192+
]
193+
}`
194+
195+
test.Equal(t, 0, len(errs))
196+
test.Equal(t, expected, string(res))
197+
})
198+
})
101199
})
102200

103201
t.Run("YAML", func(t *testing.T) {

match/custom.go

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package match
22

33
import (
44
"bytes"
5+
"strings"
56

67
"github.com/gkampitakis/go-snaps/match/internal/yaml"
78
"github.com/goccy/go-yaml/parser"
@@ -109,24 +110,64 @@ func (c *customMatcher) YAML(b []byte) ([]byte, []MatcherError) {
109110

110111
// JSON is intended to be called internally on snaps.MatchJSON for applying Custom matcher
111112
func (c *customMatcher) JSON(b []byte) ([]byte, []MatcherError) {
112-
r := gjson.GetBytes(b, c.path)
113+
var errs []MatcherError
114+
json := b
115+
116+
for _, ep := range expandArrayPaths(json, c.path) {
117+
j, err := c.processPathJSON(json, ep)
118+
if err != nil {
119+
errs = append(errs, c.matcherError(err)...)
120+
continue
121+
}
122+
123+
json = j
124+
}
125+
126+
return json, errs
127+
}
128+
129+
func (c *customMatcher) processPathJSON(json []byte, path string) ([]byte, error) {
130+
r := gjson.GetBytes(json, path)
113131
if !r.Exists() {
114132
if c.errOnMissingPath {
115-
return nil, c.matcherError(errPathNotFound)
133+
return nil, errPathNotFound
116134
}
117135

118-
return b, nil
136+
return json, nil
119137
}
120138

121-
value, err := c.callback(r.Value())
122-
if err != nil {
123-
return nil, c.matcherError(err)
124-
}
139+
if r.IsArray() && strings.HasPrefix(path, "#.") {
140+
arr := r.Array()
141+
if len(arr) == 0 {
142+
return json, nil
143+
}
125144

126-
b, err = sjson.SetBytesOptions(b, c.path, value, setJSONOptions)
127-
if err != nil {
128-
return nil, c.matcherError(err)
129-
}
145+
for _, item := range arr {
146+
value, err := c.callback(item.Value())
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
j, err := sjson.SetBytesOptions(json, path, value, setJSONOptions)
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
json = j
157+
}
130158

131-
return b, nil
159+
return json, nil
160+
} else {
161+
value, err := c.callback(r.Value())
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
j, err := sjson.SetBytesOptions(json, path, value, setJSONOptions)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
return j, nil
172+
}
132173
}

0 commit comments

Comments
 (0)