forked from rs/rest-layer
/
schema.go
341 lines (329 loc) · 11.6 KB
/
schema.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
package schema
import (
"context"
"fmt"
"log"
"reflect"
"strings"
)
// Schema defines fields for a document
type Schema struct {
// Description of the object described by this schema
Description string
// Fields defines the schema's allowed fields
Fields Fields
// MinLen defines the minimum number of fields (default 0)
MinLen int
// MaxLen defines the maximum number of fields (default no limit)
MaxLen int
}
// Validator is an interface used to validate schema against actual data
type Validator interface {
GetField(name string) *Field
Prepare(ctx context.Context, payload map[string]interface{}, original *map[string]interface{}, replace bool) (changes map[string]interface{}, base map[string]interface{})
Validate(changes map[string]interface{}, base map[string]interface{}) (doc map[string]interface{}, errs map[string][]interface{})
}
// Compiler is an interface defining a validator that can be compiled at run time in order
// to check validator configuration validity and/or prepare some data for a faster execution.
type Compiler interface {
Compile() error
}
type internal struct{}
// Tombstone is used to mark a field for removal
var Tombstone = internal{}
func addFieldError(errs map[string][]interface{}, field string, err interface{}) {
errs[field] = append(errs[field], err)
}
func mergeFieldErrors(errs map[string][]interface{}, mergeErrs map[string][]interface{}) {
// TODO recursive merge
for field, values := range mergeErrs {
if dest, found := errs[field]; found {
for _, value := range values {
dest = append(dest, value)
}
} else {
errs[field] = values
}
}
}
// Compile implements Compiler interface and call the same function on each field.
// Note: if you use schema as a standalone library, it is the *caller's* responsibility
// to invoke the Compile method before using Prepare or Validate on a Schema instance,
// otherwise FieldValidator instances may not be initialized correctly.
func (s Schema) Compile() error {
// Search for all Dependecy on fields, and compile then
if err := compileDependencies(s, s); err != nil {
return err
}
for field, def := range s.Fields {
// Compile each field
if err := def.Compile(); err != nil {
return fmt.Errorf("%s%v", field, err)
}
}
return nil
}
// GetField returns the validator for the field if the given field name is present
// in the schema.
//
// You may reference sub field using dotted notation field.subfield
func (s Schema) GetField(name string) *Field {
// Split the name to get the current level name on first element and
// the rest of the path as second element if dot notation is used
// (i.e.: field.subfield.subsubfield -> field, subfield.subsubfield)
if i := strings.IndexByte(name, '.'); i != -1 {
remaining := name[i+1:]
name = name[:i]
field, found := s.Fields[name]
if !found {
// Invalid node
return nil
}
if field.Schema == nil {
// Invalid path
return nil
}
// Recursively call has field to consume the whole path
return field.Schema.GetField(remaining)
}
if field, found := s.Fields[name]; found {
return &field
}
return nil
}
// Prepare takes a payload with an optional original payout when updating an existing item and
// return two maps, one containing changes operated by the user and another defining either
// exising data (from the current item) or data generated by the system thru "default" value
// or hooks.
//
// If the original map is nil, prepare will act as if the payload is a new document. The OnInit
// hook is executed for each field if any, and default values are assigned to missing fields.
//
// When the original map is defined, the payload is considered as an update on the original document,
// default values are not assigned, and only fields which are different than in the original are
// left in the change map. The OnUpdate hook is executed on each field.
//
// If the replace argument is set to true with the original document set, the behavior is slightly
// different as any field not present in the payload but present in the original are set to nil
// in the change map (instead of just behing absent). This instruct the validator that the field
// has been edited, so ReadOnly flag can throw an error and the field will be removed from the
// output document. The OnInit is also called instead of the OnUpdate.
func (s Schema) Prepare(ctx context.Context, payload map[string]interface{}, original *map[string]interface{}, replace bool) (changes map[string]interface{}, base map[string]interface{}) {
changes = map[string]interface{}{}
base = map[string]interface{}{}
for field, def := range s.Fields {
value, found := payload[field]
if original == nil {
if replace == true {
log.Panic("Cannot use replace=true without orignal")
}
// Handle prepare on a new document (no original)
if !found || value == nil {
// Add default fields
if def.Default != nil {
base[field] = def.Default
}
} else if found {
changes[field] = value
}
} else {
// Handle prepare on an updated document (original provided)
oValue, oFound := (*original)[field]
// Apply value to change-set only if the field was not identical same in the original doc
if found && (!oFound || !reflect.DeepEqual(value, oValue)) {
changes[field] = value
}
if !found && oFound && replace {
// When replace arg is true and a field is not present in the payload but is in the original,
// the tombstone value is set on the field in the change map so validator can enforce the
// ReadOnly and then the field can be removed from the output document.
// One exception to that though: if the field is set to hidden and is not readonly, we use
// previous value as the client would have no way to resubmit the stored value.
if def.Hidden && !def.ReadOnly {
changes[field] = oValue
} else {
changes[field] = Tombstone
}
}
if oFound {
base[field] = oValue
}
}
if def.Schema != nil {
// Prepare sub-schema
var subOriginal *map[string]interface{}
if original != nil {
// If original is provided, prepare the sub field if it exists and
// is a dictionary. Otherwise, use an empty dict.
oValue := (*original)[field]
subOriginal = &map[string]interface{}{}
if su, ok := oValue.(*map[string]interface{}); ok {
subOriginal = su
}
}
if found {
if subPayload, ok := value.(map[string]interface{}); ok {
// If payload contains a sub-document for this field, validate it
// using the sub-validator
c, b := def.Schema.Prepare(ctx, subPayload, subOriginal, replace)
changes[field] = c
base[field] = b
} else {
// Invalid payload, it will be caught by Validate()
}
} else {
// If the payload doesn't contain a sub-document, perform validation
// on an empty one so we don't miss default values
c, b := def.Schema.Prepare(ctx, map[string]interface{}{}, subOriginal, replace)
if len(c) > 0 || len(b) > 0 {
// Only apply prepared field if something was added
changes[field] = c
base[field] = b
}
}
}
// Call the OnInit or OnUpdate depending on the presence of the original doc and the
// state of the replace argument.
var hook func(ctx context.Context, value interface{}) interface{}
if original == nil || replace {
hook = def.OnInit
} else {
hook = def.OnUpdate
}
if hook != nil {
// Get the change value or fallback on the base value
if value, found := changes[field]; found {
if value == Tombstone {
// If the field has a tombstone, apply the handler on the base
// and remove the tombstone so it doesn't appear as a user
// generated change
base[field] = hook(ctx, base[field])
delete(changes, field)
} else {
changes[field] = hook(ctx, value)
}
} else {
base[field] = hook(ctx, base[field])
}
}
}
// Assign all out of schema fields to the changes map so Validate() can complain about it
for field, value := range payload {
if _, found := s.Fields[field]; !found {
changes[field] = value
}
}
return
}
// Validate validates changes applied on a base document in regard to the schema
// and generate an result document with the changes applied to the base document.
// All errors in the process are reported in the returned errs value.
func (s Schema) Validate(changes map[string]interface{}, base map[string]interface{}) (doc map[string]interface{}, errs map[string][]interface{}) {
return s.validate(changes, base, true)
}
func (s Schema) validate(changes map[string]interface{}, base map[string]interface{}, isRoot bool) (doc map[string]interface{}, errs map[string][]interface{}) {
doc = map[string]interface{}{}
errs = map[string][]interface{}{}
for field, def := range s.Fields {
// Check read only fields
if def.ReadOnly {
if _, found := changes[field]; found {
addFieldError(errs, field, "read-only")
}
}
// Check required fields
if def.Required {
if value, found := changes[field]; !found || value == nil {
if found {
// If explicitly set to null, raise the required error
addFieldError(errs, field, "required")
} else if value, found = base[field]; !found || value == nil {
// If field was omitted and isn't set by a Default of a hook, raise
addFieldError(errs, field, "required")
}
}
}
// Validate sub-schema on non provided fields in order to enforce requireds
if def.Schema != nil {
if _, found := changes[field]; !found {
if _, found := base[field]; !found {
empty := map[string]interface{}{}
if _, subErrs := def.Schema.validate(empty, empty, false); len(subErrs) > 0 {
addFieldError(errs, field, subErrs)
}
}
}
}
}
// Apply changes to the base in doc
for field, value := range base {
doc[field] = value
}
for field, value := range changes {
if value == Tombstone {
// If the value is set for removal, remove it from the doc
delete(doc, field)
} else {
doc[field] = value
}
}
// Validate all dependency from the root schema only as dependencies can refers to parent schemas
if isRoot {
mergeErrs := s.validateDependencies(changes, doc, "")
mergeFieldErrors(errs, mergeErrs)
}
for field, value := range doc {
// Check invalid field (fields provided in the payload by not present in the schema)
def, found := s.Fields[field]
if !found {
addFieldError(errs, field, "invalid field")
continue
}
if def.Schema != nil {
// Schema defines a sub-schema
subChanges := map[string]interface{}{}
subBase := map[string]interface{}{}
// Check if changes contains a valid sub-document
if v, found := changes[field]; found {
if m, ok := v.(map[string]interface{}); ok {
subChanges = m
} else {
addFieldError(errs, field, "not a dict")
}
}
// Check if base contains a valid sub-document
if v, found := base[field]; found {
if m, ok := v.(map[string]interface{}); ok {
subBase = m
} else {
addFieldError(errs, field, "not a dict")
}
}
// Validate sub document and add the result to the current doc's field
if subDoc, subErrs := def.Schema.validate(subChanges, subBase, false); len(subErrs) > 0 {
addFieldError(errs, field, subErrs)
} else {
doc[field] = subDoc
}
} else if def.Validator != nil {
// Apply validator if provided
var err error
if value, err = def.Validator.Validate(value); err != nil {
addFieldError(errs, field, err.Error())
} else {
// Store the normalized value
doc[field] = value
}
}
}
l := len(doc)
if l < s.MinLen {
addFieldError(errs, "", fmt.Sprintf("has fewer properties than %d", s.MinLen))
return nil, errs
}
if s.MaxLen > 0 && l > s.MaxLen {
addFieldError(errs, "", fmt.Sprintf("has more properties than %d", s.MaxLen))
return nil, errs
}
return doc, errs
}