Skip to content

Commit

Permalink
implemented Schema partialCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
Bespaliy committed Dec 6, 2023
1 parent e50ce30 commit d071ca6
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 17 deletions.
15 changes: 13 additions & 2 deletions lib/prototypes/abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions lib/prototypes/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions lib/prototypes/reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions lib/prototypes/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
};

Expand Down
5 changes: 3 additions & 2 deletions lib/prototypes/tuple.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
17 changes: 14 additions & 3 deletions lib/struct.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions metaschema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class Schema {
checkConsistency(): Array<string>;
findReference(name: string): Schema;
check(value: any): ValidationResult;
partialCheck(value: any): ValidationResult;
toInterface(): string;
attach(...namespaces: Array<Model>): void;
detouch(...namespaces: Array<Model>): void;
Expand Down
213 changes: 213 additions & 0 deletions test/partial-check.js
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit d071ca6

Please sign in to comment.