Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented Schema partialCheck #464

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
Loading