From 9597cf1e4afd9589773b36958e5d5bd2043a8270 Mon Sep 17 00:00:00 2001 From: Simone Date: Sat, 8 Jun 2024 16:07:25 +0100 Subject: [PATCH] Add support for ULID validation --- src/defaults.ts | 1 + src/schema/string/main.ts | 9 +++++++ src/schema/string/rules.ts | 13 ++++++++++ src/vine/helpers.ts | 19 +++++++++++++++ tests/unit/rules/string.spec.ts | 41 ++++++++++++++++++++++++++++++++ tests/unit/schema/string.spec.ts | 6 +++++ 6 files changed, 89 insertions(+) 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/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(),