Skip to content

Commit

Permalink
refactor: Move guard logic to a new TObject
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkarolyi committed Mar 8, 2022
1 parent ffd947d commit 48713e5
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 29 deletions.
55 changes: 46 additions & 9 deletions __tests__/validators.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Guard } from "../src/guard";
import { Validator } from "../src/types";
import { GuardedType, Validator } from "../src/types";
import {
TString,
TNumber,
TBoolean,
TFunction,
TObject,
TAnyObject,
TUndefined,
TBigInt,
TNull,
Expand All @@ -32,6 +32,7 @@ import {
TStringUUID,
TValidate,
TConstant,
TObject,
} from "../src/validators";

describe("Validators", () => {
Expand Down Expand Up @@ -190,23 +191,23 @@ describe("Validators", () => {
});
});

describe("TObject", () => {
describe("TAnyObject", () => {
it("is an instance of Validator", () => {
expect(TObject).toBeInstanceOf(Validator);
expect(TAnyObject).toBeInstanceOf(Validator);
});

it("it's name is 'object'", () => {
expect(TObject.name).toBe("object");
expect(TAnyObject.name).toBe("object");
});

it("returns true if an object was given", () => {
expect(TObject.isValid({})).toBe(true);
expect(TAnyObject.isValid({})).toBe(true);
});

it("returns false if not an object value was given", () => {
expect(TObject.isValid(() => 10)).toBe(false);
expect(TObject.isValid(null)).toBe(false);
expect(TObject.isValid(BigInt(100))).toBe(false);
expect(TAnyObject.isValid(() => 10)).toBe(false);
expect(TAnyObject.isValid(null)).toBe(false);
expect(TAnyObject.isValid(BigInt(100))).toBe(false);
});
});

Expand Down Expand Up @@ -727,5 +728,41 @@ describe("Validators", () => {
expect(TConstant("2").isValid("2")).toBe(true);
});
});

describe("TObject", () => {
it("is an instance of Validator", () => {
expect(TObject({})).toBeInstanceOf(Validator);
});

it("it's name is the object schema as a JSON", () => {
expect(TObject({ name: TString, age: TNumber }).name).toBe(
'{"name":"string","age":"number"}'
);
expect(
TObject({ name: TString, age: TArray(TOr(TNumber, TString)) }).name
).toBe('{"name":"string","age":"(number | string)[]"}');
});

it("returns false, if the given value not matches the object schema", () => {
expect(TObject({ foo: TString }).isValid({ foo: 5 })).toBe(false);
expect(TObject({ foo: TString }).isValid(5)).toBe(false);
expect(TObject({}).isValid(10)).toBe(false);
expect(
TObject({ foo: { bar: TNumber }, baz: TString }).isValid({
foo: { bar: 10 },
baz: 10,
})
).toBe(false);
expect(TObject({ foo: {} }).isValid({ foo: 10 })).toBe(false);
});

it("returns true, if the given value matches the object schema", () => {
expect(TObject({ foo: TString }).isValid({ foo: "bar" })).toBe(true);
expect(TObject({}).isValid({ foo: "bar" })).toBe(true);
expect(TObject({}).isValid({})).toBe(true);
expect(TObject({ foo: {} }).isValid({ foo: {} })).toBe(true);
expect(TObject({ foo: {} }).isValid({ foo: { bar: 20 } })).toBe(true);
});
});
});
});
40 changes: 21 additions & 19 deletions src/guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import has from "lodash/has";
import get from "lodash/get";
import isEmpty from "lodash/isEmpty";
import { Tree } from "./tree";
import {
Constructor,
Expand All @@ -9,6 +10,7 @@ import {
ValidatorOrConstructor,
} from "./types";
import { MissingValueError, InvalidValueError } from "./errors";
import { TAnyObject } from "./validators";

/**
* Guards a type defined by the given schema.
Expand Down Expand Up @@ -122,10 +124,13 @@ export class Guard<S extends Schema> extends Validator<SchemaType<S>> {
return value as SchemaType<S>;
}

const result = this.schema.find(
(validator, path) =>
!has(value, path) || !validator.isValid(get(value, path))
);
if (typeof value !== "object") {
throw new InvalidValueError([], this.name);
}

const result = this.schema.find((validator, path) => {
return !has(value, path) || !validator.isValid(get(value, path));
});

if (!result) return value;

Expand All @@ -135,28 +140,25 @@ export class Guard<S extends Schema> extends Validator<SchemaType<S>> {
}

private resolveSchema(schema: Schema): Tree<Validator<unknown>> {
const tree = this.createTreeFromSchema(schema);
return this.instantiateValidatorsInSchemaTree(tree);
return this.createTreeFromSchema(schema);
}

private createTreeFromSchema(schema: Schema): Tree<ValidatorOrConstructor> {
const isValidatorOrConstructor = (
value: Schema
): value is Validator<unknown> | Constructor<Validator<unknown>> =>
value instanceof Validator || typeof value === "function";

return new Tree({
private createTreeFromSchema(schema: Schema): Tree<Validator<unknown>> {
const tree = new Tree({
definition: schema,
isLeafNode: isValidatorOrConstructor,
isLeafNode: (
value: Schema
): value is Validator<unknown> | Constructor<Validator<unknown>> =>
value instanceof Validator ||
typeof value === "function" ||
(typeof value === "object" && isEmpty(value)),
});
}

private instantiateValidatorsInSchemaTree(
tree: Tree<ValidatorOrConstructor>
): Tree<Validator<unknown>> {
return tree.map(
(value) => {
return value instanceof Validator ? value : new value();
if (value instanceof Validator) return value;
if (typeof value === "object" && isEmpty(value)) return TAnyObject;
return new value();
},
(value): value is Validator<unknown> => value instanceof Validator
);
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ValidationError } from "./errors";
import { TreeDefinition } from "./tree";

/**
Expand All @@ -8,6 +9,14 @@ import { TreeDefinition } from "./tree";
export abstract class Validator<T> {
abstract readonly name: string;
abstract isValid(value: any): value is T;
cast(value: any): T {
if (this.isValid(value)) return value as T;
throw new ValidationError(
`The given value is not a valid ${this.name}`,
[],
this.name
);
}
}

export type ValidatorOrConstructor<T = unknown> =
Expand Down
14 changes: 13 additions & 1 deletion src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Guard } from "./guard";
import {
ArrayType,
GuardedType,
Schema,
SchemaType,
Validator,
ValidatorOrConstructor,
} from "./types";
Expand Down Expand Up @@ -102,7 +104,7 @@ export const TFunction = TValidate<Function>(
*
* `validator.name`: `"object"`
*/
export const TObject = TValidate<Object>(
export const TAnyObject = TValidate<Object>(
"object",
(value) => typeof value === "object" && value !== null
);
Expand Down Expand Up @@ -691,3 +693,13 @@ export function TConstant<T extends string | number | boolean | BigInt>(
(value) => value === constant
);
}

export type ObjectSchema = {
[fieldName: string]: ObjectSchema | Validator<any>;
};

export function TObject<T extends ObjectSchema>(
schema: T
): Validator<SchemaType<T>> {
return new Guard(schema);
}

0 comments on commit 48713e5

Please sign in to comment.