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,4 +1,4 @@
1
- // ObjectModel v4.2.2 - http://objectmodel.js.org
1
+ // ObjectModel v4.3.1 - http://objectmodel.js.org
2
2
  // MIT License - Sylvain Pollet-Villard
3
3
  const
4
4
  ObjectProto = Object.prototype,
@@ -45,476 +45,479 @@ const
45
45
  setProto(child, parent);
46
46
  };
47
47
 
48
- const
49
- _check = Symbol(),
50
- _checked = Symbol(), // used to skip validation at instanciation for perf
51
- _original = Symbol(), // used to bypass proxy
52
-
53
- initModel = (def, constructor, parent, init, getTraps, useNew) => {
54
- const model = function (val = model.default, mode) {
55
- if (useNew && !is(model, this)) return new model(val)
56
- if (init) val = init(val, model, this);
57
-
58
- if (mode === _checked || check(model, val))
59
- return getTraps ? proxify(val, getTraps(model)) : val
60
- };
61
-
62
- if (parent) extend(model, parent);
63
- setProto(model, constructor.prototype);
64
- model.constructor = constructor;
65
- model.definition = def;
66
- model.assertions = [...model.assertions];
67
- define(model, "errors", []);
68
- delete model.name;
69
- return model
70
- },
71
-
72
- initObjectModel = (obj, model, _this) => {
73
- if (is(model, obj)) return obj
74
-
75
- if (!isObject(obj) && !isFunction(obj) && obj !== undefined) {
76
- stackError(model.errors, Object, obj);
77
- }
78
-
79
- merge(_this, model.default);
80
- if (model.parentClass) merge(obj, new model.parentClass(obj));
81
- merge(_this, obj);
82
- return _this
83
- },
84
-
85
- extendModel = (child, parent, newProps) => {
86
- extend(child, parent, newProps);
87
- child.assertions.push(...parent.assertions);
88
- return child
89
- },
90
-
91
- stackError = (errors, expected, received, path, message) => {
92
- errors.push({ expected, received, path, message });
93
- },
94
-
95
- unstackErrors = (model, collector = model.errorCollector) => {
96
- const nbErrors = model.errors.length;
97
- if (nbErrors > 0) {
98
- const errors = model.errors.map(err => {
99
- if (!err.message) {
100
- err.message = "expecting " + (err.path ? err.path + " to be " : "") + formatDefinition(err.expected)
101
- + ", got " + (err.received != null ? bettertypeof(err.received) + " " : "") + format(err.received);
102
- }
103
- return err
104
- });
105
-
106
- model.errors.length = 0;
107
- collector.call(model, errors); // throw all errors collected
108
- }
109
- return nbErrors
110
- },
111
-
112
- isModelInstance = i => i && getProto(i) && is(Model, getProto(i).constructor),
113
-
114
- parseDefinition = (def) => {
115
- if (isPlainObject(def)) {
116
- def = {};
117
- for (let key in def) { def[key] = parseDefinition(def[key]); }
118
- }
119
- else if (!Array.isArray(def)) return [def]
120
- else if (def.length === 1) return [def[0], undefined, null]
121
-
122
- return def
123
- },
124
-
125
- formatDefinition = (def, stack) => {
126
- const parts = parseDefinition(def).map(d => format(d, stack));
127
- return parts.length > 1 ? parts.join(" or ") : parts[0]
128
- },
129
-
130
- formatAssertions = fns => fns.length ? `(${fns.map(f => f.name || f.description || f)})` : "",
131
-
132
- extendDefinition = (def, newParts = []) => {
133
- if (newParts.length > 0) {
134
- def = [].concat(def, ...[].concat(newParts))// clone to lose ref
135
- .filter((value, index, self) => self.indexOf(value) === index); // remove duplicates
136
- }
137
-
138
- return def
139
- },
140
-
141
- check = (model, obj) => {
142
- model[_check](obj, null, model.errors, [], true);
143
- return !unstackErrors(model)
144
- },
145
-
146
- checkDefinition = (obj, def, path, errors, stack, shouldCast) => {
147
- const indexFound = stack.indexOf(def);
148
- if (indexFound !== -1 && stack.indexOf(def, indexFound + 1) !== -1)
149
- return obj // if found twice in call stack, cycle detected, skip validation
150
-
151
- if (Array.isArray(def) && def.length === 1 && obj != null) {
152
- def = def[0]; // shorten validation path for optionals
153
- }
154
-
155
- if (is(Model, def)) {
156
- if (shouldCast) obj = cast(obj, def);
157
- def[_check](obj, path, errors, stack.concat(def));
158
- }
159
- else if (isPlainObject(def)) {
160
- for (let key in def) {
161
- const val = obj ? obj[key] : undefined;
162
- checkDefinition(val, def[key], formatPath(path, key), errors, stack, shouldCast);
163
- }
164
- }
165
- else {
166
- const pdef = parseDefinition(def);
167
- if (pdef.some(part => checkDefinitionPart(obj, part, path, stack))) {
168
- return shouldCast ? cast(obj, def) : obj
169
- }
170
-
171
- stackError(errors, def, obj, path);
172
- }
173
-
174
- return obj
175
- },
176
-
177
- checkDefinitionPart = (obj, def, path, stack, shouldCast) => {
178
- if (def === Any) return true
179
- if (obj == null) return obj === def
180
- if (isPlainObject(def) || is(Model, def)) { // object or model as part of union type
181
- const errors = [];
182
- checkDefinition(obj, def, path, errors, stack, shouldCast);
183
- return !errors.length
184
- }
185
- if (is(RegExp, def)) return def.test(obj)
186
- if (def === Number || def === Date) return obj.constructor === def && !isNaN(obj)
187
- return obj === def
188
- || (isFunction(def) && is(def, obj))
189
- || obj.constructor === def
190
- },
191
-
192
- checkAssertions = (obj, model, path, errors = model.errors) => {
193
- for (let assertion of model.assertions) {
194
- let result;
195
- try {
196
- result = assertion.call(model, obj);
197
- } catch (err) {
198
- result = err;
199
- }
200
- if (result !== true) {
201
- const onFail = isFunction(assertion.description) ? assertion.description : (assertionResult, value) =>
202
- `assertion "${assertion.description}" returned ${format(assertionResult)} `
203
- + `for ${path ? path + " =" : "value"} ${format(value)}`;
204
- stackError(errors, assertion, obj, path, onFail.call(model, result, obj, path));
205
- }
206
- }
207
- },
208
-
209
- format = (obj, stack = []) => {
210
- if (stack.length > 15 || stack.includes(obj)) return "..."
211
- if (obj === null || obj === undefined) return String(obj)
212
- if (isString(obj)) return `"${obj}"`
213
- if (is(Model, obj)) return obj.toString(stack)
214
-
215
- stack.unshift(obj);
216
-
217
- if (isFunction(obj)) return obj.name || obj.toString()
218
- if (is(Map, obj) || is(Set, obj)) return format([...obj])
219
- if (Array.isArray(obj)) return `[${obj.map(item => format(item, stack)).join(", ")}]`
220
- if (obj.toString && obj.toString !== ObjectProto.toString) return obj.toString()
221
- if (isObject(obj)) {
222
- const props = Object.keys(obj),
223
- indent = "\t".repeat(stack.length);
224
- return `{${props.map(
225
- key => `\n${indent + key}: ${format(obj[key], [...stack])}`
226
- ).join(", ")} ${props.length ? `\n${indent.slice(1)}` : ""}}`
227
- }
228
-
229
- return String(obj)
230
- },
231
-
232
- formatPath = (path, key) => path ? path + "." + key : key,
233
-
234
- controlMutation = (model, def, path, o, key, privateAccess, applyMutation) => {
235
- const newPath = formatPath(path, key),
236
- isPrivate = model.conventionForPrivate(key),
237
- isConstant = model.conventionForConstant(key),
238
- isOwnProperty = has(o, key),
239
- initialPropDescriptor = isOwnProperty && Object.getOwnPropertyDescriptor(o, key);
240
-
241
- if (key in def && ((isPrivate && !privateAccess) || (isConstant && o[key] !== undefined)))
242
- cannot(`modify ${isPrivate ? "private" : "constant"} property ${key}`, model);
243
-
244
- applyMutation();
245
- if (has(def, key)) checkDefinition(o[key], def[key], newPath, model.errors, []);
246
- checkAssertions(o, model, newPath);
247
-
248
- const nbErrors = model.errors.length;
249
- if (nbErrors) {
250
- if (isOwnProperty) Object.defineProperty(o, key, initialPropDescriptor);
251
- else delete o[key]; // back to the initial property defined in prototype chain
252
-
253
- unstackErrors(model);
254
- }
255
-
256
- return !nbErrors
257
- },
258
-
259
- cannot = (msg, model) => {
260
- model.errors.push({ message: "cannot " + msg });
261
- },
262
-
263
- cast = (obj, defNode = []) => {
264
- if (!obj || isPlainObject(defNode) || is(BasicModel, defNode) || isModelInstance(obj))
265
- return obj // no value or not leaf or already a model instance
266
-
267
- const def = parseDefinition(defNode),
268
- suitableModels = [];
269
-
270
- for (let part of def) {
271
- if (is(Model, part) && !is(BasicModel, part) && part.test(obj))
272
- suitableModels.push(part);
273
- }
274
-
275
- if (suitableModels.length === 1) {
276
- // automatically cast to suitable model when explicit (autocasting)
277
- return new suitableModels[0](obj, _checked)
278
- }
279
-
280
- if (suitableModels.length > 1)
281
- console.warn(`Ambiguous model for value ${format(obj)}, could be ${suitableModels.join(" or ")}`);
282
-
283
- return obj
284
- },
285
-
286
-
287
- getProp = (val, model, def, path, privateAccess) => {
288
- if (!isPlainObject(def)) return cast(val, def)
289
- return proxify(val, getTraps(model, def, path, privateAccess))
290
- },
291
-
292
- getTraps = (model, def, path, privateAccess) => {
293
- const grantPrivateAccess = f => proxify(f, {
294
- apply(fn, ctx, args) {
295
- privateAccess = true;
296
- const result = Reflect.apply(fn, ctx, args);
297
- privateAccess = false;
298
- return result
299
- }
300
- });
301
-
302
- return {
303
- get(o, key) {
304
- if (key === _original) return o
305
-
306
- if (!isString(key)) return Reflect.get(o, key)
307
-
308
- const newPath = formatPath(path, key);
309
- const inDef = has(def, key);
310
- const defPart = def[key];
311
-
312
- if (!privateAccess && inDef && model.conventionForPrivate(key)) {
313
- cannot(`access to private property ${newPath}`, model);
314
- unstackErrors(model);
315
- return
316
- }
317
-
318
- let value = o[key];
319
-
320
- if (inDef && value && has(o, key) && !isPlainObject(defPart) && !isModelInstance(value)) {
321
- Reflect.set(o, key, value = cast(value, defPart)); // cast nested models
322
- }
323
-
324
- if (isFunction(value) && key !== "constructor" && !privateAccess) {
325
- return grantPrivateAccess(value)
326
- }
327
-
328
- if (isPlainObject(defPart) && !value) {
329
- o[key] = value = {}; // null-safe traversal
330
- }
331
-
332
- return getProp(value, model, defPart, newPath, privateAccess)
333
- },
334
-
335
- set(o, key, val) {
336
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.set(o, key, cast(val, def[key])))
337
- },
338
-
339
- deleteProperty(o, key) {
340
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.deleteProperty(o, key))
341
- },
342
-
343
- defineProperty(o, key, args) {
344
- return controlMutation(model, def, path, o, key, privateAccess, () => Reflect.defineProperty(o, key, args))
345
- },
346
-
347
- has(o, key) {
348
- return Reflect.has(o, key) && Reflect.has(def, key) && !model.conventionForPrivate(key)
349
- },
350
-
351
- ownKeys(o) {
352
- return Reflect.ownKeys(o).filter(key => Reflect.has(def, key) && !model.conventionForPrivate(key))
353
- },
354
-
355
- getOwnPropertyDescriptor(o, key) {
356
- let descriptor;
357
- if (!model.conventionForPrivate(key)) {
358
- descriptor = Object.getOwnPropertyDescriptor(def, key);
359
- if (descriptor !== undefined) descriptor.value = o[key];
360
- }
361
-
362
- return descriptor
363
- }
364
- }
365
- };
366
-
367
-
368
- function Model(def, params) {
369
- return isPlainObject(def) ? new ObjectModel(def, params) : new BasicModel(def)
370
- }
371
-
372
- Object.assign(Model.prototype, {
373
- name: "Model",
374
- assertions: [],
375
-
376
- conventionForConstant: key => key.toUpperCase() === key,
377
- conventionForPrivate: key => key[0] === "_",
378
-
379
- toString(stack) {
380
- return has(this, "name") ? this.name : formatDefinition(this.definition, stack) + formatAssertions(this.assertions)
381
- },
382
-
383
- as(name) {
384
- define(this, "name", name);
385
- return this
386
- },
387
-
388
- defaultTo(val) {
389
- this.default = val;
390
- return this
391
- },
392
-
393
- [_check](obj, path, errors, stack) {
394
- checkDefinition(obj, this.definition, path, errors, stack);
395
- checkAssertions(obj, this, path, errors);
396
- },
397
-
398
- test(obj, errorCollector) {
399
- let model = this;
400
- while (!has(model, "errorCollector")) {
401
- model = getProto(model);
402
- }
403
-
404
- const initialErrorCollector = model.errorCollector;
405
- let failed;
406
-
407
- model.errorCollector = errors => {
408
- failed = true;
409
- if (errorCollector) errorCollector.call(this, errors);
410
- };
411
-
412
- new this(obj); // may trigger errorCollector
413
-
414
- model.errorCollector = initialErrorCollector;
415
- return !failed
416
- },
417
-
418
- errorCollector(errors) {
419
- const e = new TypeError(errors.map(e => e.message).join("\n"));
420
- e.stack = e.stack.replace(/\n.*object-model(.|\n)*object-model.*/, ""); // blackbox objectmodel in stacktrace
421
- throw e
422
- },
423
-
424
- assert(assertion, description = format(assertion)) {
425
- define(assertion, "description", description);
426
- this.assertions = this.assertions.concat(assertion);
427
- return this
428
- }
429
- });
430
-
431
-
432
- function BasicModel(def) {
433
- return initModel(def, BasicModel)
434
- }
435
-
436
- extend(BasicModel, Model, {
437
- extend(...newParts) {
438
- const child = extendModel(new BasicModel(extendDefinition(this.definition, newParts)), this);
439
- for (let part of newParts) {
440
- if (is(BasicModel, part)) child.assertions.push(...part.assertions);
441
- }
442
-
443
- return child
444
- }
445
- });
446
-
447
- function ObjectModel(def) {
448
- return initModel(def, ObjectModel, Object, initObjectModel, model => getTraps(model, def), true)
449
- }
450
-
451
- extend(ObjectModel, Model, {
452
- defaultTo(obj) {
453
- const def = this.definition;
454
- for (let key in obj) {
455
- if (has(def, key)) {
456
- obj[key] = checkDefinition(obj[key], def[key], key, this.errors, [], true);
457
- }
458
- }
459
- unstackErrors(this);
460
- this.default = obj;
461
- return this
462
- },
463
-
464
- toString(stack) {
465
- return format(this.definition, stack)
466
- },
467
-
468
- extend(...newParts) {
469
- const definition = { ...this.definition };
470
- const proto = { ...this.prototype };
471
- const defaults = { ...this.default };
472
- const newAssertions = [];
473
-
474
- for (let part of newParts) {
475
- if (is(Model, part)) {
476
- merge(definition, part.definition);
477
- merge(defaults, part.default);
478
- newAssertions.push(...part.assertions);
479
- }
480
- if (isFunction(part)) merge(proto, part.prototype);
481
- if (isObject(part)) merge(definition, part);
482
- }
483
-
484
- const submodel = extendModel(new ObjectModel(definition), this, proto).defaultTo(defaults);
485
- submodel.assertions = [...this.assertions, ...newAssertions];
486
-
487
- if (getProto(this) !== ObjectModel.prototype) { // extended class
488
- submodel.parentClass = this;
489
- }
490
-
491
- return submodel
492
- },
493
-
494
- [_check](obj, path, errors, stack, shouldCast) {
495
- if (isObject(obj)) {
496
- checkDefinition(obj[_original] || obj, this.definition, path, errors, stack, shouldCast);
497
- }
498
- else stackError(errors, this, obj, path);
499
-
500
- checkAssertions(obj, this, path, errors);
501
- }
502
- });
503
-
504
- const Any = proxify(BasicModel(), {
505
- apply(target, ctx, [def]) {
506
- const anyOf = Object.create(Any);
507
- anyOf.definition = def;
508
- return anyOf
509
- }
510
- });
511
- Any.definition = Any;
512
- Any.toString = () => "Any";
513
-
514
- Any.remaining = function (def) { this.definition = def; };
515
- extend(Any.remaining, Any, {
516
- toString() { return "..." + formatDefinition(this.definition) }
517
- });
48
+ const
49
+ _check = Symbol(),
50
+ _checked = Symbol(), // used to skip validation at instanciation for perf
51
+ _original = Symbol(), // used to bypass proxy
52
+ CHECK_ONCE = Symbol(),
53
+
54
+ initModel = (def, constructor, parent, init, getTraps, useNew) => {
55
+ const model = function (val = model.default, mode) {
56
+ if (useNew && !is(model, this)) return new model(val)
57
+ if (init) val = init(val, model, this);
58
+
59
+ if (mode === _checked || check(model, val))
60
+ return getTraps && mode !== CHECK_ONCE ? proxify(val, getTraps(model)) : val
61
+ };
62
+
63
+ if (parent) extend(model, parent);
64
+ setProto(model, constructor.prototype);
65
+ model.constructor = constructor;
66
+ model.definition = def;
67
+ model.assertions = [...model.assertions];
68
+ define(model, "errors", []);
69
+ delete model.name;
70
+ return model
71
+ },
72
+
73
+ initObjectModel = (obj, model, _this) => {
74
+ if (is(model, obj)) return obj
75
+
76
+ if (!isObject(obj) && !isFunction(obj) && obj !== undefined) {
77
+ // short circuit validation if not receiving an object as expected
78
+ return obj
79
+ }
80
+
81
+ merge(_this, model.default);
82
+ if (model.parentClass) merge(obj, new model.parentClass(obj));
83
+ merge(_this, obj);
84
+ return _this
85
+ },
86
+
87
+ extendModel = (child, parent, newProps) => {
88
+ extend(child, parent, newProps);
89
+ child.assertions.push(...parent.assertions);
90
+ return child
91
+ },
92
+
93
+ stackError = (errors, expected, received, path, message) => {
94
+ errors.push({ expected, received, path, message });
95
+ },
96
+
97
+ unstackErrors = (model, collector = model.errorCollector) => {
98
+ const nbErrors = model.errors.length;
99
+ if (nbErrors > 0) {
100
+ const errors = model.errors.map(err => {
101
+ if (!err.message) {
102
+ err.message = "expecting " + (err.path ? err.path + " to be " : "") + formatDefinition(err.expected)
103
+ + ", got " + (err.received != null ? bettertypeof(err.received) + " " : "") + format$1(err.received);
104
+ }
105
+ return err
106
+ });
107
+
108
+ model.errors.length = 0;
109
+ collector.call(model, errors); // throw all errors collected
110
+ }
111
+ return nbErrors
112
+ },
113
+
114
+ isModelInstance = i => i && getProto(i) && is(Model, getProto(i).constructor),
115
+
116
+ parseDefinition = (def) => {
117
+ if (isPlainObject(def)) {
118
+ def = {};
119
+ for (let key in def) { def[key] = parseDefinition(def[key]); }
120
+ }
121
+ else if (!Array.isArray(def)) return [def]
122
+ else if (def.length === 1) return [def[0], undefined, null]
123
+
124
+ return def
125
+ },
126
+
127
+ formatDefinition = (def, stack) => {
128
+ const parts = parseDefinition(def).map(d => format$1(d, stack));
129
+ return parts.length > 1 ? parts.join(" or ") : parts[0]
130
+ },
131
+
132
+ formatAssertions = fns => fns.length ? `(${fns.map(f => f.name || f.description || f)})` : "",
133
+
134
+ extendDefinition = (def, newParts = []) => {
135
+ if (newParts.length > 0) {
136
+ def = [].concat(def, ...[].concat(newParts))// clone to lose ref
137
+ .filter((value, index, self) => self.indexOf(value) === index); // remove duplicates
138
+ }
139
+
140
+ return def
141
+ },
142
+
143
+ check = (model, obj) => {
144
+ model[_check](obj, null, model.errors, [], true);
145
+ return !unstackErrors(model)
146
+ },
147
+
148
+ checkDefinition = (obj, def, path, errors, stack, shouldCast) => {
149
+ const indexFound = stack.indexOf(def);
150
+ if (indexFound !== -1 && stack.indexOf(def, indexFound + 1) !== -1)
151
+ return obj // if found twice in call stack, cycle detected, skip validation
152
+
153
+ if (Array.isArray(def) && def.length === 1 && obj != null) {
154
+ def = def[0]; // shorten validation path for optionals
155
+ }
156
+
157
+ if (is(Model, def)) {
158
+ if (shouldCast) obj = cast(obj, def);
159
+ def[_check](obj, path, errors, stack.concat(def));
160
+ }
161
+ else if (isPlainObject(def)) {
162
+ for (let key in def) {
163
+ const val = obj ? obj[key] : undefined;
164
+ checkDefinition(val, def[key], formatPath(path, key), errors, stack, shouldCast);
165
+ }
166
+ }
167
+ else {
168
+ const pdef = parseDefinition(def);
169
+ if (pdef.some(part => checkDefinitionPart(obj, part, path, stack))) {
170
+ return shouldCast ? cast(obj, def) : obj
171
+ }
172
+
173
+ stackError(errors, def, obj, path);
174
+ }
175
+
176
+ return obj
177
+ },
178
+
179
+ checkDefinitionPart = (obj, def, path, stack, shouldCast) => {
180
+ if (def === Any) return true
181
+ if (obj == null) return obj === def
182
+ if (isPlainObject(def) || is(Model, def)) { // object or model as part of union type
183
+ const errors = [];
184
+ checkDefinition(obj, def, path, errors, stack, shouldCast);
185
+ return !errors.length
186
+ }
187
+ if (is(RegExp, def)) return def.test(obj)
188
+ if (def === Number || def === Date) return obj.constructor === def && !isNaN(obj)
189
+ return obj === def
190
+ || (isFunction(def) && is(def, obj))
191
+ || obj.constructor === def
192
+ },
193
+
194
+ checkAssertions = (obj, model, path, errors = model.errors) => {
195
+ for (let assertion of model.assertions) {
196
+ let result;
197
+ try {
198
+ result = assertion.call(model, obj);
199
+ } catch (err) {
200
+ result = err;
201
+ }
202
+ if (result !== true) {
203
+ const onFail = isFunction(assertion.description) ? assertion.description : (assertionResult, value) =>
204
+ `assertion "${assertion.description}" returned ${format$1(assertionResult)} `
205
+ + `for ${path ? path + " =" : "value"} ${format$1(value)}`;
206
+ stackError(errors, assertion, obj, path, onFail.call(model, result, obj, path));
207
+ }
208
+ }
209
+ },
210
+
211
+ format$1 = (obj, stack = []) => {
212
+ if (stack.length > 15 || stack.includes(obj)) return "..."
213
+ if (obj === null || obj === undefined) return String(obj)
214
+ if (isString(obj)) return `"${obj}"`
215
+ if (is(Model, obj)) return obj.toString(stack)
216
+
217
+ stack.unshift(obj);
218
+
219
+ if (isFunction(obj)) return obj.name || obj.toString()
220
+ if (is(Map, obj) || is(Set, obj)) return format$1([...obj])
221
+ if (Array.isArray(obj)) return `[${obj.map(item => format$1(item, stack)).join(", ")}]`
222
+ if (obj.toString && obj.toString !== ObjectProto.toString) return obj.toString()
223
+ if (isObject(obj)) {
224
+ const props = Object.keys(obj),
225
+ indent = "\t".repeat(stack.length);
226
+ return `{${props.map(
227
+ key => `\n${indent + key}: ${format$1(obj[key], [...stack])}`
228
+ ).join(", ")} ${props.length ? `\n${indent.slice(1)}` : ""}}`
229
+ }
230
+
231
+ return String(obj)
232
+ },
233
+
234
+ formatPath = (path, key) => path ? path + "." + key : key,
235
+
236
+ controlMutation$1 = (model, def, path, o, key, privateAccess, applyMutation) => {
237
+ const newPath = formatPath(path, key),
238
+ isPrivate = model.conventionForPrivate(key),
239
+ isConstant = model.conventionForConstant(key),
240
+ isOwnProperty = has(o, key),
241
+ initialPropDescriptor = isOwnProperty && Object.getOwnPropertyDescriptor(o, key);
242
+
243
+ if (key in def && ((isPrivate && !privateAccess) || (isConstant && o[key] !== undefined)))
244
+ cannot(`modify ${isPrivate ? "private" : "constant"} property ${key}`, model);
245
+
246
+ applyMutation();
247
+ if (has(def, key)) checkDefinition(o[key], def[key], newPath, model.errors, []);
248
+ checkAssertions(o, model, newPath);
249
+
250
+ const nbErrors = model.errors.length;
251
+ if (nbErrors) {
252
+ if (isOwnProperty) Object.defineProperty(o, key, initialPropDescriptor);
253
+ else delete o[key]; // back to the initial property defined in prototype chain
254
+
255
+ unstackErrors(model);
256
+ }
257
+
258
+ return !nbErrors
259
+ },
260
+
261
+ cannot = (msg, model) => {
262
+ model.errors.push({ message: "cannot " + msg });
263
+ },
264
+
265
+ cast = (obj, defNode = []) => {
266
+ if (!obj || isPlainObject(defNode) || is(BasicModel, defNode) || isModelInstance(obj))
267
+ return obj // no value or not leaf or already a model instance
268
+
269
+ const def = parseDefinition(defNode),
270
+ suitableModels = [];
271
+
272
+ for (let part of def) {
273
+ if (is(Model, part) && !is(BasicModel, part) && part.test(obj))
274
+ suitableModels.push(part);
275
+ }
276
+
277
+ if (suitableModels.length === 1) {
278
+ // automatically cast to suitable model when explicit (autocasting)
279
+ return new suitableModels[0](obj, _checked)
280
+ }
281
+
282
+ if (suitableModels.length > 1)
283
+ console.warn(`Ambiguous model for value ${format$1(obj)}, could be ${suitableModels.join(" or ")}`);
284
+
285
+ return obj
286
+ },
287
+
288
+
289
+ getProp = (val, model, def, path, privateAccess) => {
290
+ if (!isPlainObject(def)) return cast(val, def)
291
+ return proxify(val, getTraps(model, def, path, privateAccess))
292
+ },
293
+
294
+ getTraps = (model, def, path, privateAccess) => {
295
+ const grantPrivateAccess = f => proxify(f, {
296
+ apply(fn, ctx, args) {
297
+ privateAccess = true;
298
+ const result = Reflect.apply(fn, ctx, args);
299
+ privateAccess = false;
300
+ return result
301
+ }
302
+ });
303
+
304
+ return {
305
+ get(o, key) {
306
+ if (key === _original) return o
307
+
308
+ if (!isString(key)) return Reflect.get(o, key)
309
+
310
+ const newPath = formatPath(path, key);
311
+ const inDef = has(def, key);
312
+ const defPart = def[key];
313
+
314
+ if (!privateAccess && inDef && model.conventionForPrivate(key)) {
315
+ cannot(`access to private property ${newPath}`, model);
316
+ unstackErrors(model);
317
+ return
318
+ }
319
+
320
+ let value = o[key];
321
+
322
+ if (inDef && value && has(o, key) && !isPlainObject(defPart) && !isModelInstance(value)) {
323
+ Reflect.set(o, key, value = cast(value, defPart)); // cast nested models
324
+ }
325
+
326
+ if (isFunction(value) && key !== "constructor" && !privateAccess) {
327
+ return grantPrivateAccess(value)
328
+ }
329
+
330
+ if (isPlainObject(defPart) && !value) {
331
+ o[key] = value = {}; // null-safe traversal
332
+ }
333
+
334
+ return getProp(value, model, defPart, newPath, privateAccess)
335
+ },
336
+
337
+ set(o, key, val) {
338
+ return controlMutation$1(model, def, path, o, key, privateAccess, () => Reflect.set(o, key, cast(val, def[key])))
339
+ },
340
+
341
+ deleteProperty(o, key) {
342
+ return controlMutation$1(model, def, path, o, key, privateAccess, () => Reflect.deleteProperty(o, key))
343
+ },
344
+
345
+ defineProperty(o, key, args) {
346
+ return controlMutation$1(model, def, path, o, key, privateAccess, () => Reflect.defineProperty(o, key, args))
347
+ },
348
+
349
+ has(o, key) {
350
+ return Reflect.has(o, key) && Reflect.has(def, key) && !model.conventionForPrivate(key)
351
+ },
352
+
353
+ ownKeys(o) {
354
+ return Reflect.ownKeys(o).filter(key => Reflect.has(def, key) && !model.conventionForPrivate(key))
355
+ },
356
+
357
+ getOwnPropertyDescriptor(o, key) {
358
+ let descriptor;
359
+ if (!model.conventionForPrivate(key)) {
360
+ descriptor = Object.getOwnPropertyDescriptor(def, key);
361
+ if (descriptor !== undefined) descriptor.value = o[key];
362
+ }
363
+
364
+ return descriptor
365
+ }
366
+ }
367
+ };
368
+
369
+
370
+ function Model(def) {
371
+ return isPlainObject(def) ? new ObjectModel(def) : new BasicModel(def)
372
+ }
373
+
374
+ Object.assign(Model.prototype, {
375
+ name: "Model",
376
+ assertions: [],
377
+
378
+ conventionForConstant: key => key.toUpperCase() === key,
379
+ conventionForPrivate: key => key[0] === "_",
380
+
381
+ toString(stack) {
382
+ return has(this, "name") ? this.name : formatDefinition(this.definition, stack) + formatAssertions(this.assertions)
383
+ },
384
+
385
+ as(name) {
386
+ define(this, "name", name);
387
+ return this
388
+ },
389
+
390
+ defaultTo(val) {
391
+ this.default = val;
392
+ return this
393
+ },
394
+
395
+ [_check](obj, path, errors, stack) {
396
+ checkDefinition(obj, this.definition, path, errors, stack);
397
+ checkAssertions(obj, this, path, errors);
398
+ },
399
+
400
+ test(obj, errorCollector) {
401
+ let model = this;
402
+ while (!has(model, "errorCollector")) {
403
+ model = getProto(model);
404
+ }
405
+
406
+ const initialErrorCollector = model.errorCollector;
407
+ let failed;
408
+
409
+ model.errorCollector = errors => {
410
+ failed = true;
411
+ if (errorCollector) errorCollector.call(this, errors);
412
+ };
413
+
414
+ new this(obj); // may trigger errorCollector
415
+
416
+ model.errorCollector = initialErrorCollector;
417
+ return !failed
418
+ },
419
+
420
+ errorCollector(errors) {
421
+ const e = new TypeError(errors.map(e => e.message).join("\n"));
422
+ e.stack = e.stack.replace(/\n.*object-model(.|\n)*object-model.*/, ""); // blackbox objectmodel in stacktrace
423
+ throw e
424
+ },
425
+
426
+ assert(assertion, description = format$1(assertion)) {
427
+ define(assertion, "description", description);
428
+ this.assertions = this.assertions.concat(assertion);
429
+ return this
430
+ }
431
+ });
432
+
433
+ Model.CHECK_ONCE = CHECK_ONCE;
434
+
435
+ function BasicModel(def) {
436
+ return initModel(def, BasicModel)
437
+ }
438
+
439
+ extend(BasicModel, Model, {
440
+ extend(...newParts) {
441
+ const child = extendModel(new BasicModel(extendDefinition(this.definition, newParts)), this);
442
+ for (let part of newParts) {
443
+ if (is(BasicModel, part)) child.assertions.push(...part.assertions);
444
+ }
445
+
446
+ return child
447
+ }
448
+ });
449
+
450
+ function ObjectModel(def) {
451
+ return initModel(def, ObjectModel, Object, initObjectModel, model => getTraps(model, def), true)
452
+ }
453
+
454
+ extend(ObjectModel, Model, {
455
+ defaultTo(obj) {
456
+ const def = this.definition;
457
+ for (let key in obj) {
458
+ if (has(def, key)) {
459
+ obj[key] = checkDefinition(obj[key], def[key], key, this.errors, [], true);
460
+ }
461
+ }
462
+ unstackErrors(this);
463
+ this.default = obj;
464
+ return this
465
+ },
466
+
467
+ toString(stack) {
468
+ return format$1(this.definition, stack)
469
+ },
470
+
471
+ extend(...newParts) {
472
+ const definition = { ...this.definition };
473
+ const proto = { ...this.prototype };
474
+ const defaults = { ...this.default };
475
+ const newAssertions = [];
476
+
477
+ for (let part of newParts) {
478
+ if (is(Model, part)) {
479
+ merge(definition, part.definition);
480
+ merge(defaults, part.default);
481
+ newAssertions.push(...part.assertions);
482
+ }
483
+ if (isFunction(part)) merge(proto, part.prototype);
484
+ if (isObject(part)) merge(definition, part);
485
+ }
486
+
487
+ const submodel = extendModel(new ObjectModel(definition), this, proto).defaultTo(defaults);
488
+ submodel.assertions = [...this.assertions, ...newAssertions];
489
+
490
+ if (getProto(this) !== ObjectModel.prototype) { // extended class
491
+ submodel.parentClass = this;
492
+ }
493
+
494
+ return submodel
495
+ },
496
+
497
+ [_check](obj, path, errors, stack, shouldCast) {
498
+ if (isObject(obj)) {
499
+ checkDefinition(obj[_original] || obj, this.definition, path, errors, stack, shouldCast);
500
+ }
501
+ else stackError(errors, this, obj, path);
502
+
503
+ checkAssertions(obj, this, path, errors);
504
+ }
505
+ });
506
+
507
+ const Any = proxify(BasicModel(), {
508
+ apply(target, ctx, [def]) {
509
+ const anyOf = Object.create(Any);
510
+ anyOf.definition = def;
511
+ return anyOf
512
+ }
513
+ });
514
+ Any.definition = Any;
515
+ Any.toString = () => "Any";
516
+
517
+ Any.remaining = function (def) { this.definition = def; };
518
+ extend(Any.remaining, Any, {
519
+ toString() { return "..." + formatDefinition(this.definition) }
520
+ });
518
521
  Any[Symbol.iterator] = function* () { yield new Any.remaining(this.definition); };
