-
Notifications
You must be signed in to change notification settings - Fork 6
/
walk.go
141 lines (123 loc) · 4.58 KB
/
walk.go
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
// Package jsonschema includes tools for walking JSON schema files and running a custom function for each instance.
package jsonschema
import (
"encoding/json"
"fmt"
"strings"
"github.com/GannettDigital/jsonparser"
"golang.org/x/exp/slices"
)
// WalkFunc processes a single Instance within a JSON schema file returning an error on any problems.
// The path corresponds to the JSONPath (http://goessner.net/articles/JsonPath/) of the instance within the JSON
// format described by the JSON Schema.
type WalkInstanceFunc func(path string, i Instance) error
// WalkRawFunc works similar to WalkFunc but rather than accepting an instance it accepts the raw JSON for each
// level of the schema.
type WalkRawFunc func(path string, value json.RawMessage) error
// Walk runs each instance in the JSON schema through the defined walk function, keeping track of the JSONPath.
// It assumes the JSON schema is valid and does not check for many errors such as bad property names.
// For instances that are objects or arrays the WalkFunc will be called for each child instance.
func Walk(s *Schema, walkFn WalkInstanceFunc) error {
rootPath := "$"
for key, value := range s.Properties {
if err := walkInstance(value, prependJSONPath(rootPath, key), walkFn); err != nil {
return err
}
}
if s.Items != nil {
if err := walkInstance(s.Items, rootPath+"[*]", walkFn); err != nil {
return err
}
}
return nil
}
// prependJSONPath a parent JSONPath to the beginning of a JSONPath.
// This allows for incrementally building up the full JSONPath.
func prependJSONPath(parent string, child string) string {
newPath := parent
if parent == "" {
newPath = child
}
if parent != "" && child != "" {
newPath = strings.Join([]string{parent, child}, ".")
}
return newPath
}
// walkInstance will recursively walk JSON Schema Instance calling defined walk functions for the root and for each
// property within an object and each item in an array.
// For each layer of depth the the JSONPath is added to so the walkFn received the full path of the JSON instance.
// Any error will halt the progress.
func walkInstance(raw json.RawMessage, path string, walkFn WalkInstanceFunc) error {
var i Instance
if err := json.Unmarshal(raw, &i); err != nil {
return fmt.Errorf("failed to unmarshal Instance at path %q: %v", path, err)
}
if err := walkFn(path, i); err != nil {
return fmt.Errorf("walkInstance failed at path %q: %v", path, err)
}
switch {
case slices.Contains(i.Type, "object"):
if i.Properties == nil {
return fmt.Errorf("object at path %q missing Properties", path)
}
for key, value := range i.Properties {
if err := walkInstance(value, prependJSONPath(path, key), walkFn); err != nil {
return err
}
}
case slices.Contains(i.Type, "array"):
if i.Items == nil {
return fmt.Errorf("array at path %q missing Items", path)
}
if i.Items != nil {
if err := walkInstance(i.Items, path+"[*]", walkFn); err != nil {
return err
}
}
}
return nil
}
// WalkRaw works nearly identical to the Walk function but rather than calling a WalkFunc calls a WalkRawFunc.
// By skipping the JSON unmarshaling of the Instance it runs nearly 10 times faster then WalkFunc.
func WalkRaw(s *Schema, walkFn WalkRawFunc) error {
rootPath := "$"
for key, value := range s.Properties {
if err := walkRaw(value, prependJSONPath(rootPath, key), walkFn); err != nil {
return err
}
}
if s.Items != nil {
if err := walkRaw(s.Items, rootPath+"[*]", walkFn); err != nil {
return err
}
}
return nil
}
// walkRaw is similar to walkInstance, it is the recursive file which drives walkRaw as walkInstance is the recursive
// function with drives Walk.
func walkRaw(raw json.RawMessage, path string, walkFn WalkRawFunc) error {
if err := walkFn(path, raw); err != nil {
return fmt.Errorf("walkRaw failed at path %q: %v", path, err)
}
iType, _, err := FieldType(raw)
if err != nil {
return fmt.Errorf("failed to determine instance type at path %q: %v", path, err)
}
switch iType {
case "object":
if err := jsonparser.ObjectEach(raw, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
return walkRaw(value, prependJSONPath(path, string(key)), walkFn)
}, "properties"); err != nil {
return fmt.Errorf("failed processing properties at path %q: %v", path, err)
}
case "array":
items, _, _, err := jsonparser.Get(raw, "items")
if err != nil {
return fmt.Errorf("failed extracting items at path %q: %v", path, err)
}
if err := walkRaw(items, path+"[*]", walkFn); err != nil {
return fmt.Errorf("failed processing items at path %q: %v", path, err)
}
}
return nil
}