From d071ca6aa6636cad2dfd86cbdabf7a7914023cb6 Mon Sep 17 00:00:00 2001 From: Bespaliy Date: Wed, 6 Dec 2023 15:14:26 +0200 Subject: [PATCH 1/2] implemented Schema partialCheck --- lib/prototypes/abstract.js | 15 ++- lib/prototypes/collections.js | 14 ++- lib/prototypes/reference.js | 7 +- lib/prototypes/schema.js | 5 +- lib/prototypes/tuple.js | 5 +- lib/schema.js | 6 + lib/struct.js | 17 ++- metaschema.d.ts | 1 + test/partial-check.js | 213 ++++++++++++++++++++++++++++++++++ 9 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 test/partial-check.js diff --git a/lib/prototypes/abstract.js b/lib/prototypes/abstract.js index 44d698f..2e8e72a 100644 --- a/lib/prototypes/abstract.js +++ b/lib/prototypes/abstract.js @@ -27,11 +27,22 @@ class AbstractType { } check(value, path) { + return this.commonCheck(value, path, false); + } + + partialCheck(value, path) { + return this.commonCheck(value, path, true); + } + + commonCheck(value, path, isPartial) { const result = new ValidationResult(path); const isEmpty = value === null || value === undefined; - if (!this.required && isEmpty) return result; + const isRequiredAndMissing = !isPartial + ? !this.required && isEmpty + : isEmpty; + if (isRequiredAndMissing) return result; try { - result.add(this.checkType(value, path)); + result.add(this.checkType(value, path, isPartial)); if (this.validate) result.add(this.validate(value, path)); for (const [name, subCheck] of Object.entries(AbstractType.checks)) { if (!this[name]) continue; diff --git a/lib/prototypes/collections.js b/lib/prototypes/collections.js index 39ea82a..7636e32 100644 --- a/lib/prototypes/collections.js +++ b/lib/prototypes/collections.js @@ -13,21 +13,24 @@ const object = { this.value = new Type(defs, prep); }, - checkType(source, path) { + checkType(source, path, isPartial) { if (!this.isInstance(source)) { return `Filed "${path}" is not a ${this.type}`; } const entries = this.entries(source); - if (entries.length === 0 && this.required) { + const isRequiredAndEmpty = + !isPartial && entries.length === 0 && this.required; + if (isRequiredAndEmpty) { return `Filed "${path}" is required`; } + const method = isPartial ? 'partialCheck' : 'check'; const errors = []; for (const [field, val] of entries) { if (typeof field !== this.key) { return `In ${this.type} "${path}": type of key must be a ${this.key}`; } const nestedPath = `${path}.${field}`; - const result = this.value.check(val, nestedPath); + const result = this.value[method](val, nestedPath); if (!result.valid) errors.push(...result.errors); } if (errors.length > 0) return errors; @@ -66,16 +69,17 @@ const array = { this.value = new Type(defs, prep); }, - checkType(source, path) { + checkType(source, path, isPartial) { if (!this.isInstance(source)) { return `Field "${path}" not of expected type: ${this.type}`; } const value = [...source]; const errors = []; + const method = isPartial ? 'partialCheck' : 'check'; for (let i = 0; i < value.length; i++) { const el = value[i]; const nestedPath = `${path}[${i}]`; - const result = this.value.check(el, nestedPath); + const result = this.value[method](el, nestedPath); if (!result.valid) errors.push(...result.errors); } if (errors.length > 0) return errors; diff --git a/lib/prototypes/reference.js b/lib/prototypes/reference.js index 23ff926..6fa00df 100644 --- a/lib/prototypes/reference.js +++ b/lib/prototypes/reference.js @@ -13,15 +13,16 @@ const reference = { this.root.relations.add({ to: reference, type: relation }); }, - checkType(source, path) { + checkType(source, path, isPartial) { const { one, many, root } = this; + const method = isPartial ? 'partialCheck' : 'check'; if (one) { const schema = root.findReference(one); - return schema.check(source, path); + return schema[method](source, path); } const schema = root.findReference(many); for (const obj of source) { - const res = schema.check(obj, path); + const res = schema[method](obj, path); if (!res.valid) return res; } return null; diff --git a/lib/prototypes/schema.js b/lib/prototypes/schema.js index ee81a8e..ad78e16 100644 --- a/lib/prototypes/schema.js +++ b/lib/prototypes/schema.js @@ -14,8 +14,9 @@ const schema = { this.validate = defs.schema.validate || undefined; }, - checkType(source, path = '') { - return this.schema.check(source, path); + checkType(source, path = '', isPartial) { + const method = isPartial ? 'partialCheck' : 'check'; + return this.schema[method](source, path); }, }; diff --git a/lib/prototypes/tuple.js b/lib/prototypes/tuple.js index 8d9bfe0..cc968d4 100644 --- a/lib/prototypes/tuple.js +++ b/lib/prototypes/tuple.js @@ -18,16 +18,17 @@ const tuple = { }); }, - checkType(src, path) { + checkType(src, path, isPartial) { if (!Array.isArray(src)) return `not of expected type: ${this.type}`; if (src.length > this.value.length) { return 'value length is more then expected in tuple'; } + const method = isPartial ? 'partialCheck' : 'check'; for (let i = 0; i < this.value.length; i++) { const scalar = this.value[i]; const nested = `${path}(${scalar.name || 'item'}${i})`; const elem = src[i]; - const res = scalar.check(elem, nested); + const res = scalar[method](elem, nested); if (!res.valid) return res; } return null; diff --git a/lib/schema.js b/lib/schema.js index 42b6473..79868d9 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -72,6 +72,12 @@ class Schema extends SchemaMetadata { return result.add(this.fields.check(source, path)); } + partialCheck(source, path = this.name) { + const result = new ValidationResult(path); + result.add(this.validate(source, path)); + return result.add(this.fields.partialCheck(source, path)); + } + toInterface() { const { name, fields } = this; const types = []; diff --git a/lib/struct.js b/lib/struct.js index e21c7d8..0bfada5 100644 --- a/lib/struct.js +++ b/lib/struct.js @@ -20,10 +20,19 @@ class Struct { } check(source, path = '') { + return this.commonCheck(source, path, false); + } + + partialCheck(source, path = '') { + return this.commonCheck(source, path, true); + } + + commonCheck(source, path, isPartial) { const result = new ValidationResult(path || this.name); const keys = Object.keys(source); - const fields = Object.keys(this); + const fields = !isPartial ? Object.keys(this) : []; const names = new Set([...fields, ...keys]); + const method = isPartial ? 'partialCheck' : 'check'; for (const name of names) { const value = source[name]; const type = this[name]; @@ -33,11 +42,13 @@ class Struct { } if (!isInstanceOf(type, 'Type')) continue; const nestedPath = path ? `${path}.${name}` : name; - if (type.required && !keys.includes(name)) { + const isRequiredAndMissing = + !isPartial && type.required && !keys.includes(name); + if (isRequiredAndMissing) { result.add(`Field "${nestedPath}" is required`); continue; } - result.add(type.check(value, nestedPath)); + result.add(type[method](value, nestedPath)); } return result; } diff --git a/metaschema.d.ts b/metaschema.d.ts index 76cda53..1f03847 100644 --- a/metaschema.d.ts +++ b/metaschema.d.ts @@ -66,6 +66,7 @@ export class Schema { checkConsistency(): Array; findReference(name: string): Schema; check(value: any): ValidationResult; + partialCheck(value: any): ValidationResult; toInterface(): string; attach(...namespaces: Array): void; detouch(...namespaces: Array): void; diff --git a/test/partial-check.js b/test/partial-check.js new file mode 100644 index 0000000..8fc73c1 --- /dev/null +++ b/test/partial-check.js @@ -0,0 +1,213 @@ +'use strict'; + +const metatests = require('metatests'); +const { Schema } = require('..'); +const { Model } = require('../metaschema'); + +metatests.test('Scalars: partialCheck scalar', (test) => { + const def1 = { type: 'string', required: true }; + const schema1 = Schema.from(def1); + test.strictSame(schema1.partialCheck('value').valid, true); + test.strictSame(schema1.partialCheck(null).valid, true); + + const def2 = 'string'; + const schema2 = Schema.from(def2); + test.strictSame(schema2.partialCheck('value').valid, true); + test.strictSame(schema1.partialCheck(null).valid, true); + + test.end(); +}); +metatests.test('Schema: partialCheck enum', (test) => { + const definition = { field: { enum: ['uno', 'due', 'tre'], required: true } }; + const schema = Schema.from(definition); + test.strictSame(schema.partialCheck({ field: 'uno' }).valid, true); + test.strictSame(schema.partialCheck({ field: null }).valid, true); + test.strictSame(schema.partialCheck({}).valid, true); + + test.end(); +}); + +metatests.test('Scalars: partialCheck null value', (test) => { + const def = { + field1: { type: 'string', required: true }, + field2: 'string', + field3: 'string', + }; + const schema = Schema.from(def); + const obj1 = { field1: null, field2: null, field3: null }; + test.strictSame(schema.partialCheck({ field4: null }).errors, [ + 'Field "field4" is not expected', + ]); + test.strictSame(schema.partialCheck(obj1).valid, true); + test.strictSame(schema.partialCheck({}).valid, true); + test.end(); +}); + +metatests.test('Rules: length partialCheck', (test) => { + const definition = { + field1: 'string', + field2: { type: 'number' }, + field3: { type: 'string', length: { min: 5, max: 30 } }, + }; + const schema = Schema.from(definition); + + const obj1 = { + field3: 'valuevaluevaluevaluevaluevaluevaluevalue', + }; + test.strictSame(schema.partialCheck(obj1).errors, [ + 'Field "field3" exceeds the maximum length', + ]); + + const obj2 = { + field1: 'value', + field2: 'value', + }; + test.strictSame(schema.partialCheck(obj2).errors, [ + 'Field "field2" not of expected type: number', + ]); + + const obj3 = { + field4: 'value', + }; + test.strictSame(schema.partialCheck(obj3).errors, [ + 'Field "field4" is not expected', + ]); + + const obj4 = {}; + test.strictSame(schema.partialCheck(obj4).valid, true); + + const obj5 = { + field1: 'value', + field2: 100, + field3: 'valuevaluevalue', + }; + test.strictSame(schema.partialCheck(obj5).valid, true); + + test.end(); +}); + +metatests.test('Collections: partialCheck collections', (test) => { + const def1 = { + field1: { array: 'number' }, + }; + const obj1 = {}; + const schema1 = Schema.from(def1); + test.strictSame(schema1.partialCheck(obj1).valid, true); + + const obj2 = { + field1: null, + }; + test.strictSame(schema1.partialCheck(obj2).valid, true); + + const obj3 = { + field1: [], + }; + test.strictSame(schema1.partialCheck(obj3).valid, true); + + const obj4 = { + field1: [1, 2, 3], + }; + test.strictSame(schema1.partialCheck(obj4).valid, true); + + const obj5 = { + field1: ['uno', 2, 3], + }; + test.strictSame(schema1.partialCheck(obj5).valid, false); + + const def2 = { + field1: { object: { string: 'string' } }, + }; + const obj6 = { + field1: { a: 'A', b: 'B' }, + }; + const schema2 = Schema.from(def2); + test.strictSame(schema2.partialCheck(obj6).valid, true); + + const obj7 = { + field1: { a: 1, b: 'B' }, + }; + test.strictSame(schema2.partialCheck(obj7).valid, false); + + const obj8 = { + field1: {}, + }; + test.strictSame(schema2.partialCheck(obj8).valid, true); + + const obj9 = {}; + test.strictSame(schema2.partialCheck(obj9).valid, true); + + const def3 = { + field1: { set: 'number' }, + }; + const obj10 = { + field1: new Set([1, 2, 3]), + }; + const schema3 = Schema.from(def3); + test.strictSame(schema3.partialCheck(obj10).valid, true); + + const obj11 = {}; + test.strictSame(schema3.partialCheck(obj11).valid, true); + + const def4 = { + field1: { map: { string: 'string' } }, + }; + const obj12 = { + field1: new Map([ + ['a', 'A'], + ['b', 'B'], + ]), + }; + const schema4 = Schema.from(def4); + test.strictSame(schema4.partialCheck(obj12).valid, true); + + const obj13 = {}; + test.strictSame(schema4.partialCheck(obj13).valid, true); + + test.end(); +}); + +metatests.test('Struct: partialCheck json type as any plain object', (test) => { + const defs = { name: 'json' }; + const schema = Schema.from(defs); + test.strictEqual(schema.partialCheck({ name: { a: 'b' } }).valid, true); + test.strictEqual(schema.partialCheck({ name: null }).valid, true); + test.strictEqual(schema.partialCheck({}).valid, true); + test.end(); +}); + +metatests.test('Tuple: with field names', (test) => { + const defs1 = [{ sum: 'number' }, { length: 'string' }]; + const schema1 = Schema.from(defs1); + test.strictEqual(schema1.partialCheck([1, '123']).valid, true); + test.strictEqual(schema1.partialCheck([1]).valid, true); + test.strictEqual(schema1.partialCheck(null).valid, true); + test.end(); +}); + +metatests.test('Schema: partialCheck with namespaces', (test) => { + const raw = { + name: { type: 'string', unique: true }, + address: 'Address', + }; + + const entities = new Map(); + entities.set('Address', { + city: 'string', + street: 'string', + building: 'number', + }); + const model = new Model({}, entities); + const schema = new Schema('Company', raw, [model]); + + const data1 = { + address: { + building: 2, + }, + }; + test.strictSame(schema.partialCheck(data1).valid, true); + + const data2 = {}; + test.strictSame(schema.partialCheck(data2).valid, true); + + test.end(); +}); From 44e87565f0f311b3345995292f32e4a680bb35fd Mon Sep 17 00:00:00 2001 From: Bespaliy Date: Wed, 6 Dec 2023 16:56:33 +0200 Subject: [PATCH 2/2] added private for commonCheck method --- lib/prototypes/abstract.js | 6 +++--- lib/struct.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/prototypes/abstract.js b/lib/prototypes/abstract.js index 2e8e72a..cf485f4 100644 --- a/lib/prototypes/abstract.js +++ b/lib/prototypes/abstract.js @@ -27,14 +27,14 @@ class AbstractType { } check(value, path) { - return this.commonCheck(value, path, false); + return this.#commonCheck(value, path, false); } partialCheck(value, path) { - return this.commonCheck(value, path, true); + return this.#commonCheck(value, path, true); } - commonCheck(value, path, isPartial) { + #commonCheck(value, path, isPartial) { const result = new ValidationResult(path); const isEmpty = value === null || value === undefined; const isRequiredAndMissing = !isPartial diff --git a/lib/struct.js b/lib/struct.js index 0bfada5..9742e17 100644 --- a/lib/struct.js +++ b/lib/struct.js @@ -20,14 +20,14 @@ class Struct { } check(source, path = '') { - return this.commonCheck(source, path, false); + return this.#commonCheck(source, path, false); } partialCheck(source, path = '') { - return this.commonCheck(source, path, true); + return this.#commonCheck(source, path, true); } - commonCheck(source, path, isPartial) { + #commonCheck(source, path, isPartial) { const result = new ValidationResult(path || this.name); const keys = Object.keys(source); const fields = !isPartial ? Object.keys(this) : [];