Skip to content

Commit

Permalink
feat: finish enum schema and rules
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jun 7, 2023
1 parent 3ac5039 commit e7f6a87
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"dependencies": {
"@poppinss/macroable": "^1.0.0-6",
"@types/validator": "^13.7.17",
"@vinejs/compiler": "^1.2.0",
"@vinejs/compiler": "^1.2.1",
"camelcase": "^7.0.1",
"validator": "^13.9.0"
}
Expand Down
8 changes: 5 additions & 3 deletions src/schema/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { VineLiteral } from './literal/main.js'
import { CamelCase } from './camelcase_types.js'
import { group } from './object/group_builder.js'
import { VineNativeEnum } from './enum/native_enum.js'
import type { EnumLike, SchemaTypes } from '../types.js'
import type { EnumLike, FieldContext, SchemaTypes } from '../types.js'

/**
* Schema builder exposes methods to construct a Vine schema. You may
Expand Down Expand Up @@ -116,10 +116,12 @@ export class SchemaBuilder extends Macroable {
/**
* Define a field whose value matches the enum choices.
*/
enum<const Values extends readonly unknown[]>(values: Values): VineEnum<Values>
enum<const Values extends readonly unknown[]>(
values: Values | ((ctx: FieldContext) => Values)
): VineEnum<Values>
enum<Values extends EnumLike>(values: Values): VineNativeEnum<Values>
enum<Values extends readonly unknown[] | EnumLike>(values: Values): any {
if (Array.isArray(values)) {
if (Array.isArray(values) || typeof values === 'function') {
return new VineEnum(values)
}
return new VineNativeEnum(values as EnumLike)
Expand Down
11 changes: 8 additions & 3 deletions src/schema/enum/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@

import { createRule } from '../../vine/create_rule.js'
import { errorMessages } from '../../defaults.js'
import { FieldContext } from '@vinejs/compiler/types'

/**
* Enum rule is used to validate the field's value to be one
* from the pre-defined choices.
*/
export const enumRule = createRule<{ choices: readonly any[] }>((value, options, ctx) => {
export const enumRule = createRule<{
choices: readonly any[] | ((ctx: FieldContext) => readonly any[])
}>((value, options, ctx) => {
const choices = typeof options.choices === 'function' ? options.choices(ctx) : options.choices

/**
* Report error when value is not part of the pre-defined
* options
*/
if (!options.choices.includes(value)) {
ctx.report(errorMessages.enum, 'enum', ctx, options)
if (!choices.includes(value)) {
ctx.report(errorMessages.enum, 'enum', ctx, { choices })
}
})
2 changes: 1 addition & 1 deletion src/vine/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Vine extends SchemaBuilder {
#options: VineOptions = {
convertEmptyStringsToNull: false,
errorReporter: () => new SimpleErrorReporter(),
messagesProvider: (messages) => new SimpleMessagesProvider(messages, {}),
messagesProvider: (messages, fields) => new SimpleMessagesProvider(messages, fields),
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/schema/boolean.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { test } from '@japa/runner'
import vine from '../../../index.js'

test.group('Boolean', () => {
test('fail when when is not a boolean', async ({ assert }) => {
test('fail when value is not a boolean', async ({ assert }) => {
const schema = vine.object({
is_admin: vine.boolean(),
})
Expand Down
156 changes: 156 additions & 0 deletions tests/integration/schema/enum.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* @vinejs/vine
*
* (c) VineJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

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

test.group('Enum', () => {
test('fail when value is not a subset of choices', async ({ assert }) => {
const schema = vine.object({
role: vine.enum(['admin', 'moderator', 'owner', 'user']),
})

const data = {
role: 'foo',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'role',
message: 'The selected role is invalid',
rule: 'enum',
meta: {
choices: ['admin', 'moderator', 'owner', 'user'],
},
},
])
})

test('pass when value is a subset of choices', async ({ assert }) => {
const schema = vine.object({
role: vine.enum(['admin', 'moderator', 'owner', 'user']),
})

const data = {
role: 'admin',
}

await assert.validationOutput(vine.validate({ schema, data }), {
role: 'admin',
})
})
})

test.group('Enum | Native enum', () => {
test('fail when value is not a subset of choices', async ({ assert }) => {
enum Roles {
ADMIN = 'admin',
MOD = 'moderator',
OWNER = 'owner',
USER = 'user',
}

const schema = vine.object({
role: vine.enum(Roles),
})

const data = {
role: 'foo',
}

await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'role',
message: 'The selected role is invalid',
rule: 'enum',
meta: {
choices: ['admin', 'moderator', 'owner', 'user'],
},
},
])
})

test('pass when value is a subset of choices', async ({ assert }) => {
enum Roles {
ADMIN = 'admin',
MOD = 'moderator',
OWNER = 'owner',
USER = 'user',
}

const schema = vine.object({
role: vine.enum(Roles),
})

const data = {
role: 'admin',
}

await assert.validationOutput(vine.validate({ schema, data }), {
role: 'admin',
})
})
})

test.group('Enum | Lazily compute enum choices', () => {
test('fail when value is not a subset of choices', async ({ assert }) => {
const schema = vine.object({
creative_device: vine.enum(['mobile', 'desktop']),
banner_width: vine.enum((ctx) => {
if (ctx.parent.creative_device === 'mobile') {
return ['320px', '640px'] as const
}

return ['1080px', '1280px'] as const
}),
})

const data = {
creative_device: 'desktop',
banner_width: '640px',
}

await assert.validationErrors(
vine.validate({ schema, data, fields: { banner_width: 'banner width' } }),
[
{
field: 'banner_width',
message: 'The selected banner width is invalid',
rule: 'enum',
meta: {
choices: ['1080px', '1280px'],
},
},
]
)
})

test('pass when value is a subset of choices', async ({ assert }) => {
const schema = vine.object({
creative_device: vine.enum(['mobile', 'desktop']),
banner_width: vine.enum((ctx) => {
if (ctx.parent.creative_device === 'mobile') {
return ['320px', '640px'] as const
}

return ['1080px', '1280px'] as const
}),
})

const data = {
creative_device: 'mobile',
banner_width: '640px',
}

await assert.validationOutput(
vine.validate({ schema, data, fields: { banner_width: 'banner width' } }),
data
)
})
})
8 changes: 8 additions & 0 deletions tests/unit/rules/enum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,12 @@ test.group('Rules | enum', () => {
const validated = new ValidatorFactory().execute(validation, value)
validated.assertErrorsCount(0)
})

test('compute choices from a callback', () => {
const validation = enumRule({ choices: () => ['admin', 'moderator', 'guest'] })
const value = 'admin'

const validated = new ValidatorFactory().execute(validation, value)
validated.assertErrorsCount(0)
})
})
58 changes: 58 additions & 0 deletions tests/unit/schema/enum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ test.group('VineEnum', () => {
],
})
})

test('apply parser', ({ assert }) => {
const schema = vine.enum(['guest', 'admin', 'moderator']).parse(() => {})
assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), {
type: 'literal',
fieldName: '*',
propertyName: '*',
allowNull: false,
isOptional: false,
bail: true,
parseFnId: 'ref://1',
validations: [
{
implicit: false,
isAsync: false,
ruleFnId: 'ref://2',
},
],
})
})
})

test.group('VineEnum | clone', () => {
Expand Down Expand Up @@ -313,4 +333,42 @@ test.group('VineEnum | clone', () => {
],
})
})

test('clone and apply parser', ({ assert }) => {
const schema = vine.enum(['guest', 'admin', 'moderator'])
const schema1 = schema.clone().parse(() => {})

assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), {
type: 'literal',
fieldName: '*',
propertyName: '*',
allowNull: false,
isOptional: false,
bail: true,
parseFnId: undefined,
validations: [
{
implicit: false,
isAsync: false,
ruleFnId: 'ref://1',
},
],
})
assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), {
type: 'literal',
fieldName: '*',
propertyName: '*',
allowNull: false,
isOptional: false,
bail: true,
parseFnId: 'ref://1',
validations: [
{
implicit: false,
isAsync: false,
ruleFnId: 'ref://2',
},
],
})
})
})

0 comments on commit e7f6a87

Please sign in to comment.