From 02d3a28048cfb96f227ee2d69a79be3087e9403e Mon Sep 17 00:00:00 2001 From: simoneNEMO <119299377+simoneNEMO@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:40:45 +0100 Subject: [PATCH] feat: add support for ULID validation (#58) * Add support for ULID validation * Add integration and helper tests --- src/defaults.ts | 1 + src/schema/string/main.ts | 9 +++++ src/schema/string/rules.ts | 13 ++++++ src/vine/helpers.ts | 19 +++++++++ tests/integration/schema/string.spec.ts | 54 +++++++++++++++++++++++++ tests/unit/helpers.spec.ts | 6 +++ tests/unit/rules/string.spec.ts | 41 +++++++++++++++++++ tests/unit/schema/string.spec.ts | 6 +++ 8 files changed, 149 insertions(+) create mode 100644 tests/integration/schema/string.spec.ts diff --git a/src/defaults.ts b/src/defaults.ts index c77e05a..d0ef160 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -40,6 +40,7 @@ export const messages = { 'notIn': 'The selected {{ field }} is invalid', 'ipAddress': 'The {{ field }} field must be a valid IP address', 'uuid': 'The {{ field }} field must be a valid UUID', + 'ulid': 'The {{ field }} field must be a valid ULID', 'hexCode': 'The {{ field }} field must be a valid hex color code', 'boolean': 'The value must be a boolean', diff --git a/src/schema/string/main.ts b/src/schema/string/main.ts index b0fe14b..98e1d19 100644 --- a/src/schema/string/main.ts +++ b/src/schema/string/main.ts @@ -23,6 +23,7 @@ import { urlRule, jwtRule, uuidRule, + ulidRule, trimRule, ibanRule, alphaRule, @@ -66,6 +67,7 @@ export class VineString extends BaseLiteralType { url: urlRule, iban: ibanRule, uuid: uuidRule, + ulid: ulidRule, trim: trimRule, email: emailRule, alpha: alphaRule, @@ -327,6 +329,13 @@ export class VineString extends BaseLiteralType { return this.use(uuidRule(...args)) } + /** + * Validates the value to be a valid ULID + */ + ulid() { + return this.use(ulidRule()) + } + /** * Validates the value contains ASCII characters only */ diff --git a/src/schema/string/rules.ts b/src/schema/string/rules.ts index 707b11b..9b5b701 100644 --- a/src/schema/string/rules.ts +++ b/src/schema/string/rules.ts @@ -592,6 +592,19 @@ export const uuidRule = createRule<{ version?: (1 | 2 | 3 | 4 | 5)[] } | undefin } ) +/** + * Validates the value to be a valid ULID + */ +export const ulidRule = createRule((value, _, field) => { + if (!field.isValid) { + return + } + + if (!helpers.isULID(value as string)) { + field.report(messages.ulid, 'ulid', field) + } +}) + /** * Validates the value contains ASCII characters only */ diff --git a/src/vine/helpers.ts b/src/vine/helpers.ts index 0140a8f..7068dcd 100644 --- a/src/vine/helpers.ts +++ b/src/vine/helpers.ts @@ -35,6 +35,8 @@ import type { FieldContext } from '../types.js' const BOOLEAN_POSITIVES = ['1', 1, 'true', true, 'on'] const BOOLEAN_NEGATIVES = ['0', 0, 'false', false] +const ULID = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/ + /** * Collection of helpers used across the codebase to coerce * and type-check values from HTML forms. @@ -228,6 +230,23 @@ export const helpers = { 'US', ] as const, + /** + * Check if the value is a valid ULID + */ + isULID(value: unknown): boolean { + if (typeof value !== 'string') { + return false + } + + // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' + // https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings + if (value[0] > '7') { + return false + } + + return ULID.test(value) + }, + /** * Check if the value is a valid color hexcode */ diff --git a/tests/integration/schema/string.spec.ts b/tests/integration/schema/string.spec.ts new file mode 100644 index 0000000..b9c0da5 --- /dev/null +++ b/tests/integration/schema/string.spec.ts @@ -0,0 +1,54 @@ +/* + * @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('String', () => { + test('fail when value is not a string', async ({ assert }) => { + const schema = vine.object({ + name: vine.string(), + }) + + const data = { name: 42 } + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'name', + message: 'The name field must be a string', + rule: 'string', + }, + ]) + }) + + test('fail when value is not a valid ULID', async ({ assert }) => { + const schema = vine.object({ + id: vine.string().ulid(), + }) + + const data = { id: '01J0TMIXKWW62H0BKGQ984AS' } + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'id', + message: 'The id field must be a valid ULID', + rule: 'ulid', + }, + ]) + }) + + test('pass when value is a valid ULID', async ({ assert }) => { + const schema = vine.object({ + id: vine.string().ulid(), + }) + + const data = { id: '01J0TMSK8WMJSTX1T2633GFA4G' } + await assert.validationOutput(vine.validate({ schema, data }), { + id: '01J0TMSK8WMJSTX1T2633GFA4G', + }) + }) +}) diff --git a/tests/unit/helpers.spec.ts b/tests/unit/helpers.spec.ts index 0a6db4c..d9638a3 100644 --- a/tests/unit/helpers.spec.ts +++ b/tests/unit/helpers.spec.ts @@ -160,4 +160,10 @@ test.group('Vine Helpers', () => { assert.isTrue(vine.helpers.isPostalCode('69200', 'FR')) assert.isTrue(vine.helpers.isMobilePhone('0612345678', 'fr-FR')) }) + + test('check if the value is a ULID', ({ assert }) => { + assert.isTrue(vine.helpers.isULID('01J0TSV6ZP6VTAHWZ7A7SYDBM2')) + assert.isTrue(vine.helpers.isULID('01j0tsv6zp6vtahwz7a7sydbm2')) + assert.isFalse(vine.helpers.isULID('01J0TSV6ZP6VTAHWZIL7A7SYDB')) + }) }) diff --git a/tests/unit/rules/string.spec.ts b/tests/unit/rules/string.spec.ts index 8913641..e081cb3 100644 --- a/tests/unit/rules/string.spec.ts +++ b/tests/unit/rules/string.spec.ts @@ -36,6 +36,7 @@ import { passportRule, postalCodeRule, uuidRule, + ulidRule, asciiRule, ibanRule, jwtRule, @@ -1288,6 +1289,46 @@ test.group('String | uuid', () => { .run(stringRuleValidator) }) +test.group('String | ulid', () => { + test('validate {value}') + .with([ + { + errorsCount: 1, + rule: ulidRule(), + value: 22, + error: 'The dummy field must be a string', + }, + { + errorsCount: 1, + rule: ulidRule(), + value: 22, + bail: false, + error: 'The dummy field must be a string', + }, + { + errorsCount: 1, + rule: ulidRule(), + value: '1999010301', + error: 'The dummy field must be a valid ULID', + }, + { + rule: ulidRule(), + value: '01HZW62CR5FNVW4PSXVXC1HTZF', + }, + { + rule: ulidRule(), + value: '7ZZZZZZZZZZZZZZZZZZZZZZZZZ', + }, + { + errorsCount: 1, + rule: ulidRule(), + value: '80000000000000000000000000', + error: 'The dummy field must be a valid ULID', + }, + ]) + .run(stringRuleValidator) +}) + test.group('String | ascii', () => { test('validate {value}') .with([ diff --git a/tests/unit/schema/string.spec.ts b/tests/unit/schema/string.spec.ts index ad7c34b..661f148 100644 --- a/tests/unit/schema/string.spec.ts +++ b/tests/unit/schema/string.spec.ts @@ -38,6 +38,7 @@ import { passportRule, postalCodeRule, uuidRule, + ulidRule, asciiRule, ibanRule, jwtRule, @@ -667,6 +668,11 @@ test.group('VineString | applying rules', () => { schema: vine.string().uuid(), rule: uuidRule(), }, + { + name: 'ulid', + schema: vine.string().ulid(), + rule: ulidRule(), + }, { name: 'ascii', schema: vine.string().ascii(),