diff --git a/packages/grid_client/src/helpers/validator.ts b/packages/grid_client/src/helpers/validator.ts index 15bb9c4a75..2d6c5cb809 100644 --- a/packages/grid_client/src/helpers/validator.ts +++ b/packages/grid_client/src/helpers/validator.ts @@ -31,4 +31,151 @@ function validateHexSeed(seed: string, length: number): boolean { return true; } -export { validateObject, validateInput, validateHexSeed }; +interface ValidationOptions { + props?: boolean | string | string[]; + methods?: boolean | string | string[]; +} + +/* + * */ + +/** + * @description + * This `ValidateMembers` is a config method which returns back a *classDecrator* + * Allows to configure which setter/methods should trigger validation for that specific class + * + * Example As follow + * @example + * ```typescript + * import { isLength, isInt } from "class-validator"; + * + * // ⁣@ValidateMembers({ props: false, methods: true }) // - disable validation on set props + * // ⁣@ValidateMembers({ props: true, methods: false }) // - disable validation on call methods + * // ⁣@ValidateMembers({ props: 'name', methods: false }) // - validate only on setting 'name' prop + * // And so on... + * ⁣@ValidateMembers() // = ⁣@ValidateMembers({ props: true, methods: true }) + * class User { + * ⁣@isLength(2) name: string + * ⁣⁣@isInt() age: number + * + * greeting() { + * // Some logic + * } + * } + * ``` + * + * + * + * @param options { ValidationOptions | undefined } + * @returns { ClassDecorator } + */ +function ValidateMembers(options?: ValidationOptions): ClassDecorator { + const _options = _normalizeValidationOptions(options); + return (target: any): any => { + const methods = _getMethods(target, _options); + for (const method of methods) { + const fn = target.prototype[method]; + target.prototype[method] = function (...args: any[]): any { + const errors = validateSync(this); + if (errors.length) { + throw errors; + } + return fn.apply(this, args); + }; + } + + return class extends target { + constructor(...args: any[]) { + super(...args); + + const props = _getProps(this, _options); + for (const prop of props) { + let _value = this[prop]; + + Object.defineProperty(this, prop, { + configurable: false, + enumerable: true, + get: () => _value, + set(value) { + _value = value; + const errors = validateSync(this); + for (const error of errors) { + if (error.property === prop) { + throw error; + } + } + }, + }); + } + } + }; + }; +} + +function _normalizeValidationOptions(options?: ValidationOptions): Required { + return { + props: options?.props ?? true, + methods: options?.methods ?? true, + }; +} + +function _getProps(ctor: any, options: Required): string[] { + /* This env variable should be used while testing to prevent throw error while setting values */ + if (process.env.SKIP_PROPS_VALIDATION) { + return []; + } + + if (options.props === true) { + return Object.getOwnPropertyNames(ctor); + } + + if (typeof options.props === "string") { + return [options.props]; + } + + if (Array.isArray(options.props)) { + return options.props; + } + + return []; +} + +function _getMethods(ctor: any, options: Required): string[] { + /* This env variable should be used to prevent throw error while calling methods if needed */ + if (process.env.SKIP_METHODS_VALIDATION) { + return []; + } + + if (options.methods === true) { + const methods = Object.getOwnPropertyNames(ctor.prototype); + const constructorIndex = methods.indexOf("constructor"); + if (constructorIndex !== -1) { + methods.splice(constructorIndex, 1); + } + return methods; + } + + if (typeof options.methods === "string") { + return [options.methods]; + } + + if (Array.isArray(options.methods)) { + return options.methods; + } + + return []; +} + +export { validateObject, validateInput, validateHexSeed, type ValidationOptions, ValidateMembers }; diff --git a/packages/grid_client/src/zos/computecapacity.ts b/packages/grid_client/src/zos/computecapacity.ts index 6b9ac9f33d..d4939a3e2f 100644 --- a/packages/grid_client/src/zos/computecapacity.ts +++ b/packages/grid_client/src/zos/computecapacity.ts @@ -1,6 +1,9 @@ import { Expose } from "class-transformer"; import { IsInt, Max, Min } from "class-validator"; +import { ValidateMembers } from "../helpers/validator"; + +@ValidateMembers() class ComputeCapacity { @Expose() @IsInt() @Min(1) @Max(32) cpu: number; @Expose() @IsInt() @Min(256 * 1024 ** 2) @Max(256 * 1024 ** 3) memory: number; // in bytes diff --git a/packages/grid_client/tests/modules/compute_capacity.test.ts b/packages/grid_client/tests/modules/compute_capacity.test.ts index 81501d6908..197bfcd074 100644 --- a/packages/grid_client/tests/modules/compute_capacity.test.ts +++ b/packages/grid_client/tests/modules/compute_capacity.test.ts @@ -6,62 +6,66 @@ beforeEach(() => { computeCapacity = new ComputeCapacity(); }); describe("Compute Capacity module", () => { - test.skip("Compute Capacity instance is of type ComputeCapacity.", () => { + test("Compute Capacity instance is of type ComputeCapacity.", () => { expect(computeCapacity).toBeInstanceOf(ComputeCapacity); }); // The following tests are skipped as there's an issue w input validation. Should be returned once validation is fixed here: https://github.com/threefoldtech/tfgrid-sdk-ts/issues/2821 - test.skip("Min values for cpu & memory.", () => { + test("Min values for cpu & memory.", () => { const cpu = 0; const mem = 255 * 1024 ** 2; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); - test.skip("Max values for cpu & memory.", () => { + test("Max values for cpu & memory.", () => { const cpu = 33; const mem = 255 * 1024 ** 4; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); - test.skip("cpu & memory doesn't accept decimal values.", () => { + test("cpu & memory doesn't accept decimal values.", () => { const cpu = 1.5; const mem = 1.2; - computeCapacity.cpu = cpu; - computeCapacity.memory = mem; - + const setCPU = () => (computeCapacity.cpu = cpu); + const setMem = () => (computeCapacity.memory = mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); - test.skip("cpu & memory empty values.", () => { + test("cpu & memory empty values.", () => { const result = () => computeCapacity.challenge(); expect(result).toThrow(); }); - test.skip("An error should be thrown if cpu & memory negative values.", () => { + test("An error should be thrown if cpu & memory negative values.", () => { const negative_cpu = -1; const negative_mem = -1; - computeCapacity.cpu = negative_cpu; - computeCapacity.memory = negative_mem; - + const setCPU = () => (computeCapacity.cpu = negative_cpu); + const setMem = () => (computeCapacity.memory = negative_mem); const result = () => computeCapacity.challenge(); + expect(setCPU).toThrow(); + expect(setMem).toThrow(); expect(result).toThrow(); }); });