From b631c165a7de286ccd139bd654609580b9daecf8 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Mon, 14 Jul 2025 19:56:00 -0600 Subject: [PATCH 01/11] Implement baseForgeType function and string validation method for enhanced type handling and validation flexibility --- proposal.ts | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 proposal.ts diff --git a/proposal.ts b/proposal.ts new file mode 100644 index 0000000..31a105c --- /dev/null +++ b/proposal.ts @@ -0,0 +1,120 @@ +type ForgeMethod = { + fn: (value: unknown) => boolean | Promise; + caller?: string; +}; + +type VerificationResult = + | { success: true; value: T } + | { success: false; errorMessage: string }; + +function baseForgeType(config: { + isOptional: boolean; + isNullable: boolean; + queue: ForgeMethod[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methods: Record ForgeMethod>; +}) { + const addToForge = (method: ForgeMethod) => { + return baseForgeType({ + ...config, + queue: [...config.queue, method] + }); + }; + + const optional = () => { + return baseForgeType({ + ...config, + isOptional: true + }); + }; + + const nullable = () => { + return baseForgeType({ + ...config, + isNullable: true + }); + }; + + const check = ( + fn: (value: T) => boolean | Promise, + config?: { errorMessage?: string; path?: string[]; loose?: boolean } + ) => { + return addToForge({ fn, caller: 'check', ...config }); + }; + + const forge = (value: T): VerificationResult => { + for (const method of config.queue) { + const result = method.fn(value); + if (result instanceof Promise) { + return { + success: false, + errorMessage: 'Async validation not supported' + }; + } + if (!result) { + return { + success: false, + errorMessage: `Validation failed in ${method.caller}` + }; + } + } + return { success: true, value }; + }; + + return { + _addToForge: addToForge, + isOptional: config.isOptional, + isNullable: config.isNullable, + optional, + nullable, + check, + forge, + ...config.methods + }; +} + +const boolean = () => { + return baseForgeType({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => typeof value === 'boolean', + caller: 'boolean' + } + ], + methods: {} + }); +}; + +const string = () => + baseForgeType({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => typeof value === 'string', + caller: 'string' + } + ], + methods: { + regExp: function (regExp: RegExp) { + return this._addToForge({ + fn: (value: T): boolean | Promise => { + return typeof value === 'string' && regExp.test(value); + }, + caller: 'regExp' + }); + } + } + }); + +boolean().forge(true); // ? +string() + .nullable() + .optional() + .check((value) => {}) + .nullable() + .optional() + .check((value) => {}) + .regExp(/abc/); // ? From 04c742e7fbfa8b8b2897b236fc4f56294b03b5ca Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Tue, 15 Jul 2025 19:12:27 -0600 Subject: [PATCH 02/11] Add baseForgeType and type definitions for boolean, number, and string validations; remove legacy proposal.ts --- lib/vProposal/baseForgeType.ts | 113 +++++++++++++++++++++++++++++++ lib/vProposal/boolean.ts | 14 ++++ lib/vProposal/index.ts | 10 +++ lib/vProposal/number.ts | 67 ++++++++++++++++++ lib/vProposal/string.ts | 46 +++++++++++++ proposal.ts | 120 --------------------------------- 6 files changed, 250 insertions(+), 120 deletions(-) create mode 100644 lib/vProposal/baseForgeType.ts create mode 100644 lib/vProposal/boolean.ts create mode 100644 lib/vProposal/index.ts create mode 100644 lib/vProposal/number.ts create mode 100644 lib/vProposal/string.ts delete mode 100644 proposal.ts diff --git a/lib/vProposal/baseForgeType.ts b/lib/vProposal/baseForgeType.ts new file mode 100644 index 0000000..42df18c --- /dev/null +++ b/lib/vProposal/baseForgeType.ts @@ -0,0 +1,113 @@ +type UnknownObject = Record; + +export type ForgeMethodConfig = { + errorMessage?: string; + path?: string[]; + loose?: boolean; +}; + +export type ForgeMethod = ForgeMethodConfig & { + fn: (value: unknown) => boolean | Promise; + caller: string; +}; + +export type UnsuccessfulVerificationResult = { + success: false; + caller: string; + errorMessage?: string; + path?: string[]; + issues?: UnsuccessfulVerificationResult[]; + arrayIndex?: number; +}; + +export type VerificationResult = + | { success: true; value: T } + | UnsuccessfulVerificationResult; + +type BaseForgeTypeConfig = { + isOptional: boolean; + isNullable: boolean; + queue: ForgeMethod[]; + methods?: (addToForge: (method: ForgeMethod) => unknown) => Methods; +}; + +type BaseForgeType = { + isOptional: boolean; + isNullable: boolean; + optional: () => BaseForgeType; + nullable: () => BaseForgeType; + check: ( + fn: (value: T) => boolean | Promise, + config?: { errorMessage?: string; path?: string[]; loose?: boolean } + ) => BaseForgeType; + forge: (value: T) => VerificationResult; +} & { + [K in keyof Methods]: Methods[K] extends (...args: infer FArgs) => unknown + ? (...args: FArgs) => BaseForgeType + : Methods[K]; +}; + +export const baseForgeType = < + FType, + Methods extends UnknownObject = UnknownObject +>( + config: BaseForgeTypeConfig +) => { + const addToForge = (method: ForgeMethod): BaseForgeType => { + return baseForgeType({ + ...config, + queue: [...config.queue, method] + }); + }; + + const optional = (): BaseForgeType => { + return baseForgeType({ + ...config, + isOptional: true + }); + }; + + const nullable = (): BaseForgeType => { + return baseForgeType({ + ...config, + isNullable: true + }); + }; + + const check = ( + fn: (value: T) => boolean | Promise, + config?: ForgeMethodConfig + ): BaseForgeType => { + return addToForge({ fn, caller: 'check', ...config }); + }; + + const forge = (value: T): VerificationResult => { + for (const method of config.queue) { + const { fn, ...methodConfig } = method; + const result = fn(value); + if (result instanceof Promise) { + return { + success: false, + ...methodConfig + }; + } + if (!result) { + return { + success: false, + ...methodConfig + }; + } + } + return { success: true, value }; + }; + + return { + isOptional: config.isOptional, + isNullable: config.isNullable, + optional, + nullable, + check, + forge, + ...config.methods?.(addToForge) + } as BaseForgeType; +}; diff --git a/lib/vProposal/boolean.ts b/lib/vProposal/boolean.ts new file mode 100644 index 0000000..c1c70cc --- /dev/null +++ b/lib/vProposal/boolean.ts @@ -0,0 +1,14 @@ +import { baseForgeType } from './baseForgeType'; + +export const boolean = () => { + return baseForgeType({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => typeof value === 'boolean', + caller: 'boolean' + } + ] + }); +}; diff --git a/lib/vProposal/index.ts b/lib/vProposal/index.ts new file mode 100644 index 0000000..287bf7e --- /dev/null +++ b/lib/vProposal/index.ts @@ -0,0 +1,10 @@ +// import { array } from './array'; +// import { blueprint } from './blueprint'; +import { boolean } from './boolean'; +import { number } from './number'; +import { string } from './string'; + +const f = { boolean, number, string }; + +export { boolean, number, string }; +export default f; diff --git a/lib/vProposal/number.ts b/lib/vProposal/number.ts new file mode 100644 index 0000000..623523a --- /dev/null +++ b/lib/vProposal/number.ts @@ -0,0 +1,67 @@ +import { baseForgeType, ForgeMethodConfig } from './baseForgeType'; + +export const number = () => { + return baseForgeType< + number, + { + min: (min: number) => unknown; + max: (max: number) => unknown; + positive: () => unknown; + negative: () => unknown; + integer: () => unknown; + } + >({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => typeof value === 'number', + caller: 'number' + } + ], + methods: (addToForge) => ({ + min: (min: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value >= min; + }, + caller: 'min', + ...config + }), + max: (max: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value <= max; + }, + caller: 'max', + ...config + }), + positive: (config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value > 0; + }, + caller: 'positive', + ...config + }), + negative: (config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value < 0; + }, + caller: 'negative', + ...config + }), + integer: (config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return ( + typeof value === 'number' && Number.isInteger(value) + ); + }, + caller: 'integer', + ...config + }) + }) + }); +}; diff --git a/lib/vProposal/string.ts b/lib/vProposal/string.ts new file mode 100644 index 0000000..0a07bcd --- /dev/null +++ b/lib/vProposal/string.ts @@ -0,0 +1,46 @@ +import { baseForgeType, ForgeMethodConfig } from './baseForgeType'; + +export const string = () => + baseForgeType< + string, + { + regExp: (pattern: RegExp) => unknown; + minLength: (min: number) => unknown; + maxLength: (max: number) => unknown; + } + >({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => typeof value === 'string', + caller: 'string' + } + ], + methods: (addToForge) => ({ + regExp: (regExp: RegExp, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'string' && regExp.test(value); + }, + caller: 'regExp', + ...config + }), + minLength: (min: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'string' && value.length >= min; + }, + caller: 'minLength', + ...config + }), + maxLength: (max: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'string' && value.length <= max; + }, + caller: 'maxLength', + ...config + }) + }) + }); diff --git a/proposal.ts b/proposal.ts deleted file mode 100644 index 31a105c..0000000 --- a/proposal.ts +++ /dev/null @@ -1,120 +0,0 @@ -type ForgeMethod = { - fn: (value: unknown) => boolean | Promise; - caller?: string; -}; - -type VerificationResult = - | { success: true; value: T } - | { success: false; errorMessage: string }; - -function baseForgeType(config: { - isOptional: boolean; - isNullable: boolean; - queue: ForgeMethod[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - methods: Record ForgeMethod>; -}) { - const addToForge = (method: ForgeMethod) => { - return baseForgeType({ - ...config, - queue: [...config.queue, method] - }); - }; - - const optional = () => { - return baseForgeType({ - ...config, - isOptional: true - }); - }; - - const nullable = () => { - return baseForgeType({ - ...config, - isNullable: true - }); - }; - - const check = ( - fn: (value: T) => boolean | Promise, - config?: { errorMessage?: string; path?: string[]; loose?: boolean } - ) => { - return addToForge({ fn, caller: 'check', ...config }); - }; - - const forge = (value: T): VerificationResult => { - for (const method of config.queue) { - const result = method.fn(value); - if (result instanceof Promise) { - return { - success: false, - errorMessage: 'Async validation not supported' - }; - } - if (!result) { - return { - success: false, - errorMessage: `Validation failed in ${method.caller}` - }; - } - } - return { success: true, value }; - }; - - return { - _addToForge: addToForge, - isOptional: config.isOptional, - isNullable: config.isNullable, - optional, - nullable, - check, - forge, - ...config.methods - }; -} - -const boolean = () => { - return baseForgeType({ - isOptional: false, - isNullable: false, - queue: [ - { - fn: (value: unknown) => typeof value === 'boolean', - caller: 'boolean' - } - ], - methods: {} - }); -}; - -const string = () => - baseForgeType({ - isOptional: false, - isNullable: false, - queue: [ - { - fn: (value: unknown) => typeof value === 'string', - caller: 'string' - } - ], - methods: { - regExp: function (regExp: RegExp) { - return this._addToForge({ - fn: (value: T): boolean | Promise => { - return typeof value === 'string' && regExp.test(value); - }, - caller: 'regExp' - }); - } - } - }); - -boolean().forge(true); // ? -string() - .nullable() - .optional() - .check((value) => {}) - .nullable() - .optional() - .check((value) => {}) - .regExp(/abc/); // ? From d75ac12c37ba02eac2e4544b1010d9184d9e39ff Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Tue, 15 Jul 2025 19:18:16 -0600 Subject: [PATCH 03/11] Refactor baseForgeType and related methods: extract ForgeMethodConfig and related types into a new types.ts file; update number and string methods to accept optional config parameter --- lib/vProposal/baseForgeType.ts | 26 ++------------------------ lib/vProposal/number.ts | 13 +++++++------ lib/vProposal/string.ts | 9 +++++---- lib/vProposal/types.ts | 23 +++++++++++++++++++++++ 4 files changed, 37 insertions(+), 34 deletions(-) create mode 100644 lib/vProposal/types.ts diff --git a/lib/vProposal/baseForgeType.ts b/lib/vProposal/baseForgeType.ts index 42df18c..0fb5eb0 100644 --- a/lib/vProposal/baseForgeType.ts +++ b/lib/vProposal/baseForgeType.ts @@ -1,28 +1,6 @@ -type UnknownObject = Record; - -export type ForgeMethodConfig = { - errorMessage?: string; - path?: string[]; - loose?: boolean; -}; - -export type ForgeMethod = ForgeMethodConfig & { - fn: (value: unknown) => boolean | Promise; - caller: string; -}; - -export type UnsuccessfulVerificationResult = { - success: false; - caller: string; - errorMessage?: string; - path?: string[]; - issues?: UnsuccessfulVerificationResult[]; - arrayIndex?: number; -}; +import { ForgeMethod, ForgeMethodConfig, VerificationResult } from './types'; -export type VerificationResult = - | { success: true; value: T } - | UnsuccessfulVerificationResult; +type UnknownObject = Record; type BaseForgeTypeConfig = { isOptional: boolean; diff --git a/lib/vProposal/number.ts b/lib/vProposal/number.ts index 623523a..c10ea2d 100644 --- a/lib/vProposal/number.ts +++ b/lib/vProposal/number.ts @@ -1,14 +1,15 @@ -import { baseForgeType, ForgeMethodConfig } from './baseForgeType'; +import { baseForgeType } from './baseForgeType'; +import { type ForgeMethodConfig } from './types'; export const number = () => { return baseForgeType< number, { - min: (min: number) => unknown; - max: (max: number) => unknown; - positive: () => unknown; - negative: () => unknown; - integer: () => unknown; + min: (min: number, config?: ForgeMethodConfig) => unknown; + max: (max: number, config?: ForgeMethodConfig) => unknown; + positive: (config?: ForgeMethodConfig) => unknown; + negative: (config?: ForgeMethodConfig) => unknown; + integer: (config?: ForgeMethodConfig) => unknown; } >({ isOptional: false, diff --git a/lib/vProposal/string.ts b/lib/vProposal/string.ts index 0a07bcd..fb4908f 100644 --- a/lib/vProposal/string.ts +++ b/lib/vProposal/string.ts @@ -1,12 +1,13 @@ -import { baseForgeType, ForgeMethodConfig } from './baseForgeType'; +import { baseForgeType } from './baseForgeType'; +import { type ForgeMethodConfig } from './types'; export const string = () => baseForgeType< string, { - regExp: (pattern: RegExp) => unknown; - minLength: (min: number) => unknown; - maxLength: (max: number) => unknown; + regExp: (pattern: RegExp, config?: ForgeMethodConfig) => unknown; + minLength: (min: number, config?: ForgeMethodConfig) => unknown; + maxLength: (max: number, config?: ForgeMethodConfig) => unknown; } >({ isOptional: false, diff --git a/lib/vProposal/types.ts b/lib/vProposal/types.ts new file mode 100644 index 0000000..43b5c81 --- /dev/null +++ b/lib/vProposal/types.ts @@ -0,0 +1,23 @@ +export type ForgeMethodConfig = { + errorMessage?: string; + path?: string[]; + loose?: boolean; +}; + +export type ForgeMethod = ForgeMethodConfig & { + fn: (value: unknown) => boolean | Promise; + caller: string; +}; + +export type UnsuccessfulVerificationResult = { + success: false; + caller: string; + errorMessage?: string; + path?: string[]; + issues?: UnsuccessfulVerificationResult[]; + arrayIndex?: number; +}; + +export type VerificationResult = + | { success: true; value: T } + | UnsuccessfulVerificationResult; From c80ae8658281fa1c149bece56e4353f6c0571057 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Wed, 16 Jul 2025 19:04:02 -0600 Subject: [PATCH 04/11] Implement array validation type and related methods; enhance baseForgeType for improved type handling and validation flexibility --- lib/vProposal/array.ts | 95 ++++++++++++++++++++++++++++ lib/vProposal/baseForgeType.ts | 108 +++++++++++++------------------- lib/vProposal/boolean.ts | 11 +++- lib/vProposal/forgeFunctions.ts | 65 +++++++++++++++++++ lib/vProposal/index.ts | 6 +- lib/vProposal/number.ts | 36 +++++++---- lib/vProposal/string.ts | 30 +++++---- lib/vProposal/types.ts | 99 +++++++++++++++++++++++++++++ 8 files changed, 357 insertions(+), 93 deletions(-) create mode 100644 lib/vProposal/array.ts create mode 100644 lib/vProposal/forgeFunctions.ts diff --git a/lib/vProposal/array.ts b/lib/vProposal/array.ts new file mode 100644 index 0000000..ef30809 --- /dev/null +++ b/lib/vProposal/array.ts @@ -0,0 +1,95 @@ +import { baseForgeType } from './baseForgeType'; +import { verifyChainAsync } from './forgeFunctions'; +import type { + BaseForgeObject, + BaseForgeType, + ForgeMethodConfig, + UnsuccessfulVerificationResult +} from './types'; + +export const array = ( + model: T +): BaseForgeType< + { + minLength: (min: number, config?: ForgeMethodConfig) => unknown; + maxLength: (max: number, config?: ForgeMethodConfig) => unknown; + }, + { + type: Array; + optional: false; + nullable: false; + minLength: false; + maxLength: false; + } +> => { + return baseForgeType({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => Array.isArray(value), + caller: 'array', + errorCode: 'value_error' + } + ], + methods: (addToForge) => ({ + minLength: (min: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return Array.isArray(value) && value.length >= min; + }, + caller: 'minLength', + ...config + }), + maxLength: (max: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return Array.isArray(value) && value.length <= max; + }, + caller: 'maxLength', + ...config + }) + }), + forge: + (config) => + async (values: T) => { + if ( + (config.isOptional && values === undefined) || + (config.isNullable && values === null) + ) { + return { success: true, value: values }; + } + + let issues: UnsuccessfulVerificationResult[] = []; + + // Verify all elements in the array + if (Array.isArray(values)) { + await Promise.all( + values.map(async (value, idx) => { + const res = await model.forge(value); + if (!res.success) { + // Push the issue with the index of the array + issues.push({ ...res, arrayIndex: idx }); + } + }) + ); + } + + // Verify validations at array level + const res = await verifyChainAsync(values, config); + + if (!res.success) { + issues = issues.concat(res.issues || []); + } + if (issues.length > 0) { + return { + success: false, + caller: '', + errorCode: 'validation_error', + issues + }; + } + return { success: true, value: values }; + } + }); +}; diff --git a/lib/vProposal/baseForgeType.ts b/lib/vProposal/baseForgeType.ts index 0fb5eb0..78ecdb8 100644 --- a/lib/vProposal/baseForgeType.ts +++ b/lib/vProposal/baseForgeType.ts @@ -1,83 +1,59 @@ -import { ForgeMethod, ForgeMethodConfig, VerificationResult } from './types'; - -type UnknownObject = Record; - -type BaseForgeTypeConfig = { - isOptional: boolean; - isNullable: boolean; - queue: ForgeMethod[]; - methods?: (addToForge: (method: ForgeMethod) => unknown) => Methods; -}; - -type BaseForgeType = { - isOptional: boolean; - isNullable: boolean; - optional: () => BaseForgeType; - nullable: () => BaseForgeType; - check: ( - fn: (value: T) => boolean | Promise, - config?: { errorMessage?: string; path?: string[]; loose?: boolean } - ) => BaseForgeType; - forge: (value: T) => VerificationResult; -} & { - [K in keyof Methods]: Methods[K] extends (...args: infer FArgs) => unknown - ? (...args: FArgs) => BaseForgeType - : Methods[K]; -}; +import { verifyChainAsync } from './forgeFunctions'; +import type { + BaseForgeType, + BaseForgeTypeConfig, + ForgeMethod, + ForgeMethodConfig, + ForgeState, + UnknownObject, + VerificationResult +} from './types'; export const baseForgeType = < - FType, - Methods extends UnknownObject = UnknownObject + Methods extends UnknownObject, + State extends ForgeState >( config: BaseForgeTypeConfig ) => { - const addToForge = (method: ForgeMethod): BaseForgeType => { - return baseForgeType({ + const addToForge = (method: ForgeMethod) => { + return baseForgeType({ ...config, queue: [...config.queue, method] }); }; - const optional = (): BaseForgeType => { - return baseForgeType({ - ...config, - isOptional: true - }); + const optional = () => { + return baseForgeType< + Methods, + { + type: State['type'] | undefined; + optional: true; + nullable: State['nullable']; + } & { [K in keyof Methods]: State[K] } + >({ ...config, isOptional: true }); }; - const nullable = (): BaseForgeType => { - return baseForgeType({ - ...config, - isNullable: true - }); + const nullable = () => { + return baseForgeType< + Methods, + { + type: State['type'] | null; + optional: State['optional']; + nullable: true; + } & { [K in keyof Methods]: State[K] } + >({ ...config, isNullable: true }); }; const check = ( - fn: (value: T) => boolean | Promise, + fn: (value: T) => boolean | Promise, config?: ForgeMethodConfig - ): BaseForgeType => { + ): BaseForgeType => { return addToForge({ fn, caller: 'check', ...config }); }; - const forge = (value: T): VerificationResult => { - for (const method of config.queue) { - const { fn, ...methodConfig } = method; - const result = fn(value); - if (result instanceof Promise) { - return { - success: false, - ...methodConfig - }; - } - if (!result) { - return { - success: false, - ...methodConfig - }; - } - } - return { success: true, value }; - }; + const forge = async ( + value: T + ): Promise> => verifyChainAsync(value, config); return { isOptional: config.isOptional, @@ -85,7 +61,13 @@ export const baseForgeType = < optional, nullable, check, - forge, + forge: config.forge + ? config.forge({ + isOptional: config.isOptional, + isNullable: config.isNullable, + queue: config.queue + }) + : forge, ...config.methods?.(addToForge) - } as BaseForgeType; + } as BaseForgeType; }; diff --git a/lib/vProposal/boolean.ts b/lib/vProposal/boolean.ts index c1c70cc..01f8e13 100644 --- a/lib/vProposal/boolean.ts +++ b/lib/vProposal/boolean.ts @@ -1,13 +1,18 @@ import { baseForgeType } from './baseForgeType'; +import type { BaseForgeType } from './types'; -export const boolean = () => { - return baseForgeType({ +export const boolean = (): BaseForgeType< + { _: unknown }, + { type: boolean; optional: false; nullable: false; _: true } +> => { + return baseForgeType({ isOptional: false, isNullable: false, queue: [ { fn: (value: unknown) => typeof value === 'boolean', - caller: 'boolean' + caller: 'boolean', + errorCode: 'value_error' } ] }); diff --git a/lib/vProposal/forgeFunctions.ts b/lib/vProposal/forgeFunctions.ts new file mode 100644 index 0000000..adab9fc --- /dev/null +++ b/lib/vProposal/forgeFunctions.ts @@ -0,0 +1,65 @@ +import type { + BaseForgeConfig, + UnsuccessfulVerificationResult, + VerificationResult +} from './types'; + +/** + * Helper function to verify a value against a chain of validation functions. + * @param data - Data and configuration for the verification. + * @param options - Options for the verification process. + * @returns A Promise that resolves to a VerificationResult indicating the outcome of the verification. + */ +export const verifyChainAsync = async ( + value: T, + config: BaseForgeConfig +): Promise> => { + let lastMethodName = ''; + const issues: UnsuccessfulVerificationResult[] = []; + + try { + for (const method of config.queue) { + lastMethodName = method.caller; + const forgeResult = await method.fn(value); + + if (typeof forgeResult === 'boolean') { + if ( + (config.isOptional && value === undefined) || + (config.isNullable && value === null) + ) { + break; + } + if (!forgeResult) { + issues.push({ + success: false, + caller: method.caller, + errorCode: method.errorCode || 'validation_error', + errorMessage: method.errorMessage, + path: method.path + }); + if (method.loose) { + continue; + } else { + break; + } + } + } + } + } catch (error) { + issues.push({ + success: false, + caller: lastMethodName, + errorCode: 'unexpected_error', + errorMessage: error instanceof Error ? error.message : String(error) + }); + } + + return issues.length === 0 + ? { success: true, value } + : { + success: false, + caller: '', + errorCode: 'validation_error', + issues + }; +}; diff --git a/lib/vProposal/index.ts b/lib/vProposal/index.ts index 287bf7e..58a6117 100644 --- a/lib/vProposal/index.ts +++ b/lib/vProposal/index.ts @@ -1,10 +1,10 @@ -// import { array } from './array'; +import { array } from './array'; // import { blueprint } from './blueprint'; import { boolean } from './boolean'; import { number } from './number'; import { string } from './string'; -const f = { boolean, number, string }; +const f = { array, boolean, number, string }; -export { boolean, number, string }; +export { array, boolean, number, string }; export default f; diff --git a/lib/vProposal/number.ts b/lib/vProposal/number.ts index c10ea2d..d58328a 100644 --- a/lib/vProposal/number.ts +++ b/lib/vProposal/number.ts @@ -1,23 +1,33 @@ import { baseForgeType } from './baseForgeType'; -import { type ForgeMethodConfig } from './types'; +import type { BaseForgeType, ForgeMethodConfig } from './types'; -export const number = () => { - return baseForgeType< - number, - { - min: (min: number, config?: ForgeMethodConfig) => unknown; - max: (max: number, config?: ForgeMethodConfig) => unknown; - positive: (config?: ForgeMethodConfig) => unknown; - negative: (config?: ForgeMethodConfig) => unknown; - integer: (config?: ForgeMethodConfig) => unknown; - } - >({ +export const number = (): BaseForgeType< + { + min: (min: number, config?: ForgeMethodConfig) => unknown; + max: (max: number, config?: ForgeMethodConfig) => unknown; + positive: (config?: ForgeMethodConfig) => { negative: false }; + negative: (config?: ForgeMethodConfig) => { positive: false }; + integer: (config?: ForgeMethodConfig) => unknown; + }, + { + type: number; + optional: false; + nullable: false; + min: false; + max: false; + positive: false; + negative: false; + integer: false; + } +> => { + return baseForgeType({ isOptional: false, isNullable: false, queue: [ { fn: (value: unknown) => typeof value === 'number', - caller: 'number' + caller: 'number', + errorCode: 'value_error' } ], methods: (addToForge) => ({ diff --git a/lib/vProposal/string.ts b/lib/vProposal/string.ts index fb4908f..10d3ade 100644 --- a/lib/vProposal/string.ts +++ b/lib/vProposal/string.ts @@ -1,21 +1,29 @@ import { baseForgeType } from './baseForgeType'; -import { type ForgeMethodConfig } from './types'; +import type { BaseForgeType, ForgeMethodConfig } from './types'; -export const string = () => - baseForgeType< - string, - { - regExp: (pattern: RegExp, config?: ForgeMethodConfig) => unknown; - minLength: (min: number, config?: ForgeMethodConfig) => unknown; - maxLength: (max: number, config?: ForgeMethodConfig) => unknown; - } - >({ +export const string = (): BaseForgeType< + { + regExp: (pattern: RegExp, config?: ForgeMethodConfig) => unknown; + minLength: (min: number, config?: ForgeMethodConfig) => unknown; + maxLength: (max: number, config?: ForgeMethodConfig) => unknown; + }, + { + type: string; + optional: false; + nullable: false; + minLength: false; + maxLength: false; + regExp: null; + } +> => + baseForgeType({ isOptional: false, isNullable: false, queue: [ { fn: (value: unknown) => typeof value === 'string', - caller: 'string' + caller: 'string', + errorCode: 'value_error' } ], methods: (addToForge) => ({ diff --git a/lib/vProposal/types.ts b/lib/vProposal/types.ts index 43b5c81..493333c 100644 --- a/lib/vProposal/types.ts +++ b/lib/vProposal/types.ts @@ -1,3 +1,9 @@ +export type ErrorCode = + | 'value_error' + | 'validation_error' + | 'async_method_error' + | 'unexpected_error'; + export type ForgeMethodConfig = { errorMessage?: string; path?: string[]; @@ -7,11 +13,13 @@ export type ForgeMethodConfig = { export type ForgeMethod = ForgeMethodConfig & { fn: (value: unknown) => boolean | Promise; caller: string; + errorCode?: ErrorCode; }; export type UnsuccessfulVerificationResult = { success: false; caller: string; + errorCode: ErrorCode; errorMessage?: string; path?: string[]; issues?: UnsuccessfulVerificationResult[]; @@ -21,3 +29,94 @@ export type UnsuccessfulVerificationResult = { export type VerificationResult = | { success: true; value: T } | UnsuccessfulVerificationResult; + +export type UnknownObject = Record; + +export type ForgeState = { + type: unknown; + optional: boolean; + nullable: boolean; +} & { + [K in keyof Methods]: boolean | null; +}; + +export type BaseForgeConfig = { + isOptional: boolean; + isNullable: boolean; + queue: ForgeMethod[]; +}; + +export type BaseForgeTypeConfig = + BaseForgeConfig & { + methods?: (addToForge: (method: ForgeMethod) => unknown) => Methods; + forge?: ( + config: BaseForgeConfig + ) => (value: T) => Promise>; + }; + +export type BaseForgeType< + Methods extends UnknownObject, + State extends ForgeState +> = { + _type: State['type']; + isOptional: boolean; + isNullable: boolean; + optional: State['optional'] extends true + ? never + : () => BaseForgeType< + Methods, + { + type: State['type'] | undefined; + optional: true; + nullable: State['nullable']; + } & { [K in keyof Methods]: State[K] } + >; + + nullable: State['nullable'] extends true + ? never + : () => BaseForgeType< + Methods, + { + type: State['type'] | null; + optional: State['optional']; + nullable: true; + } & { [K in keyof Methods]: State[K] } + >; + check: ( + fn: (value: T) => boolean | Promise, + config?: ForgeMethodConfig + ) => BaseForgeType; + forge: (value: T) => Promise>; +} & { + [MethodName in keyof Methods]: Methods[MethodName] extends ( + ...args: infer FArgs + ) => unknown + ? State[MethodName] extends null + ? (...args: FArgs) => BaseForgeType + : State[MethodName] extends true + ? never + : (...args: FArgs) => BaseForgeType< + Methods, + { + type: State['type']; + optional: State['optional']; + nullable: State['nullable']; + } & { + [StateItem in keyof Methods]: StateItem extends MethodName + ? true + : ReturnType extends { + [K in StateItem]: false; + } + ? true + : State[StateItem]; + } + > + : never; +}; + +export type BaseForgeObject = { + _type: unknown; + isOptional: boolean; + isNullable: boolean; + forge: (value: T) => Promise>; +}; From 983766db50592d57e91eda35c8467d3b18d69078 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Wed, 16 Jul 2025 19:19:53 -0600 Subject: [PATCH 05/11] Refactor number validation methods: replace min/max with lt/lte and gt/gte for improved clarity and consistency in type handling --- lib/vProposal/number.ts | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/vProposal/number.ts b/lib/vProposal/number.ts index d58328a..275d6e6 100644 --- a/lib/vProposal/number.ts +++ b/lib/vProposal/number.ts @@ -3,8 +3,10 @@ import type { BaseForgeType, ForgeMethodConfig } from './types'; export const number = (): BaseForgeType< { - min: (min: number, config?: ForgeMethodConfig) => unknown; - max: (max: number, config?: ForgeMethodConfig) => unknown; + lt: (max: number, config?: ForgeMethodConfig) => { lte: false }; + lte: (min: number, config?: ForgeMethodConfig) => { lt: false }; + gt: (min: number, config?: ForgeMethodConfig) => { gte: false }; + gte: (max: number, config?: ForgeMethodConfig) => { gt: false }; positive: (config?: ForgeMethodConfig) => { negative: false }; negative: (config?: ForgeMethodConfig) => { positive: false }; integer: (config?: ForgeMethodConfig) => unknown; @@ -13,8 +15,10 @@ export const number = (): BaseForgeType< type: number; optional: false; nullable: false; - min: false; - max: false; + lt: false; + lte: false; + gt: false; + gte: false; positive: false; negative: false; integer: false; @@ -31,20 +35,36 @@ export const number = (): BaseForgeType< } ], methods: (addToForge) => ({ - min: (min: number, config?: ForgeMethodConfig) => + lt: (val: number, config?: ForgeMethodConfig) => addToForge({ fn: (value: unknown) => { - return typeof value === 'number' && value >= min; + return typeof value === 'number' && value < val; }, - caller: 'min', + caller: 'lt', ...config }), - max: (max: number, config?: ForgeMethodConfig) => + lte: (min: number, config?: ForgeMethodConfig) => addToForge({ fn: (value: unknown) => { - return typeof value === 'number' && value <= max; + return typeof value === 'number' && value <= min; }, - caller: 'max', + caller: 'lte', + ...config + }), + gt: (val: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value > val; + }, + caller: 'gt', + ...config + }), + gte: (max: number, config?: ForgeMethodConfig) => + addToForge({ + fn: (value: unknown) => { + return typeof value === 'number' && value >= max; + }, + caller: 'gte', ...config }), positive: (config?: ForgeMethodConfig) => From f2d702ed9544dfbea39f2d044e83e6bb05d09922 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Wed, 16 Jul 2025 19:28:07 -0600 Subject: [PATCH 06/11] Enhance BaseForgeType: clarify method state handling with comments on null and true states for improved readability --- lib/vProposal/types.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/vProposal/types.ts b/lib/vProposal/types.ts index 493333c..e6adcc4 100644 --- a/lib/vProposal/types.ts +++ b/lib/vProposal/types.ts @@ -91,9 +91,11 @@ export type BaseForgeType< [MethodName in keyof Methods]: Methods[MethodName] extends ( ...args: infer FArgs ) => unknown - ? State[MethodName] extends null + ? // If state is null function can be used multiple times + State[MethodName] extends null ? (...args: FArgs) => BaseForgeType - : State[MethodName] extends true + : // If state is not null, function can only be used once + State[MethodName] extends true ? never : (...args: FArgs) => BaseForgeType< Methods, @@ -102,6 +104,8 @@ export type BaseForgeType< optional: State['optional']; nullable: State['nullable']; } & { + // Update state to true for the method that was called + // - If it has dependencies, they are set to true as well [StateItem in keyof Methods]: StateItem extends MethodName ? true : ReturnType extends { From 6dcdfae1583502cc54a2cacd8b3cd2ac5f91ed60 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Thu, 17 Jul 2025 18:26:49 -0600 Subject: [PATCH 07/11] Refactor: - Deleted boolean types and validation logic from lib/boolean. - Removed number types and validation logic from lib/number. - Consolidated verification functions into a new types module. - Introduced new array, boolean, number, object, string types with updated validation methods. - Updated index files to reflect new structure and imports. - Enhanced verification logic for async operations and error handling. --- lib/array/array.types.ts | 91 ---------- lib/array/index.ts | 221 ----------------------- lib/{vProposal => }/baseForgeType.ts | 8 +- lib/blueprint/blueprint.types.ts | 67 ------- lib/blueprint/index.ts | 214 ---------------------- lib/boolean/boolean.types.ts | 50 ----- lib/boolean/index.ts | 80 -------- lib/forgeFunctions.ts | 93 ++-------- lib/forgeTypes.ts | 93 ---------- lib/{vProposal => forgeTypes}/array.ts | 8 +- lib/{vProposal => forgeTypes}/boolean.ts | 4 +- lib/{vProposal => forgeTypes}/number.ts | 4 +- lib/forgeTypes/object.ts | 112 ++++++++++++ lib/{vProposal => forgeTypes}/string.ts | 4 +- lib/index.ts | 15 +- lib/number/index.ts | 128 ------------- lib/number/number.types.ts | 85 --------- lib/string/index.ts | 154 ---------------- lib/string/string.types.ts | 96 ---------- lib/{vProposal => }/types.ts | 4 +- lib/vProposal/forgeFunctions.ts | 65 ------- lib/vProposal/index.ts | 10 - 22 files changed, 154 insertions(+), 1452 deletions(-) delete mode 100644 lib/array/array.types.ts delete mode 100644 lib/array/index.ts rename lib/{vProposal => }/baseForgeType.ts (89%) delete mode 100644 lib/blueprint/blueprint.types.ts delete mode 100644 lib/blueprint/index.ts delete mode 100644 lib/boolean/boolean.types.ts delete mode 100644 lib/boolean/index.ts delete mode 100644 lib/forgeTypes.ts rename lib/{vProposal => forgeTypes}/array.ts (94%) rename lib/{vProposal => forgeTypes}/boolean.ts (81%) rename lib/{vProposal => forgeTypes}/number.ts (96%) create mode 100644 lib/forgeTypes/object.ts rename lib/{vProposal => forgeTypes}/string.ts (93%) delete mode 100644 lib/number/index.ts delete mode 100644 lib/number/number.types.ts delete mode 100644 lib/string/index.ts delete mode 100644 lib/string/string.types.ts rename lib/{vProposal => }/types.ts (97%) delete mode 100644 lib/vProposal/forgeFunctions.ts delete mode 100644 lib/vProposal/index.ts diff --git a/lib/array/array.types.ts b/lib/array/array.types.ts deleted file mode 100644 index e150717..0000000 --- a/lib/array/array.types.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { - BaseForgeMethods, - BaseForgeMethodsConfig, - BaseForgeOptions, - CheckConfig, - UpdateForgeConfig -} from '../forgeTypes'; - -type ForgeOptions = { hasMinLength: boolean; hasMaxLength: boolean }; - -export type ArrayForgeOptions = BaseForgeOptions & ForgeOptions; - -type ForgeMethodsConfig = BaseForgeMethodsConfig & ForgeOptions; - -type ForgeMethods = BaseForgeMethods< - Config['type'] -> & { - /** - * Checks if the value passes the specified validation function. - * @param fn - The validation function to apply. - * @param config - The configuration for the check, including error message and options. - * @returns A new ForgeMethods instance with the validation applied. - */ - check: ( - fn: (value: Config['type']) => boolean | Promise, - config?: CheckConfig - ) => ForgeMethods; -} & (Config['isOptional'] extends true - ? object - : { - /** - * Applies an optional flag to the value forged by this method. - * @returns A new ForgeMethods instance with the optional flag set. - */ - optional: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | undefined; isOptional: true } - > - >; - }) & - (Config['isNullable'] extends true - ? object - : { - /** - * Applies a nullable flag to the value forged by this method. - * @returns A new ForgeMethods instance with the nullable flag set. - */ - nullable: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | null; isNullable: true } - > - >; - }) & - (Config['hasMinLength'] extends true - ? object - : { - /** - * Checks if the value has a minimum length. - * @param minLength - The minimum length to enforce. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the validation applied. - */ - minLength: ( - minLength: number, - errorMessage?: string - ) => ForgeMethods< - UpdateForgeConfig - >; - }) & - (Config['hasMaxLength'] extends true - ? object - : { - /** - * Checks if the value has a maximum length. - * @param maxLength - The maximum length to enforce. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the validation applied. - */ - maxLength: ( - maxLength: number, - errorMessage?: string - ) => ForgeMethods< - UpdateForgeConfig - >; - }); - -export type ArrayMethods = ForgeMethods< - ForgeMethodsConfig ->; diff --git a/lib/array/index.ts b/lib/array/index.ts deleted file mode 100644 index 45ee4ce..0000000 --- a/lib/array/index.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { verifyChain, verifyChainAsync } from '../forgeFunctions'; - -import type { - BaseForgeMethods, - CheckConfig, - ForgeMethod, - UnsuccessfulVerificationResult, - VerificationResult -} from '../forgeTypes'; -import { ArrayForgeOptions, ArrayMethods } from './array.types'; - -/** - * Creates an array validation chain. - * @param model - The model to use for validation. - * @param errorMessage - Optional error message for the array validation. - * @returns A new ArrayMethods instance for the specified model. - */ -export const array = < - TForge extends BaseForgeMethods = BaseForgeMethods, - Methods = ArrayMethods ->( - model: TForge, - errorMessage?: string -) => { - const forgeType = >(value: T): boolean => { - if (!Array.isArray(value)) { - return false; - } - return true; - }; - - const createMethods = ( - methods: ForgeMethod[], - forgeOptions: ArrayForgeOptions - ) => { - const forge = (values: T): VerificationResult => { - if ( - (forgeOptions.optional && values === undefined) || - (forgeOptions.nullable && values === null) - ) { - return { success: true, value: values }; - } - - let issues: UnsuccessfulVerificationResult[] = []; - - // Verify all elements in the array - if (Array.isArray(values)) { - values.forEach((value, idx) => { - const res = model.forge(value); - if (!res.success) { - // Push the issue with the index of the array - issues.push({ ...res, arrayIndex: idx }); - } - }); - } - - // Verify validations at array level - const res = verifyChain({ value: values, methods }, forgeOptions); - - if (!res.success) { - issues = issues.concat(res.issues || []); - } - if (issues.length > 0) { - return { - success: false, - errorCode: 'validation_error', - caller: '', - issues - }; - } - return { success: true, value: values }; - }; - - const forgeAsync = async ( - values: T - ): Promise> => { - if ( - (forgeOptions.optional && values === undefined) || - (forgeOptions.nullable && values === null) - ) { - return { success: true, value: values }; - } - - let issues: UnsuccessfulVerificationResult[] = []; - - // Verify all elements in the array - if (Array.isArray(values)) { - await Promise.all( - values.map(async (value, idx) => { - const res = await model.forgeAsync(value); - if (!res.success) { - // Push the issue with the index of the array - issues.push({ ...res, arrayIndex: idx }); - } - }) - ); - } - - // Verify validations at array level - const res = await verifyChainAsync( - { value: values, methods }, - forgeOptions - ); - - if (!res.success) { - issues = issues.concat(res.issues || []); - } - if (issues.length > 0) { - return { - success: false, - errorCode: 'validation_error', - caller: '', - issues - }; - } - return { success: true, value: values }; - }; - - const optional = () => { - return createMethods(methods, { ...forgeOptions, optional: true }); - }; - - const nullable = () => { - return createMethods(methods, { ...forgeOptions, nullable: true }); - }; - - const check = ( - fn: (value: T) => boolean, - config?: CheckConfig - ) => { - return createMethods( - [...methods, { fn, caller: 'check', ...config }], - forgeOptions - ); - }; - - const minLength = (minLength: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (Array.isArray(value)) { - return value.length >= minLength; - } - return false; - }, - caller: 'minLength', - errorMessage - } - ], - { - ...forgeOptions, - hasMinLength: true - } - ); - }; - - const maxLength = (maxLength: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (Array.isArray(value)) { - return value.length <= maxLength; - } - return false; - }, - caller: 'maxLength', - errorMessage - } - ], - { - ...forgeOptions, - hasMaxLength: true - } - ); - }; - - const newMethods: Record = { - forge, - forgeAsync, - check, - isOptional: forgeOptions.optional, - isNullable: forgeOptions.nullable, - model - }; - if (!forgeOptions.optional) { - newMethods.optional = optional; - } - if (!forgeOptions.nullable) { - newMethods.nullable = nullable; - } - if (!forgeOptions.hasMinLength) { - newMethods.minLength = minLength; - } - if (!forgeOptions.hasMaxLength) { - newMethods.maxLength = maxLength; - } - - return newMethods as Methods; - }; - - return createMethods( - [ - { - fn: forgeType, - errorCode: 'value_error', - caller: 'array', - errorMessage - } - ], - { - optional: false, - nullable: false, - hasMinLength: false, - hasMaxLength: false - } - ); -}; diff --git a/lib/vProposal/baseForgeType.ts b/lib/baseForgeType.ts similarity index 89% rename from lib/vProposal/baseForgeType.ts rename to lib/baseForgeType.ts index 78ecdb8..9a85dbf 100644 --- a/lib/vProposal/baseForgeType.ts +++ b/lib/baseForgeType.ts @@ -45,10 +45,14 @@ export const baseForgeType = < }; const check = ( - fn: (value: T) => boolean | Promise, + fn: (value: State['type']) => boolean | Promise, config?: ForgeMethodConfig ): BaseForgeType => { - return addToForge({ fn, caller: 'check', ...config }); + return addToForge({ + fn: (value: unknown) => fn(value as State['type']), + caller: 'check', + ...config + }); }; const forge = async ( diff --git a/lib/blueprint/blueprint.types.ts b/lib/blueprint/blueprint.types.ts deleted file mode 100644 index 91e26d9..0000000 --- a/lib/blueprint/blueprint.types.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { - BaseForgeMethods, - BaseForgeMethodsConfig, - CheckConfig, - UpdateForgeConfig -} from '../forgeTypes'; - -type BlueprintValues = { - [K in keyof T]: T[K] extends { model: infer M } - ? BlueprintValues - : T[K] extends { _type: infer FType } - ? FType - : unknown; -}; - -type ForgeMethods = BaseForgeMethods< - BlueprintValues -> & { - /** - * The model of the blueprint, which is the type of the value forged by this method. - * Use a model to create new blueprints based on this one. - */ - model: Config['type']; - /** - * Checks if the value passes the custom validation function. - * @param fn - The custom validation function to apply. - * @param config - The configuration for the check, including error message and options. - * @returns A new ForgeMethods instance with the validation applied. - */ - check: ( - fn: ( - value: BlueprintValues - ) => boolean | Promise, - config?: CheckConfig - ) => ForgeMethods; -} & (Config['isOptional'] extends true - ? object - : { - /** - * Applies an optional flag to the value forged by this method. - * @returns A new ForgeMethods instance with the optional flag set. - */ - optional: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | undefined; isOptional: true } - > - >; - }) & - (Config['isNullable'] extends true - ? object - : { - /** - * Applies a nullable flag to the value forged by this method. - * @returns A new ForgeMethods instance with the nullable flag set. - */ - nullable: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | null; isNullable: true } - > - >; - }); - -export type BlueprintMethods = ForgeMethods< - BaseForgeMethodsConfig ->; diff --git a/lib/blueprint/index.ts b/lib/blueprint/index.ts deleted file mode 100644 index f2731df..0000000 --- a/lib/blueprint/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { verifyChain, verifyChainAsync } from '../forgeFunctions'; - -import type { - BaseForgeObject, - BaseForgeOptions, - CheckConfig, - ForgeMethod, - UnsuccessfulVerificationResult, - VerificationResult -} from '../forgeTypes'; -import type { BlueprintMethods } from './blueprint.types'; - -/** - * Creates a blueprint(object) validation chain. - * @param model - The model to use for validation. - * @param errorMessage - Optional error message for the blueprint validation. - * @returns A new BlueprintMethods instance for the specified model. - */ -export const blueprint = ( - model: TBlueprint, - errorMessage?: string -) => { - const forgeType = (value: T): boolean => { - if (typeof value !== 'object' || Array.isArray(value) || !value) { - return false; - } - return true; - }; - - const createMethods = ( - methods: ForgeMethod[], - forgeOptions: BaseForgeOptions - ) => { - const forge = (value: T): VerificationResult => { - if ( - (forgeOptions.optional && value === undefined) || - (forgeOptions.nullable && value === null) - ) { - return { success: true, value }; - } - - let issues: UnsuccessfulVerificationResult[] = []; - - // Verify all properties in the model - Object.entries(model).forEach(([key, forgeElement]) => { - const res = forgeElement.forge( - value ? (value as Record)[key] : undefined - ); - if (res.success) { - return; - } - if (res.issues) { - let levelIssues: UnsuccessfulVerificationResult[] = []; - - res.issues.forEach((issue) => { - if (issue.issues) { - levelIssues = levelIssues.concat( - issue.issues?.map((subIssue) => ({ - success: false, - errorCode: subIssue.errorCode, - caller: subIssue.caller, - errorMessage: subIssue.errorMessage, - path: [key] - })) ?? [] - ); - return; - } - levelIssues.push({ - ...issue, - path: [key].concat(issue.path || []) - }); - }); - - issues = issues.concat(levelIssues); - return; - } - }); - - // Verify validations at blueprint level - const res = verifyChain({ value, methods }, forgeOptions); - - if (!res.success) { - issues = issues.concat(res.issues || []); - } - if (issues.length > 0) { - return { - success: false, - errorCode: 'validation_error', - caller: '', - issues - }; - } - return { success: true, value }; - }; - - const forgeAsync = async ( - value: T - ): Promise> => { - if ( - (forgeOptions.optional && value === undefined) || - (forgeOptions.nullable && value === null) - ) { - return { success: true, value }; - } - - let issues: UnsuccessfulVerificationResult[] = []; - - // Verify all properties in the model - await Promise.all( - Object.entries(model).map(async ([key, forgeElement]) => { - const res = await forgeElement.forgeAsync( - value - ? (value as Record)[key] - : undefined - ); - if (res.success) { - return; - } - if (res.issues) { - let levelIssues: UnsuccessfulVerificationResult[] = []; - - res.issues.forEach((issue) => { - if (issue.issues) { - levelIssues = levelIssues.concat( - issue.issues?.map((subIssue) => ({ - success: false, - errorCode: subIssue.errorCode, - caller: subIssue.caller, - errorMessage: subIssue.errorMessage, - path: [key] - })) ?? [] - ); - return; - } - levelIssues.push({ - ...issue, - path: [key].concat(issue.path || []) - }); - }); - - issues = issues.concat(levelIssues); - return; - } - }) - ); - - // Verify validations at blueprint level - const res = await verifyChainAsync( - { value, methods }, - forgeOptions - ); - - if (!res.success) { - issues = issues.concat(res.issues || []); - } - if (issues.length > 0) { - return { - success: false, - errorCode: 'validation_error', - caller: 'blueprint', - issues - }; - } - return { success: true, value }; - }; - - const optional = () => { - return createMethods(methods, { ...forgeOptions, optional: true }); - }; - - const nullable = () => { - return createMethods(methods, { ...forgeOptions, nullable: true }); - }; - - const check = ( - fn: (value: T) => boolean | Promise, - config?: CheckConfig - ) => { - return createMethods( - [...methods, { fn, caller: 'check', ...config }], - forgeOptions - ); - }; - - const newMethods: Record = { - forge, - forgeAsync, - check, - isOptional: forgeOptions.optional, - isNullable: forgeOptions.nullable, - model - }; - if (!forgeOptions.optional) { - newMethods.optional = optional; - } - if (!forgeOptions.nullable) { - newMethods.nullable = nullable; - } - - return newMethods as BlueprintMethods; - }; - - return createMethods( - [ - { - fn: forgeType, - errorCode: 'value_error', - caller: 'blueprint', - errorMessage - } - ], - { optional: false, nullable: false } - ); -}; diff --git a/lib/boolean/boolean.types.ts b/lib/boolean/boolean.types.ts deleted file mode 100644 index 050a395..0000000 --- a/lib/boolean/boolean.types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - BaseForgeMethods, - BaseForgeMethodsConfig, - CheckConfig, - UpdateForgeConfig -} from '../forgeTypes'; - -type ForgeMethods = BaseForgeMethods< - Config['type'] -> & { - /** - * Checks if the value passes the custom validation function. - * @param fn - The custom validation function to apply. - * @param config - The configuration for the check, including error message and options. - * @returns A new ForgeMethods instance with the validation applied. - */ - check: ( - fn: (value: Config['type']) => boolean | Promise, - config?: CheckConfig - ) => ForgeMethods; -} & (Config['isOptional'] extends true - ? object - : { - /** - * Applies an optional flag to the value forged by this method. - * @returns A new ForgeMethods instance with the optional flag set. - */ - optional: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | undefined; isOptional: true } - > - >; - }) & - (Config['isNullable'] extends true - ? object - : { - /** - * Applies a nullable flag to the value forged by this method. - * @returns A new ForgeMethods instance with the nullable flag set. - */ - nullable: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | null; isNullable: true } - > - >; - }); - -export type BooleanMethods = ForgeMethods>; diff --git a/lib/boolean/index.ts b/lib/boolean/index.ts deleted file mode 100644 index 8318bdc..0000000 --- a/lib/boolean/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { verifyChain, verifyChainAsync } from '../forgeFunctions'; - -import type { - BaseForgeOptions, - CheckConfig, - ForgeMethod, - VerificationResult -} from '../forgeTypes'; -import type { BooleanMethods } from './boolean.types'; - -/** - * Creates a boolean validation chain. - * @param errorMessage - The error message to return if validation fails. - * @returns A new BooleanMethods instance with the specified error message. - */ -export const boolean = (errorMessage?: string) => { - const forgeType = (value: T): boolean => - typeof value === 'boolean'; - - const createMethods = ( - methods: ForgeMethod[], - forgeOptions: BaseForgeOptions - ) => { - const forge = (value: T): VerificationResult => { - return verifyChain({ value, methods }, forgeOptions); - }; - - const forgeAsync = ( - value: T - ): Promise> => { - return verifyChainAsync({ value, methods }, forgeOptions); - }; - - const optional = () => { - return createMethods(methods, { ...forgeOptions, optional: true }); - }; - - const nullable = () => { - return createMethods(methods, { ...forgeOptions, nullable: true }); - }; - - const check = ( - fn: (value: T) => boolean | Promise, - config?: CheckConfig - ) => { - return createMethods( - [...methods, { fn, caller: 'check', ...config }], - forgeOptions - ); - }; - - const newMethods: Record = { - forge, - forgeAsync, - check, - isOptional: forgeOptions.optional, - isNullable: forgeOptions.nullable - }; - if (!forgeOptions.optional) { - newMethods.optional = optional; - } - if (!forgeOptions.nullable) { - newMethods.nullable = nullable; - } - - return newMethods as BooleanMethods; - }; - - return createMethods( - [ - { - fn: forgeType, - errorCode: 'value_error', - caller: 'boolean', - errorMessage - } - ], - { optional: false, nullable: false } - ); -}; diff --git a/lib/forgeFunctions.ts b/lib/forgeFunctions.ts index 713f7c6..adab9fc 100644 --- a/lib/forgeFunctions.ts +++ b/lib/forgeFunctions.ts @@ -1,103 +1,39 @@ import type { - BaseForgeOptions, - ForgeData, + BaseForgeConfig, UnsuccessfulVerificationResult, VerificationResult -} from './forgeTypes'; +} from './types'; /** * Helper function to verify a value against a chain of validation functions. * @param data - Data and configuration for the verification. - * @returns A VerificationResult indicating the outcome of the verification. - */ -export const verifyChain = ( - data: ForgeData, - options: BaseForgeOptions -): VerificationResult => { - let lastMethodName = ''; - const issues: UnsuccessfulVerificationResult[] = []; - - try { - data.methods.some((method) => { - lastMethodName = method.caller; - const forgeResult = method.fn(data.value); - - if (forgeResult instanceof Promise) { - issues.push({ - success: false, - errorCode: 'async_method_error', - caller: method.caller, - errorMessage: method.errorMessage, - path: method.path - }); - return !method.loose; - } - - if (typeof forgeResult === 'boolean') { - if ( - (options.optional && data.value === undefined) || - (options.nullable && data.value === null) - ) { - return true; - } - if (!forgeResult) { - issues.push({ - success: false, - errorCode: method.errorCode || 'validation_error', - caller: method.caller, - errorMessage: method.errorMessage, - path: method.path - }); - return !method.loose; - } - return false; - } - - return false; - }); - } catch (error) { - issues.push({ - success: false, - errorCode: 'unexpected_error', - caller: lastMethodName, - errorMessage: error instanceof Error ? error.message : String(error) - }); - } - - return issues.length === 0 - ? { success: true, value: data.value } - : { success: false, errorCode: 'validation_error', caller: '', issues }; -}; -/** - * Asynchronous version of the verifyChain function. - * @param data - Data and configuration for the verification. * @param options - Options for the verification process. * @returns A Promise that resolves to a VerificationResult indicating the outcome of the verification. */ export const verifyChainAsync = async ( - data: ForgeData, - options: BaseForgeOptions + value: T, + config: BaseForgeConfig ): Promise> => { let lastMethodName = ''; const issues: UnsuccessfulVerificationResult[] = []; try { - for (const method of data.methods) { + for (const method of config.queue) { lastMethodName = method.caller; - const forgeResult = await method.fn(data.value); + const forgeResult = await method.fn(value); if (typeof forgeResult === 'boolean') { if ( - (options.optional && data.value === undefined) || - (options.nullable && data.value === null) + (config.isOptional && value === undefined) || + (config.isNullable && value === null) ) { break; } if (!forgeResult) { issues.push({ success: false, - errorCode: method.errorCode || 'validation_error', caller: method.caller, + errorCode: method.errorCode || 'validation_error', errorMessage: method.errorMessage, path: method.path }); @@ -112,13 +48,18 @@ export const verifyChainAsync = async ( } catch (error) { issues.push({ success: false, - errorCode: 'unexpected_error', caller: lastMethodName, + errorCode: 'unexpected_error', errorMessage: error instanceof Error ? error.message : String(error) }); } return issues.length === 0 - ? { success: true, value: data.value } - : { success: false, errorCode: 'validation_error', caller: '', issues }; + ? { success: true, value } + : { + success: false, + caller: '', + errorCode: 'validation_error', + issues + }; }; diff --git a/lib/forgeTypes.ts b/lib/forgeTypes.ts deleted file mode 100644 index d5edd0f..0000000 --- a/lib/forgeTypes.ts +++ /dev/null @@ -1,93 +0,0 @@ -export type ErrorCode = - | 'value_error' - | 'validation_error' - | 'async_method_error' - | 'unexpected_error'; - -export type UnsuccessfulVerificationResult = { - success: false; - errorCode: ErrorCode; - caller: string; - errorMessage?: string; - /** - * The path to the value that failed verification. - */ - path?: string[]; - /** - * An array of issues that occurred during the verification process. - * Each issue can provide additional context about the failure. - */ - issues?: UnsuccessfulVerificationResult[]; - /** - * If the value is part of an array, this indicates the index of the array element that failed verification. - */ - arrayIndex?: number; -}; - -export type VerificationResult = - | { - success: true; - value: T; - } - | UnsuccessfulVerificationResult; - -export type ValidationFunction = ( - value: T -) => boolean | Promise; - -export type MutationFunction = (value: T) => T | Promise; - -export type CheckConfig = { - errorMessage?: string; - loose?: boolean; - path?: string[]; -}; - -export type ForgeMethod = { - fn: ValidationFunction | MutationFunction; - errorCode?: ErrorCode; - caller: string; -} & CheckConfig; - -export type ForgeData = { value: T; methods: ForgeMethod[] }; - -export type BaseForgeOptions = { - optional: boolean; - nullable: boolean; -}; - -export type BaseForgeMethodsConfig = { - type: T; - isOptional: boolean; - isNullable: boolean; -}; - -export type BaseForgeMethods = { - _type: TValue; - /** - * Indicates if the value forged by this method is optional. - */ - isOptional: boolean; - /** - * Indicates if the value forged by this method can be null. - */ - isNullable: boolean; - /** - * Forge a value based on the type and options defined in the chained methods. - * @return A VerificationResult containing the forged value or an error if validation fails. - */ - forge: (value: T) => VerificationResult; - /** - * Forge a value asynchronously based on the type and options defined in the chained methods. - * @return A VerificationResult containing the forged value or an error if validation fails. - */ - forgeAsync: (value: T) => Promise>; -}; - -export type BaseForgeObject = Record>; - -export type UpdateForgeConfig = Omit< - Target, - keyof TReplace -> & - TReplace; diff --git a/lib/vProposal/array.ts b/lib/forgeTypes/array.ts similarity index 94% rename from lib/vProposal/array.ts rename to lib/forgeTypes/array.ts index ef30809..0d98783 100644 --- a/lib/vProposal/array.ts +++ b/lib/forgeTypes/array.ts @@ -1,11 +1,11 @@ -import { baseForgeType } from './baseForgeType'; -import { verifyChainAsync } from './forgeFunctions'; +import { baseForgeType } from '../baseForgeType'; +import { verifyChainAsync } from '../forgeFunctions'; import type { BaseForgeObject, BaseForgeType, ForgeMethodConfig, UnsuccessfulVerificationResult -} from './types'; +} from '../types'; export const array = ( model: T @@ -84,7 +84,7 @@ export const array = ( if (issues.length > 0) { return { success: false, - caller: '', + caller: 'array', errorCode: 'validation_error', issues }; diff --git a/lib/vProposal/boolean.ts b/lib/forgeTypes/boolean.ts similarity index 81% rename from lib/vProposal/boolean.ts rename to lib/forgeTypes/boolean.ts index 01f8e13..d5c9328 100644 --- a/lib/vProposal/boolean.ts +++ b/lib/forgeTypes/boolean.ts @@ -1,5 +1,5 @@ -import { baseForgeType } from './baseForgeType'; -import type { BaseForgeType } from './types'; +import { baseForgeType } from '../baseForgeType'; +import type { BaseForgeType } from '../types'; export const boolean = (): BaseForgeType< { _: unknown }, diff --git a/lib/vProposal/number.ts b/lib/forgeTypes/number.ts similarity index 96% rename from lib/vProposal/number.ts rename to lib/forgeTypes/number.ts index 275d6e6..1aa443d 100644 --- a/lib/vProposal/number.ts +++ b/lib/forgeTypes/number.ts @@ -1,5 +1,5 @@ -import { baseForgeType } from './baseForgeType'; -import type { BaseForgeType, ForgeMethodConfig } from './types'; +import { baseForgeType } from '../baseForgeType'; +import type { BaseForgeType, ForgeMethodConfig } from '../types'; export const number = (): BaseForgeType< { diff --git a/lib/forgeTypes/object.ts b/lib/forgeTypes/object.ts new file mode 100644 index 0000000..cf1b23a --- /dev/null +++ b/lib/forgeTypes/object.ts @@ -0,0 +1,112 @@ +import { baseForgeType } from '../baseForgeType'; +import { verifyChainAsync } from '../forgeFunctions'; +import type { + BaseForgeObject, + BaseForgeType, + UnsuccessfulVerificationResult +} from '../types'; + +export type ObjectValues = { + [K in keyof T]: T[K] extends { model: infer M } + ? ObjectValues + : T[K] extends { _type: infer FType } + ? FType + : unknown; +}; + +export const object = >( + model: Model +): BaseForgeType< + { model: Model }, + { type: ObjectValues; optional: false; nullable: false; model: null } +> => { + return baseForgeType({ + isOptional: false, + isNullable: false, + queue: [ + { + fn: (value: unknown) => { + if ( + typeof value === 'object' && + !Array.isArray(value) && + Boolean(value) + ) { + return true; + } + return false; + }, + caller: 'object', + errorCode: 'value_error' + } + ], + methods: () => ({ model }), + forge: + (config) => + async (value: T) => { + if ( + (config.isOptional && value === undefined) || + (config.isNullable && value === null) + ) { + return { success: true, value }; + } + + let issues: UnsuccessfulVerificationResult[] = []; + + // Verify all properties in the model + await Promise.all( + Object.entries(model).map(async ([key, forgeElement]) => { + const res = await forgeElement.forge( + value + ? (value as Record)[key] + : undefined + ); + if (res.success) { + return; + } + if (res.issues) { + let levelIssues: UnsuccessfulVerificationResult[] = + []; + + res.issues.forEach((issue) => { + if (issue.issues) { + levelIssues = levelIssues.concat( + issue.issues?.map((subIssue) => ({ + success: false, + caller: subIssue.caller, + errorCode: subIssue.errorCode, + errorMessage: subIssue.errorMessage, + path: [key] + })) ?? [] + ); + return; + } + levelIssues.push({ + ...issue, + path: [key].concat(issue.path || []) + }); + }); + + issues = issues.concat(levelIssues); + return; + } + }) + ); + + // Verify validations at blueprint level + const res = await verifyChainAsync(value, config); + + if (!res.success) { + issues = issues.concat(res.issues || []); + } + if (issues.length > 0) { + return { + success: false, + caller: 'object', + errorCode: 'validation_error', + issues + }; + } + return { success: true, value }; + } + }); +}; diff --git a/lib/vProposal/string.ts b/lib/forgeTypes/string.ts similarity index 93% rename from lib/vProposal/string.ts rename to lib/forgeTypes/string.ts index 10d3ade..3c1e787 100644 --- a/lib/vProposal/string.ts +++ b/lib/forgeTypes/string.ts @@ -1,5 +1,5 @@ -import { baseForgeType } from './baseForgeType'; -import type { BaseForgeType, ForgeMethodConfig } from './types'; +import { baseForgeType } from '../baseForgeType'; +import type { BaseForgeType, ForgeMethodConfig } from '../types'; export const string = (): BaseForgeType< { diff --git a/lib/index.ts b/lib/index.ts index ef38770..2348c7f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,11 +1,10 @@ -import { array } from './array'; -import { blueprint } from './blueprint'; -import { boolean } from './boolean'; -import { number } from './number'; -import { string } from './string'; -import { RegExp } from './forgeRegExp'; +import { array } from './forgeTypes/array'; +import { boolean } from './forgeTypes/boolean'; +import { number } from './forgeTypes/number'; +import { object } from './forgeTypes/object'; +import { string } from './forgeTypes/string'; -const f = { array, blueprint, boolean, number, string, RegExp }; +const f = { array, boolean, number, object, string }; -export { array, blueprint, boolean, number, string, RegExp }; +export { array, boolean, number, object, string }; export default f; diff --git a/lib/number/index.ts b/lib/number/index.ts deleted file mode 100644 index e1eae8c..0000000 --- a/lib/number/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { verifyChain, verifyChainAsync } from '../forgeFunctions'; - -import type { - CheckConfig, - ForgeMethod, - VerificationResult -} from '../forgeTypes'; -import type { NumberForgeOptions, NumberMethods } from './number.types'; - -/** - * Creates a number validation chain. - * @param errorMessage - The error message to return if validation fails. - * @returns A new NumberMethods instance with the specified error message. - */ -export const number = (errorMessage?: string) => { - const forgeType = (value: T): boolean => - typeof value === 'number'; - - const createMethods = ( - methods: ForgeMethod[], - forgeOptions: NumberForgeOptions - ) => { - const forge = (value: T): VerificationResult => { - return verifyChain({ value, methods }, forgeOptions); - }; - - const forgeAsync = ( - value: T - ): Promise> => { - return verifyChainAsync({ value, methods }, forgeOptions); - }; - - const optional = () => { - return createMethods(methods, { ...forgeOptions, optional: true }); - }; - - const nullable = () => { - return createMethods(methods, { ...forgeOptions, nullable: true }); - }; - - const check = ( - fn: (value: T) => boolean | Promise, - config?: CheckConfig - ) => { - return createMethods( - [...methods, { fn, caller: 'check', ...config }], - forgeOptions - ); - }; - - const min = (min: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (typeof value === 'number') { - return value >= min; - } - return false; - }, - caller: 'min', - errorMessage - } - ], - { ...forgeOptions, hasMin: true } - ); - }; - - const max = (max: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (typeof value === 'number') { - return value <= max; - } - return false; - }, - caller: 'max', - errorMessage - } - ], - { ...forgeOptions, hasMax: true } - ); - }; - - const newMethods: Record = { - forge, - forgeAsync, - check, - isOptional: forgeOptions.optional, - isNullable: forgeOptions.nullable - }; - if (!forgeOptions.optional) { - newMethods.optional = optional; - } - if (!forgeOptions.nullable) { - newMethods.nullable = nullable; - } - if (!forgeOptions.hasMin) { - newMethods.min = min; - } - if (!forgeOptions.hasMax) { - newMethods.max = max; - } - - return newMethods as NumberMethods; - }; - - return createMethods( - [ - { - fn: forgeType, - errorCode: 'value_error', - caller: 'number', - errorMessage - } - ], - { - optional: false, - nullable: false, - hasMin: false, - hasMax: false - } - ); -}; diff --git a/lib/number/number.types.ts b/lib/number/number.types.ts deleted file mode 100644 index 6c47367..0000000 --- a/lib/number/number.types.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { - BaseForgeMethods, - BaseForgeMethodsConfig, - BaseForgeOptions, - CheckConfig, - UpdateForgeConfig -} from '../forgeTypes'; - -type ForgeOptions = { hasMin: boolean; hasMax: boolean }; - -export type NumberForgeOptions = BaseForgeOptions & ForgeOptions; - -type ForgeMethodsConfig = BaseForgeMethodsConfig & ForgeOptions; - -type ForgeMethods = BaseForgeMethods< - Config['type'] -> & { - /** - * Checks if the value passes the custom validation function. - * @param fn - The custom validation function to apply. - * @param config - The configuration for the check, including error message and options. - * @returns A new ForgeMethods instance with the validation applied. - */ - check: ( - fn: (value: Config['type']) => boolean | Promise, - config?: CheckConfig - ) => ForgeMethods; -} & (Config['isOptional'] extends true - ? object - : { - /** - * Applies an optional flag to the value forged by this method. - * @returns A new ForgeMethods instance with the optional flag set. - */ - optional: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | undefined; isOptional: true } - > - >; - }) & - (Config['isNullable'] extends true - ? object - : { - /** - * Applies a nullable flag to the value forged by this method. - * @returns A new ForgeMethods instance with the nullable flag set. - */ - nullable: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | null; isNullable: true } - > - >; - }) & - (Config['hasMin'] extends true - ? object - : { - /** - * Applies a minimum value constraint to the value forged by this method. - * @param min - The minimum value that the forged value must be greater than or equal to. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the minimum value constraint applied. - */ - min: ( - min: number, - errorMessage?: string - ) => ForgeMethods>; - }) & - (Config['hasMax'] extends true - ? object - : { - /** - * Applies a maximum value constraint to the value forged by this method. - * @param max - The maximum value that the forged value must be less than or equal to. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the maximum value constraint applied. - */ - max: ( - max: number, - errorMessage?: string - ) => ForgeMethods>; - }); - -export type NumberMethods = ForgeMethods>; diff --git a/lib/string/index.ts b/lib/string/index.ts deleted file mode 100644 index 2593789..0000000 --- a/lib/string/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { verifyChain, verifyChainAsync } from '../forgeFunctions'; - -import type { - CheckConfig, - ForgeMethod, - VerificationResult -} from '../forgeTypes'; -import type { StringForgeOptions, StringMethods } from './string.types'; - -/** - * Creates a string validation chain. - * @param errorMessage - The error message to return if validation fails. - * @returns A new StringMethods instance with the specified error message. - */ -export const string = (errorMessage?: string) => { - const forgeType = (value: T): boolean => - typeof value === 'string'; - - const createMethods = ( - methods: ForgeMethod[], - forgeOptions: StringForgeOptions - ) => { - const forge = (value: T): VerificationResult => { - return verifyChain({ value, methods }, forgeOptions); - }; - - const forgeAsync = ( - value: T - ): Promise> => { - return verifyChainAsync({ value, methods }, forgeOptions); - }; - - const optional = () => { - return createMethods(methods, { ...forgeOptions, optional: true }); - }; - - const nullable = () => { - return createMethods(methods, { ...forgeOptions, nullable: true }); - }; - - const check = ( - fn: (value: T) => boolean | Promise, - config?: CheckConfig - ) => { - return createMethods( - [...methods, { fn, caller: 'check', ...config }], - forgeOptions - ); - }; - - const minLength = (minLength: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (typeof value === 'string') { - return value.length >= minLength; - } - return false; - }, - caller: 'minLength', - errorMessage - } - ], - { - ...forgeOptions, - hasMinLength: true - } - ); - }; - - const maxLength = (maxLength: number, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (typeof value === 'string') { - return value.length <= maxLength; - } - return false; - }, - caller: 'maxLength', - errorMessage - } - ], - { - ...forgeOptions, - hasMaxLength: true - } - ); - }; - - const regExp = (regex: RegExp, errorMessage?: string) => { - return createMethods( - [ - ...methods, - { - fn: (value: T) => { - if (typeof value === 'string') { - return regex.test(value); - } - return false; - }, - caller: 'regExp', - errorMessage - } - ], - forgeOptions - ); - }; - - const newMethods: Record = { - forge, - forgeAsync, - check, - regExp, - isOptional: forgeOptions.optional, - isNullable: forgeOptions.nullable - }; - if (!forgeOptions.optional) { - newMethods.optional = optional; - } - if (!forgeOptions.nullable) { - newMethods.nullable = nullable; - } - if (!forgeOptions.hasMinLength) { - newMethods.minLength = minLength; - } - if (!forgeOptions.hasMaxLength) { - newMethods.maxLength = maxLength; - } - - return newMethods as StringMethods; - }; - - return createMethods( - [ - { - fn: forgeType, - errorCode: 'value_error', - caller: 'string', - errorMessage - } - ], - { - optional: false, - nullable: false, - hasMinLength: false, - hasMaxLength: false - } - ); -}; diff --git a/lib/string/string.types.ts b/lib/string/string.types.ts deleted file mode 100644 index 10f66fc..0000000 --- a/lib/string/string.types.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - BaseForgeMethods, - BaseForgeMethodsConfig, - BaseForgeOptions, - CheckConfig, - UpdateForgeConfig -} from '../forgeTypes'; - -type ForgeOptions = { hasMinLength: boolean; hasMaxLength: boolean }; - -export type StringForgeOptions = BaseForgeOptions & ForgeOptions; - -type ForgeMethodsConfig = BaseForgeMethodsConfig & ForgeOptions; - -type ForgeMethods = BaseForgeMethods< - Config['type'] -> & { - /** - * Checks if the value passes the specified validation function. - * @param fn - The validation function to apply. - * @param config - The configuration for the check, including error message and options. - * @returns A new ForgeMethods instance with the validation applied. - */ - check: ( - fn: (value: Config['type']) => boolean | Promise, - config?: CheckConfig - ) => ForgeMethods; - /** - * Checks if the value matches the specified regular expression. - * @param regExp - The regular expression to apply. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the validation applied. - */ - regExp: (regExp: RegExp, errorMessage?: string) => ForgeMethods; -} & (Config['isOptional'] extends true - ? object - : { - /** - * Applies an optional flag to the value forged by this method. - * @returns A new ForgeMethods instance with the optional flag set. - */ - optional: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | undefined; isOptional: true } - > - >; - }) & - (Config['isNullable'] extends true - ? object - : { - /** - * Applies a nullable flag to the value forged by this method. - * @returns A new ForgeMethods instance with the nullable flag set. - */ - nullable: () => ForgeMethods< - UpdateForgeConfig< - Config, - { type: Config['type'] | null; isNullable: true } - > - >; - }) & - (Config['hasMinLength'] extends true - ? object - : { - /** - * Checks if the value has a minimum length. - * @param minLength - The minimum length to enforce. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the validation applied. - */ - minLength: ( - minLength: number, - errorMessage?: string - ) => ForgeMethods< - UpdateForgeConfig - >; - }) & - (Config['hasMaxLength'] extends true - ? object - : { - /** - * Checks if the value has a maximum length. - * @param maxLength - The maximum length to enforce. - * @param errorMessage - The error message to return if validation fails. - * @returns A new ForgeMethods instance with the validation applied. - */ - maxLength: ( - maxLength: number, - errorMessage?: string - ) => ForgeMethods< - UpdateForgeConfig - >; - }); - -export type StringMethods = ForgeMethods>; diff --git a/lib/vProposal/types.ts b/lib/types.ts similarity index 97% rename from lib/vProposal/types.ts rename to lib/types.ts index e6adcc4..221883c 100644 --- a/lib/vProposal/types.ts +++ b/lib/types.ts @@ -83,7 +83,7 @@ export type BaseForgeType< } & { [K in keyof Methods]: State[K] } >; check: ( - fn: (value: T) => boolean | Promise, + fn: (value: State['type']) => boolean | Promise, config?: ForgeMethodConfig ) => BaseForgeType; forge: (value: T) => Promise>; @@ -115,7 +115,7 @@ export type BaseForgeType< : State[StateItem]; } > - : never; + : Methods[MethodName]; }; export type BaseForgeObject = { diff --git a/lib/vProposal/forgeFunctions.ts b/lib/vProposal/forgeFunctions.ts deleted file mode 100644 index adab9fc..0000000 --- a/lib/vProposal/forgeFunctions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { - BaseForgeConfig, - UnsuccessfulVerificationResult, - VerificationResult -} from './types'; - -/** - * Helper function to verify a value against a chain of validation functions. - * @param data - Data and configuration for the verification. - * @param options - Options for the verification process. - * @returns A Promise that resolves to a VerificationResult indicating the outcome of the verification. - */ -export const verifyChainAsync = async ( - value: T, - config: BaseForgeConfig -): Promise> => { - let lastMethodName = ''; - const issues: UnsuccessfulVerificationResult[] = []; - - try { - for (const method of config.queue) { - lastMethodName = method.caller; - const forgeResult = await method.fn(value); - - if (typeof forgeResult === 'boolean') { - if ( - (config.isOptional && value === undefined) || - (config.isNullable && value === null) - ) { - break; - } - if (!forgeResult) { - issues.push({ - success: false, - caller: method.caller, - errorCode: method.errorCode || 'validation_error', - errorMessage: method.errorMessage, - path: method.path - }); - if (method.loose) { - continue; - } else { - break; - } - } - } - } - } catch (error) { - issues.push({ - success: false, - caller: lastMethodName, - errorCode: 'unexpected_error', - errorMessage: error instanceof Error ? error.message : String(error) - }); - } - - return issues.length === 0 - ? { success: true, value } - : { - success: false, - caller: '', - errorCode: 'validation_error', - issues - }; -}; diff --git a/lib/vProposal/index.ts b/lib/vProposal/index.ts deleted file mode 100644 index 58a6117..0000000 --- a/lib/vProposal/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { array } from './array'; -// import { blueprint } from './blueprint'; -import { boolean } from './boolean'; -import { number } from './number'; -import { string } from './string'; - -const f = { array, boolean, number, string }; - -export { array, boolean, number, string }; -export default f; From a565d93925a94f6a5ed942bdfc02c7831cbbe7a7 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Thu, 17 Jul 2025 18:28:13 -0600 Subject: [PATCH 08/11] Update devDependencies: upgrade @eslint/js to 10.0.0, @types/node to 24.0.14, typescript-eslint to 8.37.0, and vitest to 3.2.4 --- package-lock.json | 196 +++++++++++++++++++++++----------------------- package.json | 8 +- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34b0661..80c1a96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.26.0", - "@types/node": "^22.15.15", + "@eslint/js": "^10.0.0", + "@types/node": "^24.0.14", "prettier": "^3.6.2", "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.8.3", - "typescript-eslint": "^8.32.0", - "vitest": "^3.1.3" + "typescript-eslint": "^8.37.0", + "vitest": "^3.2.4" } }, "node_modules/@cspotcode/source-map-support": { @@ -527,9 +527,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -566,16 +566,14 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.0.tgz", + "integrity": "sha512-J2VKrn6YUBegZFzRQVOVA8jk7VViV/MXhQUvXuCUK1I2RGGT2E+bmNGOsgtdO9Wo25Kd3hfZFuCm8LcfsUFV7g==", + "deprecated": "This version should not be used.", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -604,20 +602,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1137,27 +1121,27 @@ "peer": true }, "node_modules/@types/node": { - "version": "22.16.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.0.tgz", - "integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==", + "version": "24.0.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", + "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1171,7 +1155,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1187,16 +1171,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -1212,14 +1196,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "engines": { @@ -1234,14 +1218,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1252,9 +1236,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", "dev": true, "license": "MIT", "engines": { @@ -1269,14 +1253,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1293,9 +1278,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -1307,16 +1292,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1362,16 +1347,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1386,13 +1371,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1898,9 +1883,9 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "peer": true, @@ -1909,9 +1894,9 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1990,6 +1975,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3402,15 +3401,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz", - "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==", + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.35.1", - "@typescript-eslint/parser": "8.35.1", - "@typescript-eslint/utils": "8.35.1" + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3425,9 +3425,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index fa71a82..5b1673b 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,13 @@ }, "homepage": "https://github.com/Feresaul/Forge#readme", "devDependencies": { - "@eslint/js": "^9.26.0", - "@types/node": "^22.15.15", + "@eslint/js": "^10.0.0", + "@types/node": "^24.0.14", "prettier": "^3.6.2", "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.8.3", - "typescript-eslint": "^8.32.0", - "vitest": "^3.1.3" + "typescript-eslint": "^8.37.0", + "vitest": "^3.2.4" } } From 4db4e99dc201e035d5d5c73e8a2829300c8c8835 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Thu, 17 Jul 2025 18:33:52 -0600 Subject: [PATCH 09/11] Add GitHub Actions workflows for npm versioning and PR tests --- .github/workflows/npm-version.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/pr-tests.yml | 25 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/npm-version.yml create mode 100644 .github/workflows/pr-tests.yml diff --git a/.github/workflows/npm-version.yml b/.github/workflows/npm-version.yml new file mode 100644 index 0000000..58f47d3 --- /dev/null +++ b/.github/workflows/npm-version.yml @@ -0,0 +1,29 @@ +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 0000000..8492b3c --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,25 @@ +name: PR Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test From 1e7a6720e6312fc6a19b39ac160c8fb838bddf26 Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Thu, 17 Jul 2025 18:49:40 -0600 Subject: [PATCH 10/11] Refactor tests to use async/await syntax for better readability and consistency; update test cases for various data types (boolean, number, string, object, array) to include async handling, method chaining, and custom checks; add new object tests for nested structures and various combinations; remove deprecated blueprint tests. --- test/TESTING_GUIDELINES.md | 31 ++++--- test/array.test.ts | 143 ++++++++++++++++++------------ test/blueprint.test.ts | 126 -------------------------- test/boolean.test.ts | 99 +++++++++++---------- test/number.test.ts | 124 ++++++++++++++++---------- test/object.test.ts | 175 +++++++++++++++++++++++++++++++++++++ test/string.test.ts | 156 ++++++++++++++++++++------------- 7 files changed, 508 insertions(+), 346 deletions(-) delete mode 100644 test/blueprint.test.ts create mode 100644 test/object.test.ts diff --git a/test/TESTING_GUIDELINES.md b/test/TESTING_GUIDELINES.md index d2dcb15..d277886 100644 --- a/test/TESTING_GUIDELINES.md +++ b/test/TESTING_GUIDELINES.md @@ -13,8 +13,9 @@ - Validate the primary functionality of the module. - Example: ```typescript - it('should return true for a valid input', () => { - expect(f.module().forge(validInput).success).toBe(true); + it('should return true for a valid input', async () => { + const result = await f.module().forge(validInput); + expect(result.success).toBe(true); }); ``` @@ -23,8 +24,9 @@ - Test invalid inputs and edge cases. - Example: ```typescript - it('should return false for an invalid input', () => { - expect(f.module().forge(invalidInput).success).toBe(false); + it('should return false for an invalid input', async () => { + const result = await f.module().forge(invalidInput); + expect(result.success).toBe(false); }); ``` @@ -32,12 +34,19 @@ - Test all available methods (`optional`, `nullable`, `check`, etc.). - Example: + ```typescript - it('should handle optional and nullable inputs', () => { + it('should handle optional and nullable configs', async () => { const schema = f.module().optional().nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge(validInput).success).toBe(true); + + const result1 = await schema.forge(undefined); + expect(result1.success).toBe(true); + + const result2 = await schema.forge(null); + expect(result2.success).toBe(true); + + const result3 = await schema.forge(validInput); + expect(result3.success).toBe(true); }); ``` @@ -63,10 +72,10 @@ - Refer to existing test files for examples: - `array.test.ts` - - `string.test.ts` - - `number.test.ts` - `boolean.test.ts` - - `blueprint.test.ts` + - `number.test.ts` + - `object.test.ts` + - `string.test.ts` ## Notes diff --git a/test/array.test.ts b/test/array.test.ts index 1441429..a8e1924 100644 --- a/test/array.test.ts +++ b/test/array.test.ts @@ -1,95 +1,122 @@ import { describe, it, expect } from 'vitest'; - import f from '../lib'; describe('f.array', () => { - it('should return true for a valid array', () => { - expect(f.array(f.string()).forge(['valid', 'array']).success).toBe( - true - ); + it('should return true for a valid array', async () => { + const schema = f.array(f.string()); + const result = await schema.forge(['valid', 'array']); + expect(result.success).toBe(true); }); - it('should return false for an invalid array', () => { - expect(f.array(f.string()).forge('invalid').success).toBe(false); - expect(f.array(f.string()).forge(undefined).success).toBe(false); - expect(f.array(f.string()).forge(null).success).toBe(false); - expect(f.array(f.string()).forge([42]).success).toBe(false); + it('should return false for an invalid array', async () => { + const schema = f.array(f.string()); + expect((await schema.forge('invalid')).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge([42])).success).toBe(false); + expect((await schema.forge({})).success).toBe(false); }); - it('should validate elements in the array', () => { - const result = f.array(f.string()).forge(['valid', 42]); + it('should validate elements in the array', async () => { + const schema = f.array(f.string()); + const result = await schema.forge(['valid', 42]); expect(result.success).toBe(false); if (!result.success && result.issues) { expect(result.issues[0].arrayIndex).toBe(1); } }); - it('should handle optional arrays', () => { - expect(f.array(f.string()).optional().forge(undefined).success).toBe( - true - ); - expect(f.array(f.string()).optional().forge(null).success).toBe(false); - expect(f.array(f.string()).optional().forge(['valid']).success).toBe( - true - ); + it('should validate minLength and maxLength', async () => { + const schema = f.array(f.string()).minLength(2).maxLength(3); + expect((await schema.forge(['a'])).success).toBe(false); + expect((await schema.forge(['a', 'b'])).success).toBe(true); + expect((await schema.forge(['a', 'b', 'c'])).success).toBe(true); + expect((await schema.forge(['a', 'b', 'c', 'd'])).success).toBe(false); }); - it('should handle nullable arrays', () => { - expect(f.array(f.string()).nullable().forge(null).success).toBe(true); - expect(f.array(f.string()).nullable().forge(undefined).success).toBe( + it('should support method chaining', async () => { + const schema = f + .array(f.string().minLength(2)) + .minLength(2) + .maxLength(3); + expect((await schema.forge(['ab', 'cd'])).success).toBe(true); + expect((await schema.forge(['a', 'cd'])).success).toBe(false); + expect((await schema.forge(['ab'])).success).toBe(false); + expect((await schema.forge(['ab', 'cd', 'ef', 'gh'])).success).toBe( false ); - expect(f.array(f.string()).nullable().forge(['valid']).success).toBe( - true - ); }); - it('should validate using custom check', () => { - const hasAtLeastOneElement = (value: unknown[]) => value.length > 0; - expect( - f - .array(f.string()) - .check(hasAtLeastOneElement, { - errorMessage: 'Must have at least one element' - }) - .forge(['valid']).success - ).toBe(true); - expect( - f - .array(f.string()) - .check(hasAtLeastOneElement, { - errorMessage: 'Must have at least one element' - }) - .forge([]).success - ).toBe(false); + it('should handle optional arrays', async () => { + const schema = f.array(f.string()).optional(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(['valid'])).success).toBe(true); + }); + + it('should handle nullable arrays', async () => { + const schema = f.array(f.string()).nullable(); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(['valid'])).success).toBe(true); }); - it('should handle optional and nullable arrays', () => { + it('should handle optional and nullable arrays', async () => { const schema = f.array(f.string()).optional().nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge(['valid']).success).toBe(true); - expect(schema.forge(42).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(['valid'])).success).toBe(true); + expect((await schema.forge(42)).success).toBe(false); }); - it('should validate async forge', async () => { - const result = await f.array(f.string()).forgeAsync(['valid', 'array']); - expect(result.success).toBe(true); + it('should validate using custom check', async () => { + const schema = f.array(f.string()).check((value) => value.length > 0, { + errorMessage: 'Must have at least one element' + }); + expect((await schema.forge(['valid'])).success).toBe(true); + expect((await schema.forge([])).success).toBe(false); }); - it('should validate async check with forgeAsync', async () => { + it('should support multiple checks in chain', async () => { + const schema = f + .array(f.string()) + .check((value) => value.length > 1, { errorMessage: 'Too short' }) + .check((value) => value.every((v) => typeof v === 'string'), { + errorMessage: 'All must be strings' + }); + expect((await schema.forge(['a'])).success).toBe(false); + expect((await schema.forge(['a', 'b'])).success).toBe(true); + expect((await schema.forge(['a', 1])).success).toBe(false); + }); + + it('should validate async check', async () => { const schema = f.array(f.string()).check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 400)); + await new Promise((resolve) => setTimeout(resolve, 10)); return value.length > 0; }, { errorMessage: 'Array must not be empty' } ); - - const result = await schema.forgeAsync(['valid', 'array']); + const result = await schema.forge(['valid']); expect(result.success).toBe(true); - - const emptyResult = await schema.forgeAsync([]); + const emptyResult = await schema.forge([]); expect(emptyResult.success).toBe(false); }); + + it('should support arrays with all method combinations', async () => { + const schema = f + .array(f.string().minLength(2)) + .minLength(2) + .maxLength(3) + .optional() + .nullable(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(['ab', 'cd'])).success).toBe(true); + expect((await schema.forge(['a', 'cd'])).success).toBe(false); + expect((await schema.forge(['ab'])).success).toBe(false); + expect((await schema.forge(['ab', 'cd', 'ef', 'gh'])).success).toBe( + false + ); + }); }); diff --git a/test/blueprint.test.ts b/test/blueprint.test.ts deleted file mode 100644 index 27d5c95..0000000 --- a/test/blueprint.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import f from '../lib'; - -describe('f.blueprint', () => { - it('should return true for a valid blueprint', () => { - const blueprint = f.blueprint({ - name: f.string(), - age: f.number() - }); - expect(blueprint.forge({ name: 'John', age: 30 }).success).toBe(true); - }); - - it('should return false for an invalid blueprint', () => { - const blueprint = f.blueprint({ - name: f.string(), - age: f.number() - }); - expect(blueprint.forge({ name: 'John', age: 'invalid' }).success).toBe( - false - ); - expect(blueprint.forge({ name: 42, age: 30 }).success).toBe(false); - expect(blueprint.forge(undefined).success).toBe(false); - expect(blueprint.forge(null).success).toBe(false); - }); - - it('should validate nested blueprints', () => { - const blueprint = f.blueprint({ - user: f.blueprint({ - name: f.string(), - age: f.number() - }) - }); - const result = blueprint.forge({ - user: { name: 'John', age: 'invalid' } - }); - expect(result.success).toBe(false); - if (!result.success && result.issues) { - expect(result.issues[0].path).toEqual(['user', 'age']); - } - }); - - it('should handle optional blueprints', () => { - const blueprint = f - .blueprint({ - name: f.string(), - age: f.number() - }) - .optional(); - expect(blueprint.forge(undefined).success).toBe(true); - expect(blueprint.forge(null).success).toBe(false); - expect(blueprint.forge({ name: 'John', age: 30 }).success).toBe(true); - }); - - it('should handle nullable blueprints', () => { - const blueprint = f - .blueprint({ - name: f.string(), - age: f.number() - }) - .nullable(); - expect(blueprint.forge(null).success).toBe(true); - expect(blueprint.forge(undefined).success).toBe(false); - expect(blueprint.forge({ name: 'John', age: 30 }).success).toBe(true); - }); - - it('should validate using custom check', () => { - const blueprint = f - .blueprint({ - name: f.string(), - age: f.number() - }) - .check((value) => value.age > 18, { - errorMessage: 'Age must be greater than 18' - }); - expect(blueprint.forge({ name: 'John', age: 30 }).success).toBe(true); - expect(blueprint.forge({ name: 'John', age: 15 }).success).toBe(false); - }); - - it('should handle optional and nullable blueprints', () => { - const schema = f - .blueprint({ - name: f.string(), - age: f.number() - }) - .optional() - .nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge({ name: 'John', age: 30 }).success).toBe(true); - expect(schema.forge(42).success).toBe(false); - }); - - it('should validate async forge', async () => { - const blueprint = f.blueprint({ - name: f.string(), - age: f.number() - }); - const result = await blueprint.forgeAsync({ name: 'John', age: 30 }); - expect(result.success).toBe(true); - }); - - it('should validate async check with forgeAsync', async () => { - const schema = f - .blueprint({ - name: f.string(), - age: f.number() - }) - .check( - async (value) => { - await new Promise((resolve) => setTimeout(resolve, 400)); - return value.age > 18; - }, - { errorMessage: 'Age must be greater than 18' } - ); - - const result = await schema.forgeAsync({ name: 'John', age: 30 }); - expect(result.success).toBe(true); - - const invalidResult = await schema.forgeAsync({ - name: 'John', - age: 15 - }); - expect(invalidResult.success).toBe(false); - }); -}); diff --git a/test/boolean.test.ts b/test/boolean.test.ts index 4685a62..5adc723 100644 --- a/test/boolean.test.ts +++ b/test/boolean.test.ts @@ -1,74 +1,83 @@ import { describe, it, expect } from 'vitest'; - import f from '../lib'; describe('f.boolean', () => { - it('should return true for a valid boolean', () => { - expect(f.boolean().forge(true).success).toBe(true); - expect(f.boolean().forge(false).success).toBe(true); + it('should return true for a valid boolean', async () => { + const schema = f.boolean(); + expect((await schema.forge(true)).success).toBe(true); + expect((await schema.forge(false)).success).toBe(true); }); - it('should return false for an invalid boolean', () => { - expect(f.boolean().forge('invalid').success).toBe(false); - expect(f.boolean().forge(undefined).success).toBe(false); - expect(f.boolean().forge(null).success).toBe(false); - expect(f.boolean().forge(42).success).toBe(false); + it('should return false for an invalid boolean', async () => { + const schema = f.boolean(); + expect((await schema.forge('invalid')).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(42)).success).toBe(false); + expect((await schema.forge({})).success).toBe(false); }); - it('should handle optional booleans', () => { - expect(f.boolean().optional().forge(undefined).success).toBe(true); - expect(f.boolean().optional().forge(null).success).toBe(false); - expect(f.boolean().optional().forge(true).success).toBe(true); + it('should handle optional booleans', async () => { + const schema = f.boolean().optional(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(true)).success).toBe(true); }); - it('should handle nullable booleans', () => { - expect(f.boolean().nullable().forge(null).success).toBe(true); - expect(f.boolean().nullable().forge(undefined).success).toBe(false); - expect(f.boolean().nullable().forge(false).success).toBe(true); + it('should handle nullable booleans', async () => { + const schema = f.boolean().nullable(); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(false)).success).toBe(true); }); - it('should handle optional and nullable booleans', () => { + it('should handle optional and nullable booleans', async () => { const schema = f.boolean().optional().nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge(true).success).toBe(true); - expect(schema.forge(42).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(true)).success).toBe(true); + expect((await schema.forge(42)).success).toBe(false); }); - it('should validate using custom check', () => { - const isTrue = (value: boolean) => value === true; - expect( - f - .boolean() - .check(isTrue, { errorMessage: 'Must be true' }) - .forge(true).success - ).toBe(true); - expect( - f - .boolean() - .check(isTrue, { errorMessage: 'Must be true' }) - .forge(false).success - ).toBe(false); + it('should validate using custom check', async () => { + const schema = f + .boolean() + .check((value) => value === true, { errorMessage: 'Must be true' }); + expect((await schema.forge(true)).success).toBe(true); + expect((await schema.forge(false)).success).toBe(false); }); - it('should validate async forge', async () => { - const result = await f.boolean().forgeAsync(true); - expect(result.success).toBe(true); + it('should support multiple checks in chain', async () => { + const schema = f + .boolean() + .check((value) => value === true, { errorMessage: 'Must be true' }) + .check((value) => typeof value === 'boolean', { + errorMessage: 'Must be boolean' + }); + expect((await schema.forge(true)).success).toBe(true); + expect((await schema.forge(false)).success).toBe(false); }); - it('should validate async check with forgeAsync', async () => { + it('should validate async check', async () => { const schema = f.boolean().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 400)); + await new Promise((resolve) => setTimeout(resolve, 10)); return value === true; }, { errorMessage: 'Value must be true' } ); - - const result = await schema.forgeAsync(true); + const result = await schema.forge(true); expect(result.success).toBe(true); - - const invalidResult = await schema.forgeAsync(false); + const invalidResult = await schema.forge(false); expect(invalidResult.success).toBe(false); }); + + it('should support booleans with all method combinations', async () => { + const schema = f.boolean().optional().nullable(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(true)).success).toBe(true); + expect((await schema.forge(false)).success).toBe(true); + expect((await schema.forge(1)).success).toBe(false); + }); }); diff --git a/test/number.test.ts b/test/number.test.ts index 9fedbb8..19adb16 100644 --- a/test/number.test.ts +++ b/test/number.test.ts @@ -1,73 +1,109 @@ import { describe, it, expect } from 'vitest'; - import f from '../lib'; describe('f.number', () => { - it('should return true for a valid number', () => { - expect(f.number().forge(42).success).toBe(true); + it('should return true for a valid number', async () => { + const schema = f.number(); + const result = await schema.forge(42); + expect(result.success).toBe(true); + }); + + it('should return false for an invalid number', async () => { + const schema = f.number(); + expect((await schema.forge('invalid')).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(true)).success).toBe(false); + expect((await schema.forge({})).success).toBe(false); + }); + + it('should validate lt, lte, gt, gte, positive, negative, integer', async () => { + expect((await f.number().lt(10).forge(9)).success).toBe(true); + expect((await f.number().lt(10).forge(10)).success).toBe(false); + expect((await f.number().lte(10).forge(10)).success).toBe(true); + expect((await f.number().lte(10).forge(11)).success).toBe(false); + expect((await f.number().gt(5).forge(6)).success).toBe(true); + expect((await f.number().gt(5).forge(5)).success).toBe(false); + expect((await f.number().gte(5).forge(5)).success).toBe(true); + expect((await f.number().gte(5).forge(4)).success).toBe(false); + expect((await f.number().positive().forge(1)).success).toBe(true); + expect((await f.number().positive().forge(-1)).success).toBe(false); + expect((await f.number().negative().forge(-1)).success).toBe(true); + expect((await f.number().negative().forge(1)).success).toBe(false); + expect((await f.number().integer().forge(2)).success).toBe(true); + expect((await f.number().integer().forge(2.5)).success).toBe(false); }); - it('should return false for an invalid number', () => { - expect(f.number().forge('invalid').success).toBe(false); - expect(f.number().forge(undefined).success).toBe(false); - expect(f.number().forge(null).success).toBe(false); - expect(f.number().forge(true).success).toBe(false); + it('should support method chaining', async () => { + const schema = f.number().gt(0).lt(10).integer(); + expect((await schema.forge(5)).success).toBe(true); + expect((await schema.forge(0)).success).toBe(false); + expect((await schema.forge(10)).success).toBe(false); + expect((await schema.forge(5.5)).success).toBe(false); }); - it('should handle optional numbers', () => { - expect(f.number().optional().forge(undefined).success).toBe(true); - expect(f.number().optional().forge(null).success).toBe(false); - expect(f.number().optional().forge(42).success).toBe(true); + it('should handle optional numbers', async () => { + const schema = f.number().optional(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(42)).success).toBe(true); }); - it('should handle nullable numbers', () => { - expect(f.number().nullable().forge(null).success).toBe(true); - expect(f.number().nullable().forge(undefined).success).toBe(false); - expect(f.number().nullable().forge(42).success).toBe(true); + it('should handle nullable numbers', async () => { + const schema = f.number().nullable(); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(42)).success).toBe(true); }); - it('should handle optional and nullable numbers', () => { + it('should handle optional and nullable numbers', async () => { const schema = f.number().optional().nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge(42).success).toBe(true); - expect(schema.forge('invalid').success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(42)).success).toBe(true); + expect((await schema.forge('invalid')).success).toBe(false); }); - it('should validate using custom check', () => { - const isPositive = (value: number) => value > 0; - expect( - f - .number() - .check(isPositive, { errorMessage: 'Must be positive' }) - .forge(42).success - ).toBe(true); - expect( - f - .number() - .check(isPositive, { errorMessage: 'Must be positive' }) - .forge(-1).success - ).toBe(false); + it('should validate using custom check', async () => { + const schema = f + .number() + .check((value) => value > 0, { errorMessage: 'Must be positive' }); + expect((await schema.forge(42)).success).toBe(true); + expect((await schema.forge(-1)).success).toBe(false); }); - it('should validate async forge', async () => { - const result = await f.number().forgeAsync(42); - expect(result.success).toBe(true); + it('should support multiple checks in chain', async () => { + const schema = f + .number() + .check((value) => value > 0, { errorMessage: 'Must be positive' }) + .check((value) => value % 2 === 0, { + errorMessage: 'Must be even' + }); + expect((await schema.forge(2)).success).toBe(true); + expect((await schema.forge(3)).success).toBe(false); + expect((await schema.forge(-2)).success).toBe(false); }); - it('should validate async check with forgeAsync', async () => { + it('should validate async check', async () => { const schema = f.number().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 400)); + await new Promise((resolve) => setTimeout(resolve, 10)); return value > 0; }, { errorMessage: 'Number must be positive' } ); - - const result = await schema.forgeAsync(42); + const result = await schema.forge(42); expect(result.success).toBe(true); - - const invalidResult = await schema.forgeAsync(-1); + const invalidResult = await schema.forge(-1); expect(invalidResult.success).toBe(false); }); + + it('should support numbers with all method combinations', async () => { + const schema = f.number().gt(0).lt(100).integer().optional().nullable(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(42)).success).toBe(true); + expect((await schema.forge(101)).success).toBe(false); + expect((await schema.forge(42.5)).success).toBe(false); + }); }); diff --git a/test/object.test.ts b/test/object.test.ts new file mode 100644 index 0000000..6f4360a --- /dev/null +++ b/test/object.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect } from 'vitest'; +import f from '../lib'; + +describe('f.object', () => { + it('should return true for a valid object', async () => { + const schema = f.object({ name: f.string(), age: f.number() }); + const result = await schema.forge({ name: 'John', age: 30 }); + expect(result.success).toBe(true); + }); + + it('should return true for an empty object if model is empty', async () => { + const schema = f.object({}); + const result = await schema.forge({}); + expect(result.success).toBe(true); + }); + + it('should return false for an invalid object', async () => { + const schema = f.object({ name: f.string(), age: f.number() }); + expect( + (await schema.forge({ name: 'John', age: 'invalid' })).success + ).toBe(false); + expect((await schema.forge({ name: 42, age: 30 })).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(123)).success).toBe(false); + expect((await schema.forge([])).success).toBe(false); + }); + + it('should validate nested objects', async () => { + const schema = f.object({ + user: f.object({ name: f.string(), age: f.number() }) + }); + const result = await schema.forge({ + user: { name: 'John', age: 'invalid' } + }); + expect(result.success).toBe(false); + if (!result.success && result.issues) { + expect(result.issues[0].path).toEqual(['user', 'age']); + } + }); + + it('should validate deeply nested objects', async () => { + const schema = f.object({ + user: f.object({ + profile: f.object({ + name: f.string(), + address: f.object({ city: f.string(), zip: f.number() }) + }) + }) + }); + const result = await schema.forge({ + user: { + profile: { + name: 'Jane', + address: { city: 'NY', zip: 'not-a-number' } + } + } + }); + expect(result.success).toBe(false); + if (!result.success && result.issues) { + expect(result.issues[0].path).toEqual([ + 'user', + 'profile', + 'address', + 'zip' + ]); + } + }); + + it('should handle optional objects', async () => { + const schema = f + .object({ name: f.string(), age: f.number() }) + .optional(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge({ name: 'John', age: 30 })).success).toBe( + true + ); + }); + + it('should handle nullable objects', async () => { + const schema = f + .object({ name: f.string(), age: f.number() }) + .nullable(); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge({ name: 'John', age: 30 })).success).toBe( + true + ); + }); + + it('should validate using custom check', async () => { + const schema = f + .object({ name: f.string(), age: f.number() }) + .check((value) => value.age > 18, { + errorMessage: 'Age must be greater than 18' + }); + expect((await schema.forge({ name: 'John', age: 30 })).success).toBe( + true + ); + expect((await schema.forge({ name: 'John', age: 15 })).success).toBe( + false + ); + }); + + it('should support multiple checks in chain', async () => { + const schema = f + .object({ name: f.string(), age: f.number() }) + .check((value) => value.age > 18, { + errorMessage: 'Age must be greater than 18' + }) + .check((value) => value.name.length > 2, { + errorMessage: 'Name too short' + }); + expect((await schema.forge({ name: 'Jo', age: 30 })).success).toBe( + false + ); + expect((await schema.forge({ name: 'John', age: 30 })).success).toBe( + true + ); + }); + + it('should handle optional and nullable objects', async () => { + const schema = f + .object({ name: f.string(), age: f.number() }) + .optional() + .nullable(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge({ name: 'John', age: 30 })).success).toBe( + true + ); + expect((await schema.forge(42)).success).toBe(false); + }); + + it('should validate async check', async () => { + const schema = f.object({ name: f.string(), age: f.number() }).check( + async (value) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return value.age > 18; + }, + { errorMessage: 'Age must be greater than 18' } + ); + const result = await schema.forge({ name: 'John', age: 30 }); + expect(result.success).toBe(true); + const invalidResult = await schema.forge({ name: 'John', age: 15 }); + expect(invalidResult.success).toBe(false); + }); + + it('should support objects with various types', async () => { + const schema = f.object({ + name: f.string(), + age: f.number().optional(), + isActive: f.boolean().nullable(), + tags: f.array(f.string()).optional() + }); + expect( + (await schema.forge({ name: 'John', isActive: null })).success + ).toBe(true); + expect( + ( + await schema.forge({ + name: 'John', + age: 25, + isActive: true, + tags: ['a', 'b'] + }) + ).success + ).toBe(true); + expect( + (await schema.forge({ name: 'John', age: 'bad', isActive: true })) + .success + ).toBe(false); + }); +}); diff --git a/test/string.test.ts b/test/string.test.ts index 174ec2f..eb78bdd 100644 --- a/test/string.test.ts +++ b/test/string.test.ts @@ -1,99 +1,131 @@ import { describe, it, expect } from 'vitest'; - import f from '../lib'; describe('f.string', () => { - it('should return true for a valid string', () => { - expect(f.string().forge('valid').success).toBe(true); + it('should return true for a valid string', async () => { + const schema = f.string(); + const result = await schema.forge('hello'); + expect(result.success).toBe(true); }); - it('should return false for an invalid string', () => { - expect(f.string().forge(42).success).toBe(false); - expect(f.string().forge(undefined).success).toBe(false); - expect(f.string().forge(null).success).toBe(false); - expect(f.string().forge(true).success).toBe(false); + it('should return false for an invalid string', async () => { + const schema = f.string(); + expect((await schema.forge(42)).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge(true)).success).toBe(false); + expect((await schema.forge({})).success).toBe(false); }); - it('should validate minLength', () => { - expect(f.string().minLength(6).forge('short').success).toBe(false); - expect(f.string().minLength(5).forge('hello').success).toBe(true); - expect(f.string().minLength(5).forge('greetings').success).toBe(true); + it('should validate minLength', async () => { + const schema = f.string().minLength(5); + expect((await schema.forge('hi')).success).toBe(false); + expect((await schema.forge('hello')).success).toBe(true); + expect((await schema.forge('greetings')).success).toBe(true); }); - it('should validate maxLength', () => { - expect(f.string().maxLength(5).forge('too long').success).toBe(false); - expect(f.string().maxLength(5).forge('short').success).toBe(true); + it('should validate maxLength', async () => { + const schema = f.string().maxLength(5); + expect((await schema.forge('too long')).success).toBe(false); + expect((await schema.forge('short')).success).toBe(true); }); - it('should validate regExp', () => { - expect( - f - .string() - .regExp(/^valid$/) - .forge('valid').success - ).toBe(true); - expect( - f - .string() - .regExp(/^valid$/) - .forge('invalid').success - ).toBe(false); + it('should validate regExp', async () => { + const schema = f.string().regExp(/^valid$/); + expect((await schema.forge('valid')).success).toBe(true); + expect((await schema.forge('invalid')).success).toBe(false); }); - it('should handle optional strings', () => { - expect(f.string().optional().forge(undefined).success).toBe(true); - expect(f.string().optional().forge(null).success).toBe(false); - expect(f.string().optional().forge('valid').success).toBe(true); + it('should support method chaining', async () => { + const schema = f + .string() + .minLength(3) + .maxLength(5) + .regExp(/^[a-z]+$/); + expect((await schema.forge('abc')).success).toBe(true); + expect((await schema.forge('ab')).success).toBe(false); + expect((await schema.forge('abcdef')).success).toBe(false); + expect((await schema.forge('ABC')).success).toBe(false); }); - it('should handle nullable strings', () => { - expect(f.string().nullable().forge(null).success).toBe(true); - expect(f.string().nullable().forge(undefined).success).toBe(false); - expect(f.string().nullable().forge('valid').success).toBe(true); + it('should handle optional strings', async () => { + const schema = f.string().optional(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(false); + expect((await schema.forge('valid')).success).toBe(true); }); - it('should handle optional and nullable strings', () => { + it('should handle nullable strings', async () => { + const schema = f.string().nullable(); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge(undefined)).success).toBe(false); + expect((await schema.forge('valid')).success).toBe(true); + }); + + it('should handle optional and nullable strings', async () => { const schema = f.string().optional().nullable(); - expect(schema.forge(undefined).success).toBe(true); - expect(schema.forge(null).success).toBe(true); - expect(schema.forge('valid').success).toBe(true); - expect(schema.forge(42).success).toBe(false); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge('valid')).success).toBe(true); + expect((await schema.forge(42)).success).toBe(false); + }); + + it('should validate using custom check', async () => { + const schema = f + .string() + .check((value) => value === value.toUpperCase(), { + errorMessage: 'Must be uppercase' + }); + expect((await schema.forge('VALID')).success).toBe(true); + expect((await schema.forge('invalid')).success).toBe(false); }); - it('should validate using custom check', () => { - const isUpperCase = (value: string) => value === value.toUpperCase(); - expect( - f - .string() - .check(isUpperCase, { errorMessage: 'Must be uppercase' }) - .forge('VALID').success - ).toBe(true); - expect( - f - .string() - .check(isUpperCase, { errorMessage: 'Must be uppercase' }) - .forge('invalid').success - ).toBe(false); + it('should support multiple checks in chain', async () => { + const schema = f + .string() + .check((value) => value.length > 2, { errorMessage: 'Too short' }) + .check((value) => value === value.toUpperCase(), { + errorMessage: 'Must be uppercase' + }); + expect((await schema.forge('AB')).success).toBe(false); + expect((await schema.forge('ABC')).success).toBe(true); + expect((await schema.forge('abc')).success).toBe(false); }); it('should validate async forge', async () => { - const result = await f.string().forgeAsync('valid'); + const schema = f.string(); + const result = await schema.forge('valid'); expect(result.success).toBe(true); }); - it('should validate async check with forgeAsync', async () => { + it('should validate async check', async () => { const schema = f.string().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 400)); + await new Promise((resolve) => setTimeout(resolve, 10)); return value === value.toUpperCase(); }, { errorMessage: 'String must be uppercase' } ); - - const result = await schema.forgeAsync('VALID'); + const result = await schema.forge('VALID'); expect(result.success).toBe(true); - - const invalidResult = await schema.forgeAsync('invalid'); + const invalidResult = await schema.forge('invalid'); expect(invalidResult.success).toBe(false); }); + + it('should support strings with all method combinations', async () => { + const schema = f + .string() + .minLength(2) + .maxLength(4) + .regExp(/^[a-z]+$/) + .optional() + .nullable(); + expect((await schema.forge(undefined)).success).toBe(true); + expect((await schema.forge(null)).success).toBe(true); + expect((await schema.forge('ab')).success).toBe(true); + expect((await schema.forge('abcd')).success).toBe(true); + expect((await schema.forge('a')).success).toBe(false); + expect((await schema.forge('abcde')).success).toBe(false); + expect((await schema.forge('AB')).success).toBe(false); + }); }); From 287750205fcc9ffa587866a5ee8a2199f462bc3c Mon Sep 17 00:00:00 2001 From: Esaul Franco Date: Thu, 17 Jul 2025 19:01:01 -0600 Subject: [PATCH 11/11] Increase timeout for async checks in tests to 100ms for improved reliability --- test/TESTING_GUIDELINES.md | 1 + test/array.test.ts | 2 +- test/boolean.test.ts | 2 +- test/number.test.ts | 2 +- test/object.test.ts | 2 +- test/string.test.ts | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/TESTING_GUIDELINES.md b/test/TESTING_GUIDELINES.md index d277886..f6f5d7c 100644 --- a/test/TESTING_GUIDELINES.md +++ b/test/TESTING_GUIDELINES.md @@ -82,3 +82,4 @@ - Always ensure tests are deterministic and do not rely on external factors. - Use mock data where necessary. - Keep tests isolated and independent of each other. +- For mocked Promises - setTimeout should resolve within 100-150ms. diff --git a/test/array.test.ts b/test/array.test.ts index a8e1924..f38215d 100644 --- a/test/array.test.ts +++ b/test/array.test.ts @@ -92,7 +92,7 @@ describe('f.array', () => { it('should validate async check', async () => { const schema = f.array(f.string()).check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 100)); return value.length > 0; }, { errorMessage: 'Array must not be empty' } diff --git a/test/boolean.test.ts b/test/boolean.test.ts index 5adc723..339ffd6 100644 --- a/test/boolean.test.ts +++ b/test/boolean.test.ts @@ -61,7 +61,7 @@ describe('f.boolean', () => { it('should validate async check', async () => { const schema = f.boolean().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 100)); return value === true; }, { errorMessage: 'Value must be true' } diff --git a/test/number.test.ts b/test/number.test.ts index 19adb16..d3c76d7 100644 --- a/test/number.test.ts +++ b/test/number.test.ts @@ -87,7 +87,7 @@ describe('f.number', () => { it('should validate async check', async () => { const schema = f.number().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 100)); return value > 0; }, { errorMessage: 'Number must be positive' } diff --git a/test/object.test.ts b/test/object.test.ts index 6f4360a..01b49c0 100644 --- a/test/object.test.ts +++ b/test/object.test.ts @@ -136,7 +136,7 @@ describe('f.object', () => { it('should validate async check', async () => { const schema = f.object({ name: f.string(), age: f.number() }).check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 100)); return value.age > 18; }, { errorMessage: 'Age must be greater than 18' } diff --git a/test/string.test.ts b/test/string.test.ts index eb78bdd..6f3fdd7 100644 --- a/test/string.test.ts +++ b/test/string.test.ts @@ -101,7 +101,7 @@ describe('f.string', () => { it('should validate async check', async () => { const schema = f.string().check( async (value) => { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 100)); return value === value.toUpperCase(); }, { errorMessage: 'String must be uppercase' }