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

implement a way to validate class prop/methods when set/call #3029

Merged
Merged
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
149 changes: 148 additions & 1 deletion packages/grid_client/src/helpers/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/*
* <script lang="ts">
* import { isLength, isInt } from "class-validator";
* // Example 1
* @ValidateMembers()
* class User {
* @isLength(2) name: string
* @isInt() age: number
*
* greeting() {
* // Some logic
* }
* }
* </script> */

/**
* @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<ValidationOptions> {
return {
props: options?.props ?? true,
methods: options?.methods ?? true,
};
}

function _getProps(ctor: any, options: Required<ValidationOptions>): 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<ValidationOptions>): 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 };
3 changes: 3 additions & 0 deletions packages/grid_client/src/zos/computecapacity.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
40 changes: 22 additions & 18 deletions packages/grid_client/tests/modules/compute_capacity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading