diff --git a/src/utils.ts b/src/utils.ts index 557e9e52..1a0099df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -146,10 +146,10 @@ export function* run( } } - let valid = true + let status: 'valid' | 'not_refined' | 'not_valid' = 'valid' for (const failure of struct.validator(value, ctx)) { - valid = false + status = 'not_valid' yield [failure, undefined] } @@ -163,7 +163,7 @@ export function* run( for (const t of ts) { if (t[0]) { - valid = false + status = t[0].refinement != null ? 'not_refined' : 'not_valid' yield [t[0], undefined] } else if (coerce) { v = t[1] @@ -181,14 +181,14 @@ export function* run( } } - if (valid) { + if (status !== 'not_valid') { for (const failure of struct.refiner(value as T, ctx)) { - valid = false + status = 'not_refined' yield [failure, undefined] } } - if (valid) { + if (status === 'valid') { yield [undefined, value as T] } } diff --git a/test/api/validate.ts b/test/api/validate.ts index f1e6590e..cfce0dae 100644 --- a/test/api/validate.ts +++ b/test/api/validate.ts @@ -1,5 +1,13 @@ import { deepStrictEqual, strictEqual } from 'assert' -import { validate, string, StructError, define, refine, object } from '../..' +import { + validate, + string, + StructError, + define, + refine, + object, + any, +} from '../..' describe('validate', () => { it('valid as helper', () => { @@ -93,4 +101,40 @@ describe('validate', () => { B.validate({ a: null }) deepStrictEqual(order, ['validator', 'refiner']) }) + + it('refiners even if nested refiners fail', () => { + let ranOuterRefiner = false + + const A = refine(any(), 'A', () => { + return 'inner refiner failed' + }) + + const B = refine(object({ a: A }), 'B', () => { + ranOuterRefiner = true + return true + }) + + const [error] = B.validate({ a: null }) + // Collect all failures. Ensures all validation runs. + error?.failures() + strictEqual(ranOuterRefiner, true) + }) + + it('skips refiners if validators return errors', () => { + let ranRefiner = false + + const A = define('A', () => { + return false + }) + + const B = refine(object({ a: A }), 'B', () => { + ranRefiner = true + return true + }) + + const [error] = B.validate({ a: null }) + // Collect all failures. Ensures all validation runs. + error?.failures() + strictEqual(ranRefiner, false) + }) }) diff --git a/test/validation/refine/invalid-multiple-refinements.ts b/test/validation/refine/invalid-multiple-refinements.ts new file mode 100644 index 00000000..1e5e11dc --- /dev/null +++ b/test/validation/refine/invalid-multiple-refinements.ts @@ -0,0 +1,41 @@ +import { string, refine, object } from '../../..' + +const PasswordValidator = refine(string(), 'MinimumLength', (pw) => + pw.length >= 8 ? true : 'required minimum length of 8' +) +const changePasswordStruct = object({ + newPassword: PasswordValidator, + confirmPassword: string(), +}) + +export const Struct = refine( + changePasswordStruct, + 'PasswordsDoNotMatch', + (values) => { + return values.newPassword === values.confirmPassword + ? true + : 'Passwords do not match' + } +) + +export const data = { + newPassword: '1234567', + confirmPassword: '123456789', +} + +export const failures = [ + { + value: data.newPassword, + type: 'string', + refinement: 'MinimumLength', + path: ['newPassword'], + branch: [data, data.newPassword], + }, + { + value: data, + type: 'object', + refinement: 'PasswordsDoNotMatch', + path: [], + branch: [data], + }, +]