diff --git a/src/core/builders/create-form-schema.ts b/src/core/builders/create-form-schema.ts index 7fcc07a..01833b6 100644 --- a/src/core/builders/create-form-schema.ts +++ b/src/core/builders/create-form-schema.ts @@ -81,12 +81,21 @@ const createFieldDescriptor = ( return rootDescriptor; case "array": { - const nth = (i: number) => + const nthHandler = (i: number) => createFieldDescriptor( decoder.inner as _FieldDecoderImpl, `${path}[${i}]` ); + const nth = defineProperties(nthHandler, { + __rootPath: { + value: path, + enumerable: false, + writable: false, + configurable: false, + }, + }); + return Object.assign(rootDescriptor, { nth }); } diff --git a/src/core/builders/create-form-validator.spec.ts b/src/core/builders/create-form-validator.spec.ts index fe03604..f885a51 100644 --- a/src/core/builders/create-form-validator.spec.ts +++ b/src/core/builders/create-form-validator.spec.ts @@ -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; @@ -280,8 +280,8 @@ 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), @@ -289,6 +289,7 @@ describe("createFormValidator", () => { ], }), ]); + const getValue = (field: FieldDescriptor): any => { switch (impl(field).__path) { case "arrayObjectString[0]": @@ -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)], }), ]); @@ -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({ @@ -537,8 +538,8 @@ describe("createFormValidator", () => { field: Schema.string, rules: () => [pass], }), - validate.each({ - field: Schema.arrayString, + validate({ + field: Schema.arrayString.nth, rules: () => [pass], }), validate({ @@ -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)], }), ]); @@ -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({ @@ -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 => { + 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" }, + ]); + }); }); diff --git a/src/core/builders/create-form-validator.ts b/src/core/builders/create-form-validator.ts index 64ac3a1..3bb980e 100644 --- a/src/core/builders/create-form-validator.ts +++ b/src/core/builders/create-form-validator.ts @@ -1,6 +1,7 @@ import { isFalsy } from "../../utils"; import { flatMap, uniqBy } from "../../utils/array"; import { + ArrayFieldDescriptor, FieldDescriptor, getArrayDescriptorChildren, getObjectDescriptorChildren, @@ -11,6 +12,8 @@ import { FormSchema } from "../types/form-schema"; import { FieldValidator, FormValidator, + ValidateConfig, + ValidateField, ValidateFn, ValidationTrigger, Validator, @@ -59,9 +62,8 @@ export const createFormValidator = ( 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; @@ -116,21 +118,28 @@ export const createFormValidator = ( 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 = ( + x: ValidateConfig | ValidateField, + ...rules: Array> +): FieldValidator => { + const config: ValidateConfig = + (x as any)["field"] != null + ? { ...(x as ValidateConfig) } + : { field: x as ValidateField, rules: () => rules }; + + const isNth = typeof config.field === "function"; + const path = isNth + ? impl(config.field as ArrayFieldDescriptor["nth"]).__rootPath + : impl(config.field as FieldDescriptor).__path; + + return { + type: isNth ? "each" : "field", + path, + triggers: config.triggers, + validators: config.rules, + dependencies: config.dependencies, + }; +}; const runValidationForField = ( validator: FieldValidator, diff --git a/src/core/types/field-descriptor.ts b/src/core/types/field-descriptor.ts index 0542413..f80c688 100644 --- a/src/core/types/field-descriptor.ts +++ b/src/core/types/field-descriptor.ts @@ -9,6 +9,11 @@ export type _FieldDescriptorImpl = { __decoder: _FieldDecoderImpl; }; +export type _NTHHandler = { + __rootPath: string; + (n: number): _FieldDecoderImpl; +}; + /** * Pointer to a form field. * Used to interact with Formts API via `useField` hook. diff --git a/src/core/types/form-validator.spec.ts b/src/core/types/form-validator.spec.ts index ec7c0ae..b454d63 100644 --- a/src/core/types/form-validator.spec.ts +++ b/src/core/types/form-validator.spec.ts @@ -59,6 +59,23 @@ describe("validateFn", () => { assert>(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>(true); + }); + it("resolves properly for object array", () => { const objFieldValidator = validator({ field: fd5, @@ -79,7 +96,7 @@ describe("validateFn", () => { it("resolves properly with no dependencies", () => { const fieldValidator = validator({ field: fd1, - rules: () => [false], + rules: () => [null], }); type Actual = typeof fieldValidator; @@ -87,4 +104,31 @@ describe("validateFn", () => { assert>(true); }); + + it("resolves properly for simple signature", () => { + const stringFieldValidator = validator(fd1, () => null); + + type Actual = typeof stringFieldValidator; + type Expected = FieldValidator; + + assert>(true); + }); + + it("resolves properly for simple signature for array", () => { + const stringFieldValidator = validator(fd4, () => null); + + type Actual = typeof stringFieldValidator; + type Expected = FieldValidator; + + assert>(true); + }); + + it("resolves properly for simple signature for array.nth", () => { + const stringFieldValidator = validator(fd4.nth, () => null); + + type Actual = typeof stringFieldValidator; + type Expected = FieldValidator; + + assert>(true); + }); }); diff --git a/src/core/types/form-validator.ts b/src/core/types/form-validator.ts index 124d364..092ac70 100644 --- a/src/core/types/form-validator.ts +++ b/src/core/types/form-validator.ts @@ -1,4 +1,4 @@ -import { Falsy } from "../../utils"; +import { Falsy, NoInfer } from "../../utils"; import { ArrayFieldDescriptor, @@ -45,29 +45,35 @@ export type FormValidator = { export type FieldValidator = { type: "field" | "each"; - field: FieldDescriptor; + path: string; triggers?: Array; validators: (...deps: [...Dependencies]) => Array>; dependencies?: readonly [...FieldDescTuple]; }; export type ValidateFn = { - each: ValidateEachFn; + ( + config: ValidateConfig + ): FieldValidator; - (config: { - field: GenericFieldDescriptor; - triggers?: ValidationTrigger[]; - dependencies?: readonly [...FieldDescTuple]; - rules: (...deps: [...Dependencies]) => Array>; - }): FieldValidator; + ( + field: ValidateField, + ...rules: Array>> + ): FieldValidator; }; -export type ValidateEachFn = (config: { - field: ArrayFieldDescriptor; +export type ValidateConfig = { + field: ValidateField; triggers?: ValidationTrigger[]; dependencies?: readonly [...FieldDescTuple]; - rules: (...deps: [...Dependencies]) => Array>; -}) => FieldValidator; + rules: ( + ...deps: [...Dependencies] + ) => Array>>; +}; + +export type ValidateField = + | GenericFieldDescriptor + | ArrayFieldDescriptor["nth"]; type FieldDescTuple = { [Index in keyof ValuesTuple]: GenericFieldDescriptor; diff --git a/src/core/types/type-mapper-util.ts b/src/core/types/type-mapper-util.ts index d91a7bd..d77a294 100644 --- a/src/core/types/type-mapper-util.ts +++ b/src/core/types/type-mapper-util.ts @@ -4,6 +4,7 @@ import { ArrayFieldDescriptor, ObjectFieldDescriptor, _FieldDescriptorImpl, + _NTHHandler, } from "./field-descriptor"; import { FormController, _FormControllerImpl } from "./form-controller"; @@ -26,6 +27,10 @@ type GetImplFn = { (it: FieldDecoder): _FieldDecoderImpl; (it: FormController): _FormControllerImpl; + + (it: ArrayFieldDescriptor["nth"]): _NTHHandler< + T + >; }; /** diff --git a/src/utils/utility-types.ts b/src/utils/utility-types.ts index 951f0bf..2028ed3 100644 --- a/src/utils/utility-types.ts +++ b/src/utils/utility-types.ts @@ -45,3 +45,5 @@ export type WidenType = [T] extends [string] : [T] extends [boolean] ? boolean : T; + +export type NoInfer = [A][A extends any ? 0 : never];