objectmodel 4.2.3 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,477 +1,480 @@
1
- import {
2
- bettertypeof, define, extend, getProto, has,
3
- is, isFunction, isObject, isPlainObject, isString,
4
- merge, ObjectProto, proxify, setProto
5
- } from "./helpers.js"
6
-
7
- export const
8
- _check = Symbol(),
9
- _checked = Symbol(), // used to skip validation at instanciation for perf
10
- _original = Symbol(), // used to bypass proxy
11
-
12
- initModel = (def, constructor, parent, init, getTraps, useNew) => {
13
- const model = function (val = model.default, mode) {
14
- if (useNew && !is(model, this)) return new model(val)
15
- if (init) val = init(val, model, this)
16
-
17
- if (mode === _checked || check(model, val))
18
- return getTraps ? proxify(val, getTraps(model)) : val
19
- }
20
-
21
- if (parent) extend(model, parent)
22
- setProto(model, constructor.prototype)
23
- model.constructor = constructor
24
- model.definition = def
25
- model.assertions = [...model.assertions]
26
- define(model, "errors", [])
27
- delete model.name
28
- return model
29
- },
30
-
31
- initObjectModel = (obj, model, _this) => {
32
- if (is(model, obj)) return obj
33
-
34
- if (!isObject(obj) && !isFunction(obj) && obj !== undefined) {
35
- stackError(model.errors, Object, obj)
36
- }
37
-
38
- merge(_this, model.default)
39
- if (model.parentClass) merge(obj, new model.parentClass(obj))
40
- merge(_this, obj)
41
- return _this
42
- },
43
-
44
- extendModel = (child, parent, newProps) => {
45
- extend(child, parent, newProps)
46
- child.assertions.push(...parent.assertions)
47
- return child
48
- },
49
-
50
- stackError = (errors, expected, received, path, message) => {
51
- errors.push({ expected, received, path, message })
52
- },
53
-
54
- unstackErrors = (model, collector = model.errorCollector) => {
55
- const nbErrors = model.errors.length
56
- if (nbErrors > 0) {
57
- const errors = model.errors.map(err => {
58
- if (!err.message) {
59
- err.message = "expecting " + (err.path ? err.path + " to be " : "") + formatDefinition(err.expected)
60
- + ", got " + (err.received != null ? bettertypeof(err.received) + " " : "") + format(err.received)
61
- }
62
- return err
63
- })
64
-
65
- model.errors.length = 0
66
- collector.call(model, errors) // throw all errors collected
67
- }
68
- return nbErrors
69
- },
70
-
71
- isModelInstance = i => i && getProto(i) && is(Model, getProto(i).constructor),
72
-
73
- parseDefinition = (def) => {
74
- if (isPlainObject(def)) {
75
- def = {}
76
- for (let key in def) { def[key] = parseDefinition(def[key]) }
77
- }
78
- else if (!Array.isArray(def)) return [def]
79
- else if (def.length === 1) return [def[0], undefined, null]
80
-
81
- return def
82
- },
83
-
84
- formatDefinition = (def, stack) => {
85
- const parts = parseDefinition(def).map(d => format(d, stack))
86
- return parts.length > 1 ? parts.join(" or ") : parts[0]
87
- },
88
-
89
- formatAssertions = fns => fns.length ? `(${fns.map(f => f.name || f.description || f)})` : "",
90
-
91
- extendDefinition = (def, newParts = []) => {
92
- if (newParts.length > 0) {
93
- def = [].concat(def, ...[].concat(newParts))// clone to lose ref
94
- .filter((value, index, self) => self.indexOf(value) === index) // remove duplicates
95
- }
96
-
97
- return def
98
- },
99
-
100
- check = (model, obj) => {
101
- model[_check](obj, null, model.errors, [], true);
102
- return !unstackErrors(model)
103
- },
104
-
105
- checkDefinition = (obj, def, path, errors, stack, shouldCast) => {
106
- const indexFound = stack.indexOf(def)
107
- if (indexFound !== -1 && stack.indexOf(def, indexFound + 1) !== -1)
108
- return obj // if found twice in call stack, cycle detected, skip validation
109
-
110
- if (Array.isArray(def) && def.length === 1 && obj != null) {
111
- def = def[0] // shorten validation path for optionals
112
- }
113
-
114
- if (is(Model, def)) {
115
- if (shouldCast) obj = cast(obj, def)
116
- def[_check](obj, path, errors, stack.concat(def))
117
- }
118
- else if (isPlainObject(def)) {
119
- for (let key in def) {
120
- const val = obj ? obj[key] : undefined
121
- checkDefinition(val, def[key], formatPath(path, key), errors, stack, shouldCast)
122
- }
123
- }
124
- else {
125
- const pdef = parseDefinition(def)
126
- if (pdef.some(part => checkDefinitionPart(obj, part, path, stack))) {
127
- return shouldCast ? cast(obj, def) : obj
128
- }
129
-
130
- stackError(errors, def, obj, path)
131
- }
132
-
133
- return obj
134
- },
135
-
136
- checkDefinitionPart = (obj, def, path, stack, shouldCast) => {
137
- if (def === Any) return true
138
- if (obj == null) return obj === def
139
- if (isPlainObject(def) || is(Model, def)) { // object or model as part of union type
140
- const errors = []
141
- checkDefinition(obj, def, path, errors, stack, shouldCast)
142
- return !errors.length
143
- }
144
- if (is(RegExp, def)) return def.test(obj)
145
- if (def === Number || def === Date) return obj.constructor === def && !isNaN(obj)
146
- return obj === def
147
- || (isFunction(def) && is(def, obj))
148
- || obj.constructor === def
149
- },
150
-
151
- checkAssertions = (obj, model, path, errors = model.errors) => {
152
- for (let assertion of model.assertions) {
153
- let result
154
- try {
155
- result = assertion.call(model, obj)
156
- } catch (err) {
157
- result = err
158
- }
159
- if (result !== true) {
160
- const onFail = isFunction(assertion.description) ? assertion.description : (assertionResult, value) =>
161
- `assertion "${assertion.description}" returned ${format(assertionResult)} `
162
- + `for ${path ? path + " =" : "value"} ${format(value)}`
163
- stackError(errors, assertion, obj, path, onFail.call(model, result, obj, path))
164
- }
165
- }
166
- },
167
-
168
- format = (obj, stack = []) => {
169
- if (stack.length > 15 || stack.includes(obj)) return "..."
170
- if (obj === null || obj === undefined) return String(obj)
171
- if (isString(obj)) return `"${obj}"`
172
- if (is(Model, obj)) return obj.toString(stack)
173
-
174
- stack.unshift(obj)
175
-
176
- if (isFunction(obj)) return obj.name || obj.toString()
177
- if (is(Map, obj) || is(Set, obj)) return format([...obj])
178
- if (Array.isArray(obj)) return `[${obj.map(item => format(item, stack)).join(", ")}]`
179
- if (obj.toString && obj.toString !== ObjectProto.toString) return obj.toString()
180
- if (isObject(obj)) {
181
- const props = Object.keys(obj),
182
- indent = "\t".repeat(stack.length)
183
- return `{${props.map(
184
- key => `\n${indent + key}: ${format(obj[key], [...stack])}`
185
- ).join(", ")} ${props.length ? `\n${indent.slice(1)}` : ""}}`
186
- }
187
-
188
- return String(obj)
189
- },
190
-
191
- formatPath = (path, key) => path ? path + "." + key : key,
192
-
193
- controlMutation = (model, def, path, o, key, privateAccess, applyMutation) => {
194
- const newPath = formatPath(path, key),
195
- isPrivate = model.conventionForPrivate(key),
196
- isConstant = model.conventionForConstant(key),
197
- isOwnProperty = has(o, key),
198
- initialPropDescriptor = isOwnProperty && Object.getOwnPropertyDescriptor(o, key)
199
-
200
- if (key in def && ((isPrivate && !privateAccess) || (isConstant && o[key] !== undefined)))
201
- cannot(`modify ${isPrivate ? "private" : "constant"} property ${key}`, model)
202
-
203
- applyMutation()
204
- if (has(def, key)) checkDefinition(o[key], def[key], newPath, model.errors, [])
205
- checkAssertions(o, model, newPath)
206
-
207
- const nbErrors = model.errors.length
208
- if (nbErrors) {
209
- if (isOwnProperty) Object.defineProperty(o, key, initialPropDescriptor)
210
- else delete o[key] // back to the initial property defined in prototype chain
211
-
212
- unstackErrors(model)
213
- }
214
-
215
- return !nbErrors
216
- },
217
-
218
- cannot = (msg, model) => {
219
- model.errors.push({ message: "cannot " + msg })
220
- },
221
-
222
- cast = (obj, defNode = []) => {
223
- if (!obj || isPlainObject(defNode) || is(BasicModel, defNode) || isModelInstance(obj))
224
- return obj // no value or not leaf or already a model instance
225
-
226
- const def = parseDefinition(defNode),
227
- suitableModels = []
228
-
229
- for (let part of def) {
230
- if (is(Model, part) && !is(BasicModel, part) && part.test(obj))
231
- suitableModels.push(part)
232
- }
233
-
234
- if (suitableModels.length === 1) {
235
- // automatically cast to suitable model when explicit (autocasting)
236
- return new suitableModels[0](obj, _checked)
237
- }
238
-
239
- if (suitableModels.length > 1)
240
- console.warn(`Ambiguous model for value ${format(obj)}, could be ${suitableModels.join(" or ")}`)
241
-
242
- return obj
243
- },
244
-
245
-
246
- getProp = (val, model, def, path, privateAccess) => {
247
- if (!isPlainObject(def)) return cast(val, def)
248
- return proxify(val, getTraps(model, def, path, privateAccess))
249
- },
250
-
251
- getTraps = (model, def, path, privateAccess) => {
252
- const grantPrivateAccess = f => proxify(f, {
253
- apply(fn, ctx, args) {
254
- privateAccess = true
255
- const result = Reflect.apply(fn, ctx, args)
256
- privateAccess = false
257
- return result
258
- }
259
- })
260
-
261
- return {
262
- get(o, key) {
263
- if (key === _original) return o
264
-
265
- if (!isString(key)) return Reflect.get(o, key)
266
-
267
- const newPath = formatPath(path, key)
268
- const inDef = has(def, key)
269
- const defPart = def[key]
270
-
271
- if (!privateAccess && inDef && model.conventionForPrivate(key)) {
272
- cannot(`access to private property ${newPath}`, model)
273
- unstackErrors(model)
274
- return
275
- }
276
-
277
- let value = o[key]
278
-
279
- if (inDef && value && has(o, key) && !isPlainObject(defPart) && !isModelInstance(value)) {
280
- Reflect.set(o, key, value = cast(value, defPart)) // cast nested models
281
- }
282
-
283
- if (isFunction(value) && key !== "constructor" && !privateAccess) {
284
- return grantPrivateAccess(value)
285
- }
286
-
287
- if (isPlainObject(defPart) && !value) {
288
- o[key] = value = {} // null-safe traversal
289
- }
290
-
291
- return getProp(value, model, defPart, newPath, privateAccess)
292
- },
293
-
294
- set(o, key, val) {
295
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.set(o, key, cast(val, def[key])))
296
- },
297
-
298
- deleteProperty(o, key) {
299
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.deleteProperty(o, key))
300
- },
301
-
302
- defineProperty(o, key, args) {
303
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.defineProperty(o, key, args))
304
- },
305
-
306
- has(o, key) {
307
- return Reflect.has(o, key) && Reflect.has(def, key) && !model.conventionForPrivate(key)
308
- },
309
-
310
- ownKeys(o) {
311
- return Reflect.ownKeys(o).filter(key => Reflect.has(def, key) && !model.conventionForPrivate(key))
312
- },
313
-
314
- getOwnPropertyDescriptor(o, key) {
315
- let descriptor
316
- if (!model.conventionForPrivate(key)) {
317
- descriptor = Object.getOwnPropertyDescriptor(def, key)
318
- if (descriptor !== undefined) descriptor.value = o[key]
319
- }
320
-
321
- return descriptor
322
- }
323
- }
324
- }
325
-
326
-
327
- export function Model(def, params) {
328
- return isPlainObject(def) ? new ObjectModel(def, params) : new BasicModel(def)
329
- }
330
-
331
- Object.assign(Model.prototype, {
332
- name: "Model",
333
- assertions: [],
334
-
335
- conventionForConstant: key => key.toUpperCase() === key,
336
- conventionForPrivate: key => key[0] === "_",
337
-
338
- toString(stack) {
339
- return has(this, "name") ? this.name : formatDefinition(this.definition, stack) + formatAssertions(this.assertions)
340
- },
341
-
342
- as(name) {
343
- define(this, "name", name)
344
- return this
345
- },
346
-
347
- defaultTo(val) {
348
- this.default = val
349
- return this
350
- },
351
-
352
- [_check](obj, path, errors, stack) {
353
- checkDefinition(obj, this.definition, path, errors, stack)
354
- checkAssertions(obj, this, path, errors)
355
- },
356
-
357
- test(obj, errorCollector) {
358
- let model = this
359
- while (!has(model, "errorCollector")) {
360
- model = getProto(model)
361
- }
362
-
363
- const initialErrorCollector = model.errorCollector
364
- let failed
365
-
366
- model.errorCollector = errors => {
367
- failed = true
368
- if (errorCollector) errorCollector.call(this, errors)
369
- }
370
-
371
- new this(obj) // may trigger errorCollector
372
-
373
- model.errorCollector = initialErrorCollector
374
- return !failed
375
- },
376
-
377
- errorCollector(errors) {
378
- const e = new TypeError(errors.map(e => e.message).join("\n"))
379
- e.stack = e.stack.replace(/\n.*object-model(.|\n)*object-model.*/, "") // blackbox objectmodel in stacktrace
380
- throw e
381
- },
382
-
383
- assert(assertion, description = format(assertion)) {
384
- define(assertion, "description", description)
385
- this.assertions = this.assertions.concat(assertion)
386
- return this
387
- }
388
- })
389
-
390
-
391
- export function BasicModel(def) {
392
- return initModel(def, BasicModel)
393
- }
394
-
395
- extend(BasicModel, Model, {
396
- extend(...newParts) {
397
- const child = extendModel(new BasicModel(extendDefinition(this.definition, newParts)), this)
398
- for (let part of newParts) {
399
- if (is(BasicModel, part)) child.assertions.push(...part.assertions)
400
- }
401
-
402
- return child
403
- }
404
- })
405
-
406
- export function ObjectModel(def) {
407
- return initModel(def, ObjectModel, Object, initObjectModel, model => getTraps(model, def), true)
408
- }
409
-
410
- extend(ObjectModel, Model, {
411
- defaultTo(obj) {
412
- const def = this.definition
413
- for (let key in obj) {
414
- if (has(def, key)) {
415
- obj[key] = checkDefinition(obj[key], def[key], key, this.errors, [], true)
416
- }
417
- }
418
- unstackErrors(this)
419
- this.default = obj;
420
- return this
421
- },
422
-
423
- toString(stack) {
424
- return format(this.definition, stack)
425
- },
426
-
427
- extend(...newParts) {
428
- const definition = { ...this.definition }
429
- const proto = { ...this.prototype }
430
- const defaults = { ...this.default }
431
- const newAssertions = []
432
-
433
- for (let part of newParts) {
434
- if (is(Model, part)) {
435
- merge(definition, part.definition)
436
- merge(defaults, part.default)
437
- newAssertions.push(...part.assertions)
438
- }
439
- if (isFunction(part)) merge(proto, part.prototype)
440
- if (isObject(part)) merge(definition, part)
441
- }
442
-
443
- const submodel = extendModel(new ObjectModel(definition), this, proto).defaultTo(defaults)
444
- submodel.assertions = [...this.assertions, ...newAssertions]
445
-
446
- if (getProto(this) !== ObjectModel.prototype) { // extended class
447
- submodel.parentClass = this
448
- }
449
-
450
- return submodel
451
- },
452
-
453
- [_check](obj, path, errors, stack, shouldCast) {
454
- if (isObject(obj)) {
455
- checkDefinition(obj[_original] || obj, this.definition, path, errors, stack, shouldCast)
456
- }
457
- else stackError(errors, this, obj, path)
458
-
459
- checkAssertions(obj, this, path, errors)
460
- }
461
- })
462
-
463
- export const Any = proxify(BasicModel(), {
464
- apply(target, ctx, [def]) {
465
- const anyOf = Object.create(Any)
466
- anyOf.definition = def;
467
- return anyOf
468
- }
469
- })
470
- Any.definition = Any
471
- Any.toString = () => "Any"
472
-
473
- Any.remaining = function (def) { this.definition = def }
474
- extend(Any.remaining, Any, {
475
- toString() { return "..." + formatDefinition(this.definition) }
476
- })
1
+ import {
2
+ bettertypeof, define, extend, getProto, has,
3
+ is, isFunction, isObject, isPlainObject, isString,
4
+ merge, ObjectProto, proxify, setProto
5
+ } from "./helpers.js"
6
+
7
+ export const
8
+ _check = Symbol(),
9
+ _checked = Symbol(), // used to skip validation at instanciation for perf
10
+ _original = Symbol(), // used to bypass proxy
11
+ CHECK_ONCE = Symbol(),
12
+
13
+ initModel = (def, constructor, parent, init, getTraps, useNew) => {
14
+ const model = function (val = model.default, mode) {
15
+ if (useNew && !is(model, this)) return new model(val)
16
+ if (init) val = init(val, model, this)
17
+
18
+ if (mode === _checked || check(model, val))
19
+ return getTraps && mode !== CHECK_ONCE ? proxify(val, getTraps(model)) : val
20
+ }
21
+
22
+ if (parent) extend(model, parent)
23
+ setProto(model, constructor.prototype)
24
+ model.constructor = constructor
25
+ model.definition = def
26
+ model.assertions = [...model.assertions]
27
+ define(model, "errors", [])
28
+ delete model.name
29
+ return model
30
+ },
31
+
32
+ initObjectModel = (obj, model, _this) => {
33
+ if (is(model, obj)) return obj
34
+
35
+ if (!isObject(obj) && !isFunction(obj) && obj !== undefined) {
36
+ // short circuit validation if not receiving an object as expected
37
+ return obj
38
+ }
39
+
40
+ merge(_this, model.default)
41
+ if (model.parentClass) merge(obj, new model.parentClass(obj))
42
+ merge(_this, obj)
43
+ return _this
44
+ },
45
+
46
+ extendModel = (child, parent, newProps) => {
47
+ extend(child, parent, newProps)
48
+ child.assertions.push(...parent.assertions)
49
+ return child
50
+ },
51
+
52
+ stackError = (errors, expected, received, path, message) => {
53
+ errors.push({ expected, received, path, message })
54
+ },
55
+
56
+ unstackErrors = (model, collector = model.errorCollector) => {
57
+ const nbErrors = model.errors.length
58
+ if (nbErrors > 0) {
59
+ const errors = model.errors.map(err => {
60
+ if (!err.message) {
61
+ err.message = "expecting " + (err.path ? err.path + " to be " : "") + formatDefinition(err.expected)
62
+ + ", got " + (err.received != null ? bettertypeof(err.received) + " " : "") + format(err.received)
63
+ }
64
+ return err
65
+ })
66
+
67
+ model.errors.length = 0
68
+ collector.call(model, errors) // throw all errors collected
69
+ }
70
+ return nbErrors
71
+ },
72
+
73
+ isModelInstance = i => i && getProto(i) && is(Model, getProto(i).constructor),
74
+
75
+ parseDefinition = (def) => {
76
+ if (isPlainObject(def)) {
77
+ def = {}
78
+ for (let key in def) { def[key] = parseDefinition(def[key]) }
79
+ }
80
+ else if (!Array.isArray(def)) return [def]
81
+ else if (def.length === 1) return [def[0], undefined, null]
82
+
83
+ return def
84
+ },
85
+
86
+ formatDefinition = (def, stack) => {
87
+ const parts = parseDefinition(def).map(d => format(d, stack))
88
+ return parts.length > 1 ? parts.join(" or ") : parts[0]
89
+ },
90
+
91
+ formatAssertions = fns => fns.length ? `(${fns.map(f => f.name || f.description || f)})` : "",
92
+
93
+ extendDefinition = (def, newParts = []) => {
94
+ if (newParts.length > 0) {
95
+ def = [].concat(def, ...[].concat(newParts))// clone to lose ref
96
+ .filter((value, index, self) => self.indexOf(value) === index) // remove duplicates
97
+ }
98
+
99
+ return def
100
+ },
101
+
102
+ check = (model, obj) => {
103
+ model[_check](obj, null, model.errors, [], true);
104
+ return !unstackErrors(model)
105
+ },
106
+
107
+ checkDefinition = (obj, def, path, errors, stack, shouldCast) => {
108
+ const indexFound = stack.indexOf(def)
109
+ if (indexFound !== -1 && stack.indexOf(def, indexFound + 1) !== -1)
110
+ return obj // if found twice in call stack, cycle detected, skip validation
111
+
112
+ if (Array.isArray(def) && def.length === 1 && obj != null) {
113
+ def = def[0] // shorten validation path for optionals
114
+ }
115
+
116
+ if (is(Model, def)) {
117
+ if (shouldCast) obj = cast(obj, def)
118
+ def[_check](obj, path, errors, stack.concat(def))
119
+ }
120
+ else if (isPlainObject(def)) {
121
+ for (let key in def) {
122
+ const val = obj ? obj[key] : undefined
123
+ checkDefinition(val, def[key], formatPath(path, key), errors, stack, shouldCast)
124
+ }
125
+ }
126
+ else {
127
+ const pdef = parseDefinition(def)
128
+ if (pdef.some(part => checkDefinitionPart(obj, part, path, stack))) {
129
+ return shouldCast ? cast(obj, def) : obj
130
+ }
131
+
132
+ stackError(errors, def, obj, path)
133
+ }
134
+
135
+ return obj
136
+ },
137
+
138
+ checkDefinitionPart = (obj, def, path, stack, shouldCast) => {
139
+ if (def === Any) return true
140
+ if (obj == null) return obj === def
141
+ if (isPlainObject(def) || is(Model, def)) { // object or model as part of union type
142
+ const errors = []
143
+ checkDefinition(obj, def, path, errors, stack, shouldCast)
144
+ return !errors.length
145
+ }
146
+ if (is(RegExp, def)) return def.test(obj)
147
+ if (def === Number || def === Date) return obj.constructor === def && !isNaN(obj)
148
+ return obj === def
149
+ || (isFunction(def) && is(def, obj))
150
+ || obj.constructor === def
151
+ },
152
+
153
+ checkAssertions = (obj, model, path, errors = model.errors) => {
154
+ for (let assertion of model.assertions) {
155
+ let result
156
+ try {
157
+ result = assertion.call(model, obj)
158
+ } catch (err) {
159
+ result = err
160
+ }
161
+ if (result !== true) {
162
+ const onFail = isFunction(assertion.description) ? assertion.description : (assertionResult, value) =>
163
+ `assertion "${assertion.description}" returned ${format(assertionResult)} `
164
+ + `for ${path ? path + " =" : "value"} ${format(value)}`
165
+ stackError(errors, assertion, obj, path, onFail.call(model, result, obj, path))
166
+ }
167
+ }
168
+ },
169
+
170
+ format = (obj, stack = []) => {
171
+ if (stack.length > 15 || stack.includes(obj)) return "..."
172
+ if (obj === null || obj === undefined) return String(obj)
173
+ if (isString(obj)) return `"${obj}"`
174
+ if (is(Model, obj)) return obj.toString(stack)
175
+
176
+ stack.unshift(obj)
177
+
178
+ if (isFunction(obj)) return obj.name || obj.toString()
179
+ if (is(Map, obj) || is(Set, obj)) return format([...obj])
180
+ if (Array.isArray(obj)) return `[${obj.map(item => format(item, stack)).join(", ")}]`
181
+ if (obj.toString && obj.toString !== ObjectProto.toString) return obj.toString()
182
+ if (isObject(obj)) {
183
+ const props = Object.keys(obj),
184
+ indent = "\t".repeat(stack.length)
185
+ return `{${props.map(
186
+ key => `\n${indent + key}: ${format(obj[key], [...stack])}`
187
+ ).join(", ")} ${props.length ? `\n${indent.slice(1)}` : ""}}`
188
+ }
189
+
190
+ return String(obj)
191
+ },
192
+
193
+ formatPath = (path, key) => path ? path + "." + key : key,
194
+
195
+ controlMutation = (model, def, path, o, key, privateAccess, applyMutation) => {
196
+ const newPath = formatPath(path, key),
197
+ isPrivate = model.conventionForPrivate(key),
198
+ isConstant = model.conventionForConstant(key),
199
+ isOwnProperty = has(o, key),
200
+ initialPropDescriptor = isOwnProperty && Object.getOwnPropertyDescriptor(o, key)
201
+
202
+ if (key in def && ((isPrivate && !privateAccess) || (isConstant && o[key] !== undefined)))
203
+ cannot(`modify ${isPrivate ? "private" : "constant"} property ${key}`, model)
204
+
205
+ applyMutation()
206
+ if (has(def, key)) checkDefinition(o[key], def[key], newPath, model.errors, [])
207
+ checkAssertions(o, model, newPath)
208
+
209
+ const nbErrors = model.errors.length
210
+ if (nbErrors) {
211
+ if (isOwnProperty) Object.defineProperty(o, key, initialPropDescriptor)
212
+ else delete o[key] // back to the initial property defined in prototype chain
213
+
214
+ unstackErrors(model)
215
+ }
216
+
217
+ return !nbErrors
218
+ },
219
+
220
+ cannot = (msg, model) => {
221
+ model.errors.push({ message: "cannot " + msg })
222
+ },
223
+
224
+ cast = (obj, defNode = []) => {
225
+ if (!obj || isPlainObject(defNode) || is(BasicModel, defNode) || isModelInstance(obj))
226
+ return obj // no value or not leaf or already a model instance
227
+
228
+ const def = parseDefinition(defNode),
229
+ suitableModels = []
230
+
231
+ for (let part of def) {
232
+ if (is(Model, part) && !is(BasicModel, part) && part.test(obj))
233
+ suitableModels.push(part)
234
+ }
235
+
236
+ if (suitableModels.length === 1) {
237
+ // automatically cast to suitable model when explicit (autocasting)
238
+ return new suitableModels[0](obj, _checked)
239
+ }
240
+
241
+ if (suitableModels.length > 1)
242
+ console.warn(`Ambiguous model for value ${format(obj)}, could be ${suitableModels.join(" or ")}`)
243
+
244
+ return obj
245
+ },
246
+
247
+
248
+ getProp = (val, model, def, path, privateAccess) => {
249
+ if (!isPlainObject(def)) return cast(val, def)
250
+ return proxify(val, getTraps(model, def, path, privateAccess))
251
+ },
252
+
253
+ getTraps = (model, def, path, privateAccess) => {
254
+ const grantPrivateAccess = f => proxify(f, {
255
+ apply(fn, ctx, args) {
256
+ privateAccess = true
257
+ const result = Reflect.apply(fn, ctx, args)
258
+ privateAccess = false
259
+ return result
260
+ }
261
+ })
262
+
263
+ return {
264
+ get(o, key) {
265
+ if (key === _original) return o
266
+
267
+ if (!isString(key)) return Reflect.get(o, key)
268
+
269
+ const newPath = formatPath(path, key)
270
+ const inDef = has(def, key)
271
+ const defPart = def[key]
272
+
273
+ if (!privateAccess && inDef && model.conventionForPrivate(key)) {
274
+ cannot(`access to private property ${newPath}`, model)
275
+ unstackErrors(model)
276
+ return
277
+ }
278
+
279
+ let value = o[key]
280
+
281
+ if (inDef && value && has(o, key) && !isPlainObject(defPart) && !isModelInstance(value)) {
282
+ Reflect.set(o, key, value = cast(value, defPart)) // cast nested models
283
+ }
284
+
285
+ if (isFunction(value) && key !== "constructor" && !privateAccess) {
286
+ return grantPrivateAccess(value)
287
+ }
288
+
289
+ if (isPlainObject(defPart) && !value) {
290
+ o[key] = value = {} // null-safe traversal
291
+ }
292
+
293
+ return getProp(value, model, defPart, newPath, privateAccess)
294
+ },
295
+
296
+ set(o, key, val) {
297
+ return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.set(o, key, cast(val, def[key])))
298
+ },
299
+
300
+ deleteProperty(o, key) {
301
+ return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.deleteProperty(o, key))
302
+ },
303
+
304
+ defineProperty(o, key, args) {
305
+ return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.defineProperty(o, key, args))
306
+ },
307
+
308
+ has(o, key) {
309
+ return Reflect.has(o, key) && Reflect.has(def, key) && !model.conventionForPrivate(key)
310
+ },
311
+
312
+ ownKeys(o) {
313
+ return Reflect.ownKeys(o).filter(key => Reflect.has(def, key) && !model.conventionForPrivate(key))
314
+ },
315
+
316
+ getOwnPropertyDescriptor(o, key) {
317
+ let descriptor
318
+ if (!model.conventionForPrivate(key)) {
319
+ descriptor = Object.getOwnPropertyDescriptor(def, key)
320
+ if (descriptor !== undefined) descriptor.value = o[key]
321
+ }
322
+
323
+ return descriptor
324
+ }
325
+ }
326
+ }
327
+
328
+
329
+ export function Model(def) {
330
+ return isPlainObject(def) ? new ObjectModel(def) : new BasicModel(def)
331
+ }
332
+
333
+ Object.assign(Model.prototype, {
334
+ name: "Model",
335
+ assertions: [],
336
+
337
+ conventionForConstant: key => key.toUpperCase() === key,
338
+ conventionForPrivate: key => key[0] === "_",
339
+
340
+ toString(stack) {
341
+ return has(this, "name") ? this.name : formatDefinition(this.definition, stack) + formatAssertions(this.assertions)
342
+ },
343
+
344
+ as(name) {
345
+ define(this, "name", name)
346
+ return this
347
+ },
348
+
349
+ defaultTo(val) {
350
+ this.default = val
351
+ return this
352
+ },
353
+
354
+ [_check](obj, path, errors, stack) {
355
+ checkDefinition(obj, this.definition, path, errors, stack)
356
+ checkAssertions(obj, this, path, errors)
357
+ },
358
+
359
+ test(obj, errorCollector) {
360
+ let model = this
361
+ while (!has(model, "errorCollector")) {
362
+ model = getProto(model)
363
+ }
364
+
365
+ const initialErrorCollector = model.errorCollector
366
+ let failed
367
+
368
+ model.errorCollector = errors => {
369
+ failed = true
370
+ if (errorCollector) errorCollector.call(this, errors)
371
+ }
372
+
373
+ new this(obj) // may trigger errorCollector
374
+
375
+ model.errorCollector = initialErrorCollector
376
+ return !failed
377
+ },
378
+
379
+ errorCollector(errors) {
380
+ const e = new TypeError(errors.map(e => e.message).join("\n"))
381
+ e.stack = e.stack.replace(/\n.*object-model(.|\n)*object-model.*/, "") // blackbox objectmodel in stacktrace
382
+ throw e
383
+ },
384
+
385
+ assert(assertion, description = format(assertion)) {
386
+ define(assertion, "description", description)
387
+ this.assertions = this.assertions.concat(assertion)
388
+ return this
389
+ }
390
+ })
391
+
392
+ Model.CHECK_ONCE = CHECK_ONCE
393
+
394
+ export function BasicModel(def) {
395
+ return initModel(def, BasicModel)
396
+ }
397
+
398
+ extend(BasicModel, Model, {
399
+ extend(...newParts) {
400
+ const child = extendModel(new BasicModel(extendDefinition(this.definition, newParts)), this)
401
+ for (let part of newParts) {
402
+ if (is(BasicModel, part)) child.assertions.push(...part.assertions)
403
+ }
404
+
405
+ return child
406
+ }
407
+ })
408
+
409
+ export function ObjectModel(def) {
410
+ return initModel(def, ObjectModel, Object, initObjectModel, model => getTraps(model, def), true)
411
+ }
412
+
413
+ extend(ObjectModel, Model, {
414
+ defaultTo(obj) {
415
+ const def = this.definition
416
+ for (let key in obj) {
417
+ if (has(def, key)) {
418
+ obj[key] = checkDefinition(obj[key], def[key], key, this.errors, [], true)
419
+ }
420
+ }
421
+ unstackErrors(this)
422
+ this.default = obj;
423
+ return this
424
+ },
425
+
426
+ toString(stack) {
427
+ return format(this.definition, stack)
428
+ },
429
+
430
+ extend(...newParts) {
431
+ const definition = { ...this.definition }
432
+ const proto = { ...this.prototype }
433
+ const defaults = { ...this.default }
434
+ const newAssertions = []
435
+
436
+ for (let part of newParts) {
437
+ if (is(Model, part)) {
438
+ merge(definition, part.definition)
439
+ merge(defaults, part.default)
440
+ newAssertions.push(...part.assertions)
441
+ }
442
+ if (isFunction(part)) merge(proto, part.prototype)
443
+ if (isObject(part)) merge(definition, part)
444
+ }
445
+
446
+ const submodel = extendModel(new ObjectModel(definition), this, proto).defaultTo(defaults)
447
+ submodel.assertions = [...this.assertions, ...newAssertions]
448
+
449
+ if (getProto(this) !== ObjectModel.prototype) { // extended class
450
+ submodel.parentClass = this
451
+ }
452
+
453
+ return submodel
454
+ },
455
+
456
+ [_check](obj, path, errors, stack, shouldCast) {
457
+ if (isObject(obj)) {
458
+ checkDefinition(obj[_original] || obj, this.definition, path, errors, stack, shouldCast)
459
+ }
460
+ else stackError(errors, this, obj, path)
461
+
462
+ checkAssertions(obj, this, path, errors)
463
+ }
464
+ })
465
+
466
+ export const Any = proxify(BasicModel(), {
467
+ apply(target, ctx, [def]) {
468
+ const anyOf = Object.create(Any)
469
+ anyOf.definition = def;
470
+ return anyOf
471
+ }
472
+ })
473
+ Any.definition = Any
474
+ Any.toString = () => "Any"
475
+
476
+ Any.remaining = function (def) { this.definition = def }
477
+ extend(Any.remaining, Any, {
478
+ toString() { return "..." + formatDefinition(this.definition) }
479
+ })
477
480
  Any[Symbol.iterator] = function* () { yield new Any.remaining(this.definition) }