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

WIP: Implement schema validation in Collector #232

Closed
wants to merge 1 commit into from
Closed
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
16 changes: 15 additions & 1 deletion lib/collector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ class Collector {
count = 0;
exact = true;
timeout = 0;
schema = null;
#fulfill = null;
#reject = null;
#timer = null;
#cause = null;

constructor(keys, { exact = true, timeout = 0 } = {}) {
constructor(keys, { exact = true, timeout = 0, schema = null } = {}) {
this.keys = keys;
if (exact === false) this.exact = false;
if (typeof timeout === 'number') this.#timeout(timeout);
if (schema) this.schema = schema;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (schema) this.schema = schema;
this.schema = schema;

}

#timeout(msec) {
Expand All @@ -29,6 +31,12 @@ class Collector {
this.#timer = setTimeout(handler, msec);
}

#validate(data) {
if (!this.schema) return { valid: true, errors: [] };

return this.schema.check(data);
Comment on lines +35 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!this.schema) return { valid: true, errors: [] };
return this.schema.check(data);
if (!this.schema) return { valid: true, errors: [] };
return this.schema.check(data);

}

set(key, value) {
if (this.done) return;
const has = this.data[key] !== undefined;
Expand All @@ -40,6 +48,12 @@ class Collector {
if (!has && expected) this.count++;
this.data[key] = value;
if (this.count === this.keys.length) {
const { valid, errors } = this.#validate(this.data);
if (!valid) {
const problems = errors.join('; ');
this.fail(new Error(`Invalid keys type: ${problems}`));
return;
}
this.done = true;
this.#timeout(0);
if (this.#fulfill) this.#fulfill(this.data);
Expand Down
3 changes: 3 additions & 0 deletions metautil.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IncomingMessage } from 'node:http';
import { ScryptOptions, X509Certificate } from 'node:crypto';
import { Schema } from 'metaschema';

type Strings = Array<string>;
type Dictionary = Record<string, unknown>;
Expand Down Expand Up @@ -235,6 +236,7 @@ export function sizeToBytes(size: string): number;
export interface CollectorOptions {
exact?: boolean;
timeout?: number;
schema?: Schema;
}

type AsyncFunction = (...args: Array<unknown>) => Promise<unknown>;
Expand All @@ -245,6 +247,7 @@ export class Collector {
keys: Array<string>;
count: number;
exact: boolean;
schema: Schema | null;
timeout: number;
constructor(keys: Array<string>, options?: CollectorOptions);
set(key: string, value: unknown): void;
Expand Down
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.0",
"metaschema": "^2.1.5",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"metaschema": "^2.1.5",

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include a type schema in the 'd.ts' file, something like this:

interface ValidationResult {
  valid: boolean;
  errors: string[];
}

export class Schema {
  check(value: any): ValidationResult;
}

or is there another approach?

Copy link
Member Author

@Bespaliy Bespaliy Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For testing purposes, we can create a mock Schema class that imitates the functionality of the 'check' method based on the test requirements.

"metatests": "^0.8.2",
"prettier": "^3.1.1",
"typescript": "^5.3.3"
Expand Down
103 changes: 103 additions & 0 deletions test/collector.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { collect } = require('..');
const metatests = require('metatests');
const { Schema } = require('metaschema');

metatests.test('Collector: keys', async (test) => {
const expectedResult = { key1: 1, key2: 2 };
Expand Down Expand Up @@ -209,3 +210,105 @@ metatests.test('Collector: error in then chain', (test) => {
},
);
});

metatests.test('Collector: collect with schema validation', async (test) => {
const schema = Schema.from({
firstname: 'string',
lastname: 'string',
});
const ac1 = collect(['firstname', 'lastname'], { schema });

ac1.set('firstname', undefined);
ac1.set('lastname', 1);

const expectedResult1 = new Error(
'Invalid keys type: Field "firstname" not of expected type: string; Field "lastname" not of expected type: string',
);

try {
await ac1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all tests where you should catch an error, you should also throw an extra error if that test passed without the expected error. Otherwise, you simply won't notice that the test went wrong(after npm run test you will see simply one test less, that is, all test will go successfully without errors). It was recently added in all our tests.

So after all await in try you should add this line:
test.error(new Error('Should not be executed'));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await ac1;
await ac1;
test.error(new Error('Should not be executed'));

} catch (error) {
test.strictSame(error.message, expectedResult1.message);
}

const ac2 = collect(['firstname', 'lastname'], {
schema,
exact: false,
});

ac2.set('fullName', 'Michael Jordan');
ac2.set('firstname', 'Michael');
ac2.set('lastname', 'Jordan');

const expectedResult2 = new Error(
'Invalid keys type: Field "fullName" is not expected',
);
try {
await ac2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await ac2;
await ac2;
test.error(new Error('Should not be executed'));

} catch (error) {
test.strictSame(error.message, expectedResult2.message);
}

test.end();
});

metatests.test(
'Collector: compose collect with schema validation',
async (test) => {
const schema = Schema.from({
id: 'number',
profile: {
firstname: 'string',
lastname: 'string',
fullName: { type: 'string', required: false },
age: 'number',
},
});

const ac1 = collect(['id', 'profile'], { schema });
const profile1 = collect(['firstname', 'lastname', 'fullName', 'age']);

ac1.collect({ profile: profile1 });
ac1.set('id', 1);
profile1.set('firstname', 'Michael');
profile1.set('lastname', 'Jordan');
profile1.set('fullName', undefined);
profile1.set('age', 60);

const expectedResult1 = {
id: 1,
profile: {
firstname: 'Michael',
lastname: 'Jordan',
fullName: undefined,
age: 60,
},
};

const result1 = await ac1;

test.strictSame(result1, expectedResult1);

const ac2 = collect(['id', 'profile'], { schema });
const profile2 = collect(['firstname', 'lastname', 'fullName', 'age']);

ac2.collect({ profile: profile2 });
ac2.set('id', 'string');
profile2.set('firstname', undefined);
profile2.set('lastname', 'Jordan');
profile2.set('fullName', undefined);
profile2.set('age', 'string');

const expectedResult2 = new Error(
'Invalid keys type: Field "id" not of expected type: number; Field "profile.firstname" not of expected type: string; Field "profile.age" not of expected type: number',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Max line: 80

);

try {
await ac2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await ac2;
await ac2;
test.error(new Error('Should not be executed'));

} catch (error) {
test.strictSame(error.message, expectedResult2.message);
}

test.end();
},
);
Loading