-
Notifications
You must be signed in to change notification settings - Fork 54
/
keyword.go
220 lines (187 loc) · 6.54 KB
/
keyword.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
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package jsonschema
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sync"
jptr "github.com/qri-io/jsonpointer"
)
var notSupported = map[string]bool{
// core
"$vocabulary": true,
// other
"contentEncoding": true,
"contentMediaType": true,
"contentSchema": true,
"deprecated": true,
// backward compatibility with draft7
"definitions": true,
"dependencies": true,
}
var kr *KeywordRegistry
var krLock sync.Mutex
// KeywordRegistry contains a mapping of jsonschema keywords and their
// expected behavior.
type KeywordRegistry struct {
keywordRegistry map[string]KeyMaker
keywordOrder map[string]int
keywordInsertOrder map[string]int
}
func getGlobalKeywordRegistry() (*KeywordRegistry, func()) {
krLock.Lock()
if kr == nil {
kr = &KeywordRegistry{
keywordRegistry: make(map[string]KeyMaker, 0),
keywordOrder: make(map[string]int, 0),
keywordInsertOrder: make(map[string]int, 0),
}
}
return kr, func() { krLock.Unlock() }
}
func copyGlobalKeywordRegistry() *KeywordRegistry {
kr, release := getGlobalKeywordRegistry()
defer release()
return kr.Copy()
}
// Copy creates a new KeywordRegistry populated with the same data.
func (r *KeywordRegistry) Copy() *KeywordRegistry {
dest := &KeywordRegistry{
keywordRegistry: make(map[string]KeyMaker, len(r.keywordRegistry)),
keywordOrder: make(map[string]int, len(r.keywordOrder)),
keywordInsertOrder: make(map[string]int, len(r.keywordInsertOrder)),
}
for k, v := range r.keywordRegistry {
dest.keywordRegistry[k] = v
}
for k, v := range r.keywordOrder {
dest.keywordOrder[k] = v
}
for k, v := range r.keywordInsertOrder {
dest.keywordInsertOrder[k] = v
}
return dest
}
// IsRegisteredKeyword validates if a given prop string is a registered keyword
func (r *KeywordRegistry) IsRegisteredKeyword(prop string) bool {
_, ok := r.keywordRegistry[prop]
return ok
}
// GetKeyword returns a new instance of the keyword
func (r *KeywordRegistry) GetKeyword(prop string) Keyword {
if !r.IsRegisteredKeyword(prop) {
return NewVoid()
}
return r.keywordRegistry[prop]()
}
// GetKeywordOrder returns the order index of
// the given keyword or defaults to 1
func (r *KeywordRegistry) GetKeywordOrder(prop string) int {
if order, ok := r.keywordOrder[prop]; ok {
return order
}
return 1
}
// GetKeywordInsertOrder returns the insert index of
// the given keyword
func (r *KeywordRegistry) GetKeywordInsertOrder(prop string) int {
if order, ok := r.keywordInsertOrder[prop]; ok {
return order
}
// TODO(arqu): this is an arbitrary max
return 1000
}
// SetKeywordOrder assigns a given order to a keyword
func (r *KeywordRegistry) SetKeywordOrder(prop string, order int) {
r.keywordOrder[prop] = order
}
// SetKeywordOrder assigns a given order to a keyword
func SetKeywordOrder(prop string, order int) {
r, release := getGlobalKeywordRegistry()
defer release()
r.SetKeywordOrder(prop, order)
}
// IsNotSupportedKeyword is a utility function to clarify when
// a given keyword, while expected is not supported
func (r *KeywordRegistry) IsNotSupportedKeyword(prop string) bool {
_, ok := notSupported[prop]
return ok
}
// IsRegistryLoaded checks if any keywords are present
func (r *KeywordRegistry) IsRegistryLoaded() bool {
return r.keywordRegistry != nil && len(r.keywordRegistry) > 0
}
// RegisterKeyword registers a keyword with the registry
func (r *KeywordRegistry) RegisterKeyword(prop string, maker KeyMaker) {
r.keywordRegistry[prop] = maker
r.keywordInsertOrder[prop] = len(r.keywordInsertOrder)
}
// RegisterKeyword registers a keyword with the registry
func RegisterKeyword(prop string, maker KeyMaker) {
r, release := getGlobalKeywordRegistry()
defer release()
r.RegisterKeyword(prop, maker)
}
// MaxKeywordErrStringLen sets how long a value can be before it's length is truncated
// when printing error strings
// a special value of -1 disables output trimming
var MaxKeywordErrStringLen = 20
// Keyword is an interface for anything that can validate.
// JSON-Schema keywords are all examples of Keyword
type Keyword interface {
// ValidateKeyword checks decoded JSON data and writes
// validation errors (if any) to an outparam slice of KeyErrors
ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{})
// Register builds up the schema tree by evaluating the current key
// and the current location pointer which is later used with resolve to
// navigate the schema tree and substitute the propper schema for a given
// reference.
Register(uri string, registry *SchemaRegistry)
// Resolve unraps a pointer to the destination schema
// It usually starts with a $ref validation call which
// uses the pointer token by token to navigate the
// schema tree to get to the last schema in the chain.
// Since every keyword can have it's specifics around resolving
// each keyword need to implement it's own version of Resolve.
// Terminal keywords should respond with nil as it's not a schema
// Keywords that wrap a schema should return the appropriate schema.
// In case of a non-existing location it will fail to resolve, return nil
// on ref resolution and error out.
Resolve(pointer jptr.Pointer, uri string) *Schema
}
// KeyMaker is a function that generates instances of a Keyword.
// Calls to KeyMaker will be passed directly to json.Marshal,
// so the returned value should be a pointer
type KeyMaker func() Keyword
// KeyError represents a single error in an instance of a schema
// The only absolutely-required property is Message.
type KeyError struct {
// PropertyPath is a string path that leads to the
// property that produced the error
PropertyPath string `json:"propertyPath,omitempty"`
// InvalidValue is the value that returned the error
InvalidValue interface{} `json:"invalidValue,omitempty"`
// Message is a human-readable description of the error
Message string `json:"message"`
}
// Error implements the error interface for KeyError
func (v KeyError) Error() string {
if v.PropertyPath != "" && v.InvalidValue != nil {
return fmt.Sprintf("%s: %s %s", v.PropertyPath, InvalidValueString(v.InvalidValue), v.Message)
} else if v.PropertyPath != "" {
return fmt.Sprintf("%s: %s", v.PropertyPath, v.Message)
}
return v.Message
}
// InvalidValueString returns the errored value as a string
func InvalidValueString(data interface{}) string {
bt, err := json.Marshal(data)
if err != nil {
return ""
}
bt = bytes.Replace(bt, []byte{'\n', '\r'}, []byte{' '}, -1)
if MaxKeywordErrStringLen != -1 && len(bt) > MaxKeywordErrStringLen {
bt = append(bt[:MaxKeywordErrStringLen], []byte("...")...)
}
return string(bt)
}