Skip to content

Commit

Permalink
feat: add support for inline comparison in requiredWhen rule
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Mar 13, 2024
1 parent f9a3c5d commit 718685d
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 18 deletions.
67 changes: 65 additions & 2 deletions src/schema/base/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import type {
FieldOptions,
ParserOptions,
ConstructableSchema,
ComparisonOperators,
ArrayComparisonOperators,
NumericComparisonOperators,
} from '../../types.js'
import { requiredWhen } from './rules.js'
import { helpers } from '../../vine/helpers.js'
Expand Down Expand Up @@ -177,8 +180,68 @@ class OptionalModifier<Schema extends BaseModifiersType<any, any>> extends BaseM
* field as required, or "false" to skip the required
* validation
*/
requiredWhen(callback: (field: FieldContext) => boolean) {
return this.use(requiredWhen(callback))
requiredWhen<Operator extends ComparisonOperators>(
otherField: string,
operator: Operator,
expectedValue: Operator extends ArrayComparisonOperators
? (string | number | boolean)[]
: Operator extends NumericComparisonOperators
? number
: string | number | boolean
): this
requiredWhen(callback: (field: FieldContext) => boolean): this
requiredWhen(
otherField: string | ((field: FieldContext) => boolean),
operator?: ComparisonOperators,
expectedValue?: any
) {
/**
* The equality check if self implemented
*/
if (typeof otherField === 'function') {
return this.use(requiredWhen(otherField))
}

/**
* Creating the checker function based upon the
* operator used for the comparison
*/
let checker: (value: any) => boolean
switch (operator!) {
case '=':
checker = (value) => value === expectedValue
break
case '!=':
checker = (value) => value !== expectedValue
break
case 'in':
checker = (value) => expectedValue.includes(value)
break
case 'notIn':
checker = (value) => !expectedValue.includes(value)
break
case '>':
checker = (value) => value > expectedValue
break
case '<':
checker = (value) => value < expectedValue
break
case '>=':
checker = (value) => value >= expectedValue
break
case '<=':
checker = (value) => value <= expectedValue
}

/**
* Registering rule with custom implementation
*/
return this.use(
requiredWhen((field) => {
const otherFieldValue = helpers.getNestedValue(otherField, field)
return checker(otherFieldValue)
})
)
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,11 @@ export type ValidationOptions<MetaData extends Record<string, any> | undefined>
* Infers the schema type
*/
export type Infer<Schema extends { [OTYPE]: any }> = Schema[typeof OTYPE]

/**
* Comparison operators supported by requiredWhen
* rule
*/
export type NumericComparisonOperators = '>' | '<' | '>=' | '<='
export type ArrayComparisonOperators = 'in' | 'notIn'
export type ComparisonOperators = ArrayComparisonOperators | NumericComparisonOperators | '=' | '!='
189 changes: 175 additions & 14 deletions tests/integration/schema/conditional_required.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import { test } from '@japa/runner'
import vine from '../../../index.js'
import { helpers } from '../../../src/vine/helpers.js'

test.group('requiredIfExists', () => {
test('fail when value is missing but other field exists', async ({ assert }) => {
Expand Down Expand Up @@ -260,16 +259,11 @@ test.group('requiredIfMissingAny', () => {
})
})

test.group('requiredWhen | optional', () => {
test.group('requiredWhen', () => {
test('fail when required field is missing', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine
.string()
.optional()
.requiredWhen((field) => {
return helpers.exists(field.data.game) && field.data.game === 'volleyball'
}),
teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'),
})

const data = {
Expand All @@ -285,15 +279,25 @@ test.group('requiredWhen | optional', () => {
])
})

test('pass when required condition has not been met', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'),
})

const data = {
game: 'handball',
}

await assert.validationOutput(vine.validate({ schema, data }), {
game: 'handball',
})
})

test('pass when required field is defined', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine
.string()
.optional()
.requiredWhen((field) => {
return helpers.exists(field.data.game) && field.data.game === 'volleyball'
}),
teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'),
})

const data = {
Expand All @@ -306,4 +310,161 @@ test.group('requiredWhen | optional', () => {
teamName: 'foo',
})
})

test('compare using "not equal" operator', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine.string().optional().requiredWhen('game', '!=', 'volleyball'),
})

const data = {
game: 'handball',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'teamName',
message: 'The teamName field must be defined',
rule: 'required',
},
])
})

test('compare using "in" operator', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine.string().optional().requiredWhen('game', 'in', ['volleyball']),
})

const data = {
game: 'volleyball',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'teamName',
message: 'The teamName field must be defined',
rule: 'required',
},
])
})

test('compare using "not In" operator', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine.string().optional().requiredWhen('game', 'notIn', ['volleyball']),
})

const data = {
game: 'handball',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'teamName',
message: 'The teamName field must be defined',
rule: 'required',
},
])
})

test('compare using ">" operator', async ({ assert }) => {
const schema = vine.object({
age: vine.number(),
guardianName: vine.string().optional().requiredWhen('age', '>', 1),
})

const data = {
age: 2,
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'guardianName',
message: 'The guardianName field must be defined',
rule: 'required',
},
])
})

test('compare using "<" operator', async ({ assert }) => {
const schema = vine.object({
age: vine.number(),
guardianName: vine.string().optional().requiredWhen('age', '<', 19),
})

const data = {
age: 2,
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'guardianName',
message: 'The guardianName field must be defined',
rule: 'required',
},
])
})

test('compare using "<=" operator', async ({ assert }) => {
const schema = vine.object({
age: vine.number(),
guardianName: vine.string().optional().requiredWhen('age', '<=', 18),
})

const data = {
age: 18,
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'guardianName',
message: 'The guardianName field must be defined',
rule: 'required',
},
])
})

test('compare using ">=" operator', async ({ assert }) => {
const schema = vine.object({
age: vine.number(),
guardianName: vine.string().optional().requiredWhen('age', '>=', 1),
})

const data = {
age: 1,
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'guardianName',
message: 'The guardianName field must be defined',
rule: 'required',
},
])
})

test('compare using custom callback', async ({ assert }) => {
const schema = vine.object({
game: vine.string().optional(),
teamName: vine
.string()
.optional()
.requiredWhen((field) => {
return field.parent.game === 'volleyball'
}),
})

const data = {
game: 'volleyball',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'teamName',
message: 'The teamName field must be defined',
rule: 'required',
},
])
})
})
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"extends": "@adonisjs/tsconfig/tsconfig.package.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "./build",
},
"outDir": "./build"
}
}

0 comments on commit 718685d

Please sign in to comment.