Skip to content

Commit

Permalink
feat(core): validators api v2 (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
pidkopajo authored Nov 26, 2020
1 parent 88e8ea5 commit 5ee0694
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 49 deletions.
11 changes: 10 additions & 1 deletion src/core/builders/create-form-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,21 @@ const createFieldDescriptor = (
return rootDescriptor;

case "array": {
const nth = (i: number) =>
const nthHandler = (i: number) =>
createFieldDescriptor(
decoder.inner as _FieldDecoderImpl<any>,
`${path}[${i}]`
);

const nth = defineProperties(nthHandler, {
__rootPath: {
value: path,
enumerable: false,
writable: false,
configurable: false,
},
});

return Object.assign(rootDescriptor, { nth });
}

Expand Down
86 changes: 70 additions & 16 deletions src/core/builders/create-form-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe("createFormValidator", () => {
const { validate } = createFormValidator(Schema, validate => [
validate({
field: Schema.choice,
rules: () => [x => (x === "A" ? "INVALID_CHOICE" : null)],
rules: () => [x => (x === "A" ? "INVALID_VALUE" : null)],
}),
]);
const getValue = () => "C" as any;
Expand Down Expand Up @@ -280,15 +280,16 @@ describe("createFormValidator", () => {

it("validate.each should run for each element of list", async () => {
const { validate } = createFormValidator(Schema, validate => [
validate.each({
field: Schema.arrayObjectString,
validate({
field: Schema.arrayObjectString.nth,
rules: () => [
x => wait(x.str === "invalid" ? "INVALID_VALUE" : null),
x => (x.str === "" ? "REQUIRED" : null),
x => wait(x.str?.length < 3 ? "TOO_SHORT" : null),
],
}),
]);

const getValue = (field: FieldDescriptor<any>): any => {
switch (impl(field).__path) {
case "arrayObjectString[0]":
Expand Down Expand Up @@ -322,12 +323,12 @@ describe("createFormValidator", () => {

it("validate.each for multiple arrays should run for each element of corresponding list", async () => {
const { validate } = createFormValidator(Schema, validate => [
validate.each({
field: Schema.arrayObjectString,
validate({
field: Schema.arrayObjectString.nth,
rules: () => [x => wait(x.str?.length < 3 ? "TOO_SHORT" : null)],
}),
validate.each({
field: Schema.arrayChoice,
validate({
field: Schema.arrayChoice.nth,
rules: () => [x => wait(x === "c" ? "INVALID_VALUE" : null)],
}),
]);
Expand Down Expand Up @@ -485,8 +486,8 @@ describe("createFormValidator", () => {
field: Schema.string,
rules: () => [x => (x.length < 3 ? "TOO_SHORT" : null)],
}),
validate.each({
field: Schema.arrayString,
validate({
field: Schema.arrayString.nth,
rules: () => [x => wait(x.length < 3 ? "TOO_SHORT" : null)],
}),
validate({
Expand Down Expand Up @@ -537,8 +538,8 @@ describe("createFormValidator", () => {
field: Schema.string,
rules: () => [pass],
}),
validate.each({
field: Schema.arrayString,
validate({
field: Schema.arrayString.nth,
rules: () => [pass],
}),
validate({
Expand Down Expand Up @@ -668,10 +669,10 @@ describe("createFormValidator", () => {
const { validate } = createFormValidator(Schema, validate => [
validate({
field: Schema.arrayObjectString,
rules: () => [x => (x.length < 3 ? "TOO_SHORT" : undefined)],
rules: () => [x => (x.length < 3 ? "TOO_SHORT" : null)],
}),
validate.each({
field: Schema.arrayObjectString,
validate({
field: Schema.arrayObjectString.nth,
rules: () => [x => (x.str === "" ? "REQUIRED" : null)],
}),
]);
Expand Down Expand Up @@ -742,8 +743,8 @@ describe("createFormValidator", () => {
field: Schema.objectObjectArrayObjectString.obj.array,
rules: () => [arrayValidator],
}),
validate.each({
field: Schema.objectObjectArrayObjectString.obj.array,
validate({
field: Schema.objectObjectArrayObjectString.obj.array.nth,
rules: () => [arrayItemValidator],
}),
validate({
Expand Down Expand Up @@ -793,4 +794,57 @@ describe("createFormValidator", () => {
expect(arrayItemValidator).toHaveBeenCalledTimes(2);
expect(stringValidator).toHaveBeenCalledTimes(1);
});

it("should work with simple signature", async () => {
const { validate } = createFormValidator(Schema, validate => [
validate(Schema.string, x => (x ? null : "REQUIRED")),
]);

const getValue = () => "" as any;

const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: "REQUIRED" }]);
});

it("should work with simple signature with array.nth", async () => {
const { validate } = createFormValidator(Schema, validate => [
validate(
Schema.arrayObjectString.nth,
x => wait(x.str === "invalid" ? "INVALID_VALUE" : null),
x => (x.str === "" ? "REQUIRED" : null),
x => wait(x.str?.length < 3 ? "TOO_SHORT" : null)
),
]);

const getValue = (field: FieldDescriptor<any>): any => {
switch (impl(field).__path) {
case "arrayObjectString[0]":
return { str: "sm" };
case "arrayObjectString[1]":
return { str: "" };
case "arrayObjectString[2]":
return { str: "valid string" };
case "arrayObjectString[3]":
return { str: "invalid" };
}
};

const validation = await validate({
fields: [
Schema.arrayObjectString.nth(0),
Schema.arrayObjectString.nth(1),
Schema.arrayObjectString.nth(2),
Schema.arrayObjectString.nth(3),
],
getValue,
});

expect(validation).toEqual([
{ field: Schema.arrayObjectString.nth(0), error: "TOO_SHORT" },
{ field: Schema.arrayObjectString.nth(1), error: "REQUIRED" },
{ field: Schema.arrayObjectString.nth(2), error: null },
{ field: Schema.arrayObjectString.nth(3), error: "INVALID_VALUE" },
]);
});
});
45 changes: 27 additions & 18 deletions src/core/builders/create-form-validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isFalsy } from "../../utils";
import { flatMap, uniqBy } from "../../utils/array";
import {
ArrayFieldDescriptor,
FieldDescriptor,
getArrayDescriptorChildren,
getObjectDescriptorChildren,
Expand All @@ -11,6 +12,8 @@ import { FormSchema } from "../types/form-schema";
import {
FieldValidator,
FormValidator,
ValidateConfig,
ValidateField,
ValidateFn,
ValidationTrigger,
Validator,
Expand Down Expand Up @@ -59,9 +62,8 @@ export const createFormValidator = <Values extends object, Err>(
const rootArrayPath = getRootArrayPath(path);

return allValidators.filter(x => {
const xPath = impl(x.field).__path;
const isFieldMatch = x.type === "field" && xPath === path;
const isEachMatch = x.type === "each" && xPath === rootArrayPath;
const isFieldMatch = x.type === "field" && x.path === path;
const isEachMatch = x.type === "each" && x.path === rootArrayPath;
const triggerMatches =
trigger && x.triggers ? x.triggers.includes(trigger) : true;

Expand Down Expand Up @@ -116,21 +118,28 @@ export const createFormValidator = <Values extends object, Err>(
return formValidator;
};

const validate: ValidateFn = config => ({
type: "field",
field: config.field,
triggers: config.triggers,
validators: config.rules,
dependencies: config.dependencies,
});

validate.each = config => ({
type: "each",
field: config.field as any,
triggers: config.triggers,
validators: config.rules,
dependencies: config.dependencies,
});
const validate: ValidateFn = <T, Err, Deps extends any[]>(
x: ValidateConfig<T, Err, Deps> | ValidateField<T, Err>,
...rules: Array<Validator<T, Err>>
): FieldValidator<T, Err, Deps> => {
const config: ValidateConfig<T, Err, Deps> =
(x as any)["field"] != null
? { ...(x as ValidateConfig<T, Err, Deps>) }
: { field: x as ValidateField<T, Err>, rules: () => rules };

const isNth = typeof config.field === "function";
const path = isNth
? impl(config.field as ArrayFieldDescriptor<T[], Err>["nth"]).__rootPath
: impl(config.field as FieldDescriptor<T, Err>).__path;

return {
type: isNth ? "each" : "field",
path,
triggers: config.triggers,
validators: config.rules,
dependencies: config.dependencies,
};
};

const runValidationForField = <Value, Err>(
validator: FieldValidator<Value, Err, unknown[]>,
Expand Down
5 changes: 5 additions & 0 deletions src/core/types/field-descriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export type _FieldDescriptorImpl<T> = {
__decoder: _FieldDecoderImpl<T>;
};

export type _NTHHandler<T> = {
__rootPath: string;
(n: number): _FieldDecoderImpl<T>;
};

/**
* Pointer to a form field.
* Used to interact with Formts API via `useField` hook.
Expand Down
46 changes: 45 additions & 1 deletion src/core/types/form-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ describe("validateFn", () => {
assert<IsExact<Actual, Expected>>(true);
});

it("resolves properly for array nth", () => {
const arrayFieldValidator = validator({
field: fd4.nth,
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,
Expand All @@ -79,12 +96,39 @@ describe("validateFn", () => {
it("resolves properly with no dependencies", () => {
const fieldValidator = validator({
field: fd1,
rules: () => [false],
rules: () => [null],
});

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

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

it("resolves properly for simple signature", () => {
const stringFieldValidator = validator(fd1, () => null);

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

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

it("resolves properly for simple signature for array", () => {
const stringFieldValidator = validator(fd4, () => null);

type Actual = typeof stringFieldValidator;
type Expected = FieldValidator<Date[], Err, []>;

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

it("resolves properly for simple signature for array.nth", () => {
const stringFieldValidator = validator(fd4.nth, () => null);

type Actual = typeof stringFieldValidator;
type Expected = FieldValidator<Date, Err, []>;

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

import {
ArrayFieldDescriptor,
Expand Down Expand Up @@ -45,29 +45,35 @@ export type FormValidator<Values extends object, Err> = {

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

export type ValidateFn = {
each: ValidateEachFn;
<T, Err, Dependencies extends any[]>(
config: ValidateConfig<T, Err, Dependencies>
): FieldValidator<T, Err, Dependencies>;

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

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

export type ValidateField<T, Err> =
| GenericFieldDescriptor<T, Err>
| ArrayFieldDescriptor<T[], Err>["nth"];

type FieldDescTuple<ValuesTuple extends readonly any[]> = {
[Index in keyof ValuesTuple]: GenericFieldDescriptor<ValuesTuple[Index]>;
Expand Down
5 changes: 5 additions & 0 deletions src/core/types/type-mapper-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ArrayFieldDescriptor,
ObjectFieldDescriptor,
_FieldDescriptorImpl,
_NTHHandler,
} from "./field-descriptor";
import { FormController, _FormControllerImpl } from "./form-controller";

Expand All @@ -26,6 +27,10 @@ type GetImplFn = {
<T>(it: FieldDecoder<T>): _FieldDecoderImpl<T>;

<V extends object, Err>(it: FormController): _FormControllerImpl<V, Err>;

<T extends any>(it: ArrayFieldDescriptor<T[], unknown>["nth"]): _NTHHandler<
T
>;
};

/**
Expand Down
Loading

0 comments on commit 5ee0694

Please sign in to comment.