Skip to content

Commit

Permalink
Merge pull request #3029 from threefoldtech/development_2.6_implement…
Browse files Browse the repository at this point in the history
…_class_validation

implement a way to validate class prop/methods when set/call
  • Loading branch information
MohamedElmdary authored Jul 3, 2024
2 parents db2daf4 + 40e824d commit b45604f
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 19 deletions.
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();
});
});

0 comments on commit b45604f

Please sign in to comment.