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

feat: validation types #17

Merged
merged 3 commits into from
Oct 10, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
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 @@ -23,6 +23,7 @@ import {
_DescriptorApprox_,
} from "../../types/form-schema-approx";
import { impl, opaque } from "../../types/type-mapper-util";
import { FormValidator } from "../../validation";
pidkopajo marked this conversation as resolved.
Show resolved Hide resolved

import { createInitialValues } from "./create-initial-values";
import { createReducer, getInitialState } from "./reducer";
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
36 changes: 36 additions & 0 deletions src/core/validation/create-form-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { assert, IsExact } from "conditional-type-checks";

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

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

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", () => {
//@ts-ignore
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);
});
});
42 changes: 42 additions & 0 deletions src/core/validation/create-form-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FormSchema } from "../types/form-schema";

import { FieldValidator, FormValidator, ValidateFn } from "./types";

/**
* 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>(
//@ts-ignore
schema: FormSchema<Values, Err>,
//@ts-ignore
builder: (
validate: ValidateFn
) => Array<FieldValidator<unknown, Err, unknown[]>>
): FormValidator<Values, Err> => {
return {} as any;
};
2 changes: 2 additions & 0 deletions src/core/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./types";
export * from "./create-form-validator";
92 changes: 92 additions & 0 deletions src/core/validation/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { assert, IsExact } from "conditional-type-checks";

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

import { FieldValidator, ValidateFn } from "./types";

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],
//@ts-ignore
pidkopajo marked this conversation as resolved.
Show resolved Hide resolved
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],
//@ts-ignore
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],
//@ts-ignore
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],
//@ts-ignore
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/validation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Falsy } from "../../utils";
import { FieldDescriptor } from "../types/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>;
pidkopajo marked this conversation as resolved.
Show resolved Hide resolved

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;