-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson_parse.go
More file actions
199 lines (158 loc) · 4.94 KB
/
json_parse.go
File metadata and controls
199 lines (158 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package testastic
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"regexp"
"strings"
)
var (
// errUnknownPlaceholder is returned when a placeholder is not found in the matcher map.
errUnknownPlaceholder = errors.New("unknown placeholder")
// errJSONTrailingContent is returned when JSON contains more than one top-level value.
errJSONTrailingContent = errors.New("trailing content after top-level JSON value")
)
type expectedJSON struct {
Data any // Parsed JSON with Matcher objects in place of template expressions
Matchers map[string]string // Map of placeholder to original template expression
Raw string // Original file content for update operations
}
const jsonMatcherPlaceholderPrefix = "__TESTASTIC_MATCHER_"
// jsonTemplateExprRegex matches {{...}} expressions.
var jsonTemplateExprRegex = regexp.MustCompile(
`"?\{\{((?:[^}` + "`" + `]+|` + "`" + `[^` + "`" + `]*` + "`" + `)+)\}\}"?`,
)
// parseExpectedJSONFile reads and parses an expected file, replacing template expressions with matchers.
func parseExpectedJSONFile(path string) (*expectedJSON, error) {
content, err := os.ReadFile(path) //nolint:gosec // Path is controlled by test code.
if err != nil {
return nil, fmt.Errorf("failed to read expected file: %w", err)
}
return parseExpectedJSONString(string(content))
}
func parseExpectedJSONString(content string) (*expectedJSON, error) {
expected := &expectedJSON{
Matchers: make(map[string]string),
Raw: content,
}
matcherIndex := 0
processedContent := jsonTemplateExprRegex.ReplaceAllStringFunc(content, func(match string) string {
expr := match
// Strip surrounding quotes if the expression was quoted in JSON.
if strings.HasPrefix(expr, `"{{`) {
expr = strings.TrimPrefix(expr, `"`)
}
if strings.HasSuffix(expr, `}}"`) {
expr = strings.TrimSuffix(expr, `"`)
}
expr = strings.TrimPrefix(expr, "{{")
expr = strings.TrimSuffix(expr, "}}")
expr = trimSpace(expr)
placeholder := fmt.Sprintf(`"%s%d__"`, jsonMatcherPlaceholderPrefix, matcherIndex)
expected.Matchers[fmt.Sprintf("%s%d__", jsonMatcherPlaceholderPrefix, matcherIndex)] = expr
matcherIndex++
return placeholder
})
var data any
err := decodeJSON([]byte(processedContent), &data)
if err != nil {
return nil, fmt.Errorf("failed to parse expected file as JSON: %w", err)
}
replaced, err := replaceJSONPlaceholders(data, expected.Matchers)
if err != nil {
return nil, err
}
expected.Data = replaced
return expected, nil
}
func decodeJSON(data []byte, target *any) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
err := decoder.Decode(target)
if err != nil {
return fmt.Errorf("decode JSON: %w", err)
}
var extra any
extraErr := decoder.Decode(&extra)
if errors.Is(extraErr, io.EOF) {
return nil
}
if extraErr != nil {
return fmt.Errorf("decode JSON: %w", extraErr)
}
return errJSONTrailingContent
}
// replaceJSONPlaceholders walks the parsed JSON and replaces placeholder strings with Matcher objects.
//
//nolint:dupl // Similar to YAML version but uses different placeholder prefix.
func replaceJSONPlaceholders(data any, matchers map[string]string) (any, error) {
switch v := data.(type) {
case map[string]any:
result := make(map[string]any, len(v))
for key, val := range v {
replaced, err := replaceJSONPlaceholders(val, matchers)
if err != nil {
return nil, err
}
result[key] = replaced
}
return result, nil
case []any:
result := make([]any, len(v))
for i, val := range v {
replaced, err := replaceJSONPlaceholders(val, matchers)
if err != nil {
return nil, err
}
result[i] = replaced
}
return result, nil
case string:
if strings.HasPrefix(v, jsonMatcherPlaceholderPrefix) {
expr, ok := matchers[v]
if !ok {
return nil, fmt.Errorf("%w: %s", errUnknownPlaceholder, v)
}
matcher, err := parseMatcher(expr)
if err != nil {
return nil, fmt.Errorf("failed to parse matcher %q: %w", expr, err)
}
return matcher, nil
}
return v, nil
default:
return v, nil
}
}
// extractMatcherPositions returns a map of JSON paths to their original template expressions.
// This is used when updating expected files to preserve matchers.
func (e *expectedJSON) extractMatcherPositions() map[string]string {
positions := make(map[string]string)
extractJSONMatcherPaths(e.Data, "$", positions)
return positions
}
func extractJSONMatcherPaths(data any, path string, positions map[string]string) {
switch v := data.(type) {
case map[string]any:
for key, val := range v {
childPath := path + "." + key
if m, ok := val.(Matcher); ok {
positions[childPath] = m.String()
} else {
extractJSONMatcherPaths(val, childPath, positions)
}
}
case []any:
for i, val := range v {
childPath := fmt.Sprintf("%s[%d]", path, i)
if m, ok := val.(Matcher); ok {
positions[childPath] = m.String()
} else {
extractJSONMatcherPaths(val, childPath, positions)
}
}
}
}