519
522
 
520
523
  const initListModel = (base, constructor, def, init, clone, mutators, otherTraps) => {
@@ -578,11 +581,11 @@ function ArrayModel(initialDefinition) {
578
581
  },
579
582
  {
580
583
  set(arr, key, val) {
581
- return controlMutation$1(model, arr, key, val, (a, v) => a[key] = v, true)
584
+ return controlMutation(model, arr, key, val, (a, v) => a[key] = v, true)
582
585
  },
583
586
 
584
587
  deleteProperty(arr, key) {
585
- return controlMutation$1(model, arr, key, undefined, a => delete a[key])
588
+ return controlMutation(model, arr, key, undefined, a => delete a[key])
586
589
  }
587
590
  }
588
591
  );
@@ -608,7 +611,7 @@ extend(ArrayModel, Model, {
608
611
  }
609
612
  });
610
613
 
611
- const controlMutation$1 = (model, array, key, value, applyMutation, canBeExtended) => {
614
+ const controlMutation = (model, array, key, value, applyMutation, canBeExtended) => {
612
615
  const path = `Array[${key}]`;
613
616
  const isInDef = (+key >= 0 && (canBeExtended || key in array));
614
617
  if (isInDef) value = checkDefinition(value, model.definition, path, model.errors, [], true);
@@ -685,7 +688,7 @@ extend(MapModel, Model, {
685
688
  path = path || "Map";
686
689
  for (let [key, value] of map) {
687
690
  checkDefinition(key, this.definition.key, `${path} key`, errors, stack);
688
- checkDefinition(value, this.definition.value, `${path}[${format(key)}]`, errors, stack);
691
+ checkDefinition(value, this.definition.value, `${path}[${format$1(key)}]`, errors, stack);
689
692
  }
690
693
  } else stackError(errors, this, map, path);
691
694
 
@@ -792,7 +795,7 @@ const getModel = (instance) => {
792
795
 
793
796
  const span = (style, ...children) => ["span", { style }, ...children];
794
797
 
795
- const format$1 = (x, config = {}) => {
798
+ const format = (x, config = {}) => {
796
799
  if (x === null || x === undefined)
797
800
  return span(styles.null, "" + x);
798
801
 
@@ -806,7 +809,7 @@ const format$1 = (x, config = {}) => {
806
809
  return span(styles.string, `"${x}"`);
807
810
 
808
811
  if (Array.isArray(x) && config.isModelDefinition) {
809
- return span("", ...x.flatMap(part => [format$1(part, config), " or "]).slice(0, -1))
812
+ return span("", ...x.flatMap(part => [format(part, config), " or "]).slice(0, -1))
810
813
  }
811
814
 
812
815
  if (isPlainObject(x))
@@ -821,7 +824,7 @@ const format$1 = (x, config = {}) => {
821
824
  const formatObject = (o, model, config) => span("",
822
825
  "{",
823
826
  ["ol", { style: styles.list }, ...Object.keys(o).map(prop =>
824
- ["li", { style: styles.listItem }, span(styles.property, prop), ": ", format$1(o[prop], config)])
827
+ ["li", { style: styles.listItem }, span(styles.property, prop), ": ", format(o[prop], config)])
825
828
  ],
826
829
  "}"
827
830
  );
@@ -833,13 +836,13 @@ const formatModel = model => {
833
836
  formatList = (list, map) => list.flatMap(e => [map(e), ", "]).slice(0, -1);
834
837
  let parts = [];
835
838
 
836
- if (is(BasicModel, model)) parts = [format$1(def, cfg)];
837
- if (is(ArrayModel, model)) parts = ["Array of ", format$1(def, cfg)];
838
- if (is(SetModel, model)) parts = ["Set of ", format$1(def, cfg)];
839
- if (is(MapModel, model)) parts = ["Map of ", format$1(def.key, cfg), " : ", format$1(def.value, cfg)];
839
+ if (is(BasicModel, model)) parts = [format(def, cfg)];
840
+ if (is(ArrayModel, model)) parts = ["Array of ", format(def, cfg)];
841
+ if (is(SetModel, model)) parts = ["Set of ", format(def, cfg)];
842
+ if (is(MapModel, model)) parts = ["Map of ", format(def.key, cfg), " : ", format(def.value, cfg)];
840
843
  if (is(FunctionModel, model)) {
841
- parts = ["Function(", ...formatList(def.arguments, arg => format$1(arg, cfg)), ")"];
842
- if ("return" in def) parts.push(" => ", format$1(def.return, cfg));
844
+ parts = ["Function(", ...formatList(def.arguments, arg => format(arg, cfg)), ")"];
845
+ if ("return" in def) parts.push(" => ", format(def.return, cfg));
843
846
  }
844
847
 
845
848
  if (model.assertions.length > 0) {
@@ -855,7 +858,7 @@ const ModelFormatter = {
855
858
  return span(styles.model, "Any")
856
859
 
857
860
  if (is(Any.remaining, x))
858
- return span(styles.model, "...", format$1(x.definition, { isModelDefinition: true }))
861
+ return span(styles.model, "...", format(x.definition, { isModelDefinition: true }))
859
862
 
860
863
  if (is(ObjectModel, x))
861
864
  return span(styles.model, x.name)
@@ -865,7 +868,7 @@ const ModelFormatter = {
865
868
  }
866
869
 
867
870
  if (config.isModelDefinition && isPlainObject(x))
868
- return format$1(x, config)
871
+ return format(x, config)
869
872
 
870
873
  return null;
871
874
  },
@@ -888,8 +891,8 @@ const ModelFormatter = {
888
891
  }
889
892
 
890
893
  return ["li", { style: styles.listItem },
891
- span(style, prop), ": ", format$1(model.definition[prop], { isModelDefinition: true }),
892
- hasDefault ? span(styles.proto, " = ", format$1(model.default[prop])) : ""
894
+ span(style, prop), ": ", format(model.definition[prop], { isModelDefinition: true }),
895
+ hasDefault ? span(styles.proto, " = ", format(model.default[prop])) : ""
893
896
  ]
894
897
  })],
895
898
  "}"
@@ -900,7 +903,7 @@ const ModelFormatter = {
900
903
  const ModelInstanceFormatter = {
901
904
  header(x, config = {}) {
902
905
  if (config.isInstanceProperty && isPlainObject(x)) {
903
- return format$1(x, config)
906
+ return format(x, config)
904
907
  }
905
908
 
906
909
  const model = getModel(x);
@@ -937,7 +940,7 @@ const ModelInstanceFormatter = {
937
940
  }
938
941
 
939
942
  return ["li", { style: styles.listItem },
940
- span(style, prop), ": ", format$1(o[prop], { isInstanceProperty: true })
943
+ span(style, prop), ": ", format(o[prop], { isInstanceProperty: true })
941
944
  ]
942
945
  }),
943
946
  ["li", { style: styles.listItem },