Skip to content

Commit

Permalink
feat: validation types (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
pidkopajo authored Oct 10, 2020
1 parent 57c2a2d commit 9d08ec4
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/core/builders/create-form-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { assert, IsExact } from "conditional-type-checks";

import { FormValidator } from "../types/form-validator";

import { createFormSchema } from "./create-form-schema";
import { createFormValidator } from "./create-form-validator";

describe("createFormValidator", () => {
const Schema = createFormSchema(
fields => ({
string: fields.string(),
choice: fields.choice("A", "B", "C"),
num: fields.number(),
bool: fields.bool(),
}),
errors => errors<"err1" | "err2">()
);

it("resolves ok", () => {
const formValidator = createFormValidator(Schema, _validate => []);

type Actual = typeof formValidator;
type Expected = FormValidator<
{
string: string;
choice: "A" | "B" | "C";
num: number | "";
bool: boolean;
},
"err1" | "err2"
>;

assert<IsExact<Actual, Expected>>(true);
});
});
43 changes: 43 additions & 0 deletions src/core/builders/create-form-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FormSchema } from "../types/form-schema";
import {
FieldValidator,
FormValidator,
ValidateFn,
} from "../types/form-validator";

/**
* Create form validator based on provided set of validation rules.
* Error type of all validation rules is specified by the FormSchema.
* You can also specify validation dependencies between fields and validation triggers.
*
* @example
* ```
* const validator = createForm.validator(Schema, validate => [
* validate({
* field: Schema.password,
* rules: () => [required(), minLength(6)]
* }),
* validate({
* field: Schema.passwordConfirm,
* dependencies: [Schema.password],
* triggers: ["blur", "submit"],
* rules: (password) => [
* required(),
* val => val === password ? null : { code: "passwordMismatch" },
* ]
* }),
* validate.each({
* field: Schema.promoCodes,
* rules: () => [optional(), exactLength(6)],
* })
* ])
* ```
*/
export const createFormValidator = <Values extends object, Err>(
_schema: FormSchema<Values, Err>,
_builder: (
validate: ValidateFn
) => Array<FieldValidator<unknown, Err, unknown[]>>
): FormValidator<Values, Err> => {
return {} as any;
};
4 changes: 4 additions & 0 deletions src/core/hooks/use-formts/use-formts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
objectDescriptorKeys,
_DescriptorApprox_,
} from "../../types/form-schema-approx";
import { FormValidator } from "../../types/form-validator";
import { impl, opaque } from "../../types/type-mapper-util";

import { createInitialValues } from "./create-initial-values";
Expand All @@ -38,6 +39,9 @@ export type FormtsOptions<Values extends object, Err> = {
* The defaults depend on field type (defined in the Schema).
*/
initialValues?: DeepPartial<Values>;

/** Form validator created using `createForm.validator` function (optional). */
validator?: FormValidator<Values, Err>;
};

type FormtsReturn<Values extends object, Err> = [
Expand Down
87 changes: 87 additions & 0 deletions src/core/types/form-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { assert, IsExact } from "conditional-type-checks";

import { FieldDescriptor } from "./field-descriptor";
import { FieldValidator, ValidateFn } from "./form-validator";

const validator: ValidateFn = (() => {}) as any;

describe("validateFn", () => {
type Err = { code: "err1" | "err2" };
const fd1: FieldDescriptor<string, Err> = {} as any;
const fd2: FieldDescriptor<number, Err> = {} as any;
const fd3: FieldDescriptor<"a" | "b" | "c", Err> = {} as any;
const fd4: FieldDescriptor<Date[], Err> = {} as any;
const fd5: FieldDescriptor<{ parent: { child: string[] } }, Err> = {} as any;

it("resolves properly for string", () => {
const stringFieldValidator = validator({
field: fd1,
dependencies: [fd2, fd3],
rules: (_number, _choice) => [false],
});

type Actual = typeof stringFieldValidator;
type Expected = FieldValidator<string, Err, [number, "a" | "b" | "c"]>;

assert<IsExact<Actual, Expected>>(true);
});

it("resolves properly for choice", () => {
const choiceFieldValidator = validator({
field: fd3,
dependencies: [fd1, fd4],
rules: (_string, _dateArray) => [false],
});

type Actual = typeof choiceFieldValidator;
type Expected = FieldValidator<"a" | "b" | "c", Err, [string, Date[]]>;

assert<IsExact<Actual, Expected>>(true);
});

it("resolves properly for array", () => {
const arrayFieldValidator = validator({
field: fd4,
dependencies: [fd1, fd2, fd3, fd5],
rules: (_string, _number, _choice, _obj) => [false],
});

type Actual = typeof arrayFieldValidator;
type Expected = FieldValidator<
Date[],
Err,
[string, number, "a" | "b" | "c", { parent: { child: string[] } }]
>;

assert<IsExact<Actual, Expected>>(true);
});

it("resolves properly for object array", () => {
const objFieldValidator = validator({
field: fd5,
dependencies: [fd1],
rules: _string => [false],
});

type Actual = typeof objFieldValidator;
type Expected = FieldValidator<
{ parent: { child: string[] } },
Err,
[string]
>;

assert<IsExact<Actual, Expected>>(true);
});

it("resolves properly with no dependencies", () => {
const fieldValidator = validator({
field: fd1,
rules: () => [false],
});

type Actual = typeof fieldValidator;
type Expected = FieldValidator<string, Err, []>;

assert<IsExact<Actual, Expected>>(true);
});
});
62 changes: 62 additions & 0 deletions src/core/types/form-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Falsy } from "../../utils";

import { FieldDescriptor } from "./field-descriptor";

/**
* Function responsible for validating single field.
*
* @param value - value to be validated of type `T`
*
* @returns validation error of type `Err`, or `null` when field is valid
*/
export type Validator<T, Err> = Validator.Sync<T, Err>;
// | Validator.Async<T, Err>;

export namespace Validator {
export type Sync<T, Err> = {
(value: T): Err | null;
};
export type Async<T, Err> = {
(value: T): Promise<Err | null>;
};
}

export type ValidationTrigger = "change" | "blur" | "submit";

export type FormValidator<Values extends object, Err> = {
validate: (
fields: Array<FieldDescriptor<unknown, Err>>,
getValue: <P>(field: FieldDescriptor<P, Err>) => P,
trigger?: ValidationTrigger
) => Array<{ field: FieldDescriptor<unknown, Err>; error: Err | null }>;
};

export type FieldValidator<T, Err, Dependencies extends any[]> = {
type: "inner" | "outer";
field: FieldDescriptor<T, Err>;
validators: (...deps: [...Dependencies]) => Array<Falsy | Validator<T, Err>>;
triggers?: Array<ValidationTrigger>;
dependencies?: readonly [...FieldDescTuple<Dependencies>];
};

export type ValidateFn = {
each: ValidateEachFn;

<T, Err, Dependencies extends any[]>(config: {
field: FieldDescriptor<T, Err>;
triggers?: ValidationTrigger[];
dependencies?: readonly [...FieldDescTuple<Dependencies>];
rules: (...deps: [...Dependencies]) => Array<Falsy | Validator<T, Err>>;
}): FieldValidator<T, Err, Dependencies>;
};

export type ValidateEachFn = <T, Err, Dependencies extends any[]>(config: {
field: FieldDescriptor<T[], Err>;
triggers?: ValidationTrigger[];
dependencies?: readonly [...FieldDescTuple<Dependencies>];
rules: (...deps: [...Dependencies]) => Array<Falsy | Validator<T, Err>>;
}) => FieldValidator<T[], Err, Dependencies>;

type FieldDescTuple<ValuesTuple extends readonly any[]> = {
[Index in keyof ValuesTuple]: FieldDescriptor<ValuesTuple[Index]>;
};
2 changes: 2 additions & 0 deletions src/utils/utility-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ export type DeepPartial<T> = T extends Function
export type IdentityDict<T extends string> = IsUnion<T> extends true
? { [K in T]: K }
: never;

export type Falsy = null | undefined | false;

0 comments on commit 9d08ec4

Please sign in to comment.