-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson_parse.go
More file actions
157 lines (123 loc) · 3.84 KB
/
Copy pathjson_parse.go
File metadata and controls
157 lines (123 loc) · 3.84 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
package testastic
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strconv"
)
// errJSONTrailingContent is returned when JSON contains more than one top-level value.
var 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
Raw string // Original file content for update operations
}
const jsonMatcherPlaceholderPrefix = "__TESTASTIC_MATCHER_"
type jsonMatcherPreparer struct{}
func (jsonMatcherPreparer) prepare(source string) (preparedMatcherSource, error) {
sites := matcherSourceSites(source, matcherSourceRules{
includeWholeQuotes: true,
unclosedBacktickConsumes: true,
}, strconv.Quote)
if len(sites) == 0 {
return preparedMatcherSource{source: source}, nil
}
literalSource := substituteMatcherSourceSites(source, sites, "null")
var literalData any
err := decodeJSON([]byte(literalSource), &literalData)
if err != nil {
return preparedMatcherSource{}, err
}
literalValues := stringValues(literalData)
return preparedMatcherSource{
source: source,
sites: sites,
placeholder: jsonMatcherPlaceholderPrefix,
collides: func(candidate string) bool {
_, exists := literalValues[candidate]
return exists
},
}, nil
}
// 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) {
program, err := compileMatcherProgram(content, jsonMatcherPreparer{})
if err != nil {
compileErr, ok := errors.AsType[*matcherCompileError](err)
if ok {
return nil, fmt.Errorf("failed to parse matcher %q: %w", compileErr.expression, compileErr.cause)
}
return nil, fmt.Errorf("failed to parse expected file as JSON: %w", err)
}
expected := &expectedJSON{Raw: program.original}
var data any
err = decodeJSON([]byte(program.sourceForParser()), &data)
if err != nil {
return nil, fmt.Errorf("failed to parse expected file as JSON: %w", err)
}
replaced, err := program.resolve(data)
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
}
// 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) {
if m, ok := data.(Matcher); ok {
positions[path] = m.String()
return
}
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)
}
}
}
}