From 81aa8685d900b2a58166b924827c6f0627c5b3dd Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Sun, 28 Jan 2024 01:01:44 +0100 Subject: [PATCH] feat: BigInt support --- src/schema/index.ts | 24 +++++ src/types.ts | 23 +++++ src/validations/index.ts | 2 + src/validations/primitives/bigint.ts | 50 ++++++++++ src/validations/primitives/number.ts | 5 + tests/schema.spec.ts | 110 ++++++++++++++++++++ tests/validations/bigint.spec.ts | 144 +++++++++++++++++++++++++++ 7 files changed, 358 insertions(+) create mode 100644 src/validations/primitives/bigint.ts create mode 100644 tests/validations/bigint.spec.ts diff --git a/src/schema/index.ts b/src/schema/index.ts index cbbfc51..f2b8065 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -20,6 +20,7 @@ import { StringType, ObjectType, NumberType, + BigIntType, EnumSetType, BooleanType, TypedSchema, @@ -284,6 +285,28 @@ oneOf.nullableAndOptional = function nullableAndOptionalEnum(enumOptions: any[], > } +/** + * BigInt schema type + */ +function bigint(rules?: Rule[]) { + return getLiteralType('bigint', false, false, undefined, rules || []) as ReturnType +} +bigint.optional = function optionalBigInt(rules?: Rule[]) { + return getLiteralType('bigint', true, false, undefined, rules || []) as ReturnType< + BigIntType['optional'] + > +} +bigint.nullable = function nullableBigInt(rules?: Rule[]) { + return getLiteralType('bigint', false, true, undefined, rules || []) as ReturnType< + BigIntType['nullable'] + > +} +bigint.nullableAndOptional = function nullableAndOptionalBigInt(rules?: Rule[]) { + return getLiteralType('bigint', true, true, undefined, rules || []) as ReturnType< + BigIntType['nullableAndOptional'] + > +} + /** * Enum set schema type */ @@ -365,6 +388,7 @@ export const schema: Schema = { date, object, array, + bigint, enum: oneOf as unknown as EnumType, enumSet: enumSet as unknown as EnumSetType, file, diff --git a/src/types.ts b/src/types.ts index 6ea616d..fe1008c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -523,6 +523,28 @@ export type EnumSetReturnValue = ? Options[number][] : never +/** + * BigInt schema types + */ +export interface BigIntType { + (rules?: Rule[]): { + t: bigint + getTree(): SchemaLiteral + } + optional(rules?: Rule[]): { + t?: bigint + getTree(): SchemaLiteral + } + nullable(rules?: Rule[]): { + t: bigint | null + getTree(): SchemaLiteral + } + nullableAndOptional(rules?: Rule[]): { + t?: bigint | null + getTree(): SchemaLiteral + } +} + /** * Signature to define an enum type. We accept a static list of enum * values or a ref that is resolved lazily. @@ -644,6 +666,7 @@ export interface Schema { boolean: BooleanType number: NumberType date: DateType + bigint: BigIntType enum: EnumType enumSet: EnumSetType object: ObjectType diff --git a/src/validations/index.ts b/src/validations/index.ts index 780623b..572befe 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -43,6 +43,7 @@ import { file } from './primitives/file.js' import { number } from './primitives/number.js' import { object } from './primitives/object.js' import { string } from './primitives/string.js' +import { bigint } from './primitives/bigint.js' import { alpha } from './string/alpha.js' import { alphaNum } from './string/alpha_num.js' @@ -70,6 +71,7 @@ const validations = { beforeOrEqual, beforeField, beforeOrEqualToField, + bigint, confirmed, required, nullable, diff --git a/src/validations/primitives/bigint.ts b/src/validations/primitives/bigint.ts new file mode 100644 index 0000000..4bbd60b --- /dev/null +++ b/src/validations/primitives/bigint.ts @@ -0,0 +1,50 @@ +/* + * @adonisjs/validator + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { SyncValidation } from '../../types.js' +import { wrapCompile } from '../../validator/helpers.js' + +const DEFAULT_MESSAGE = 'bigint validation failed' +const RULE_NAME = 'bigint' + +/** + * Ensure the value is a valid bigint. Numeric string will be casted + * to valid bigint + */ +export const bigint: SyncValidation = { + compile: wrapCompile(RULE_NAME), + validate(value, _, { mutate, errorReporter, pointer, arrayExpressionPointer }) { + if (typeof value === 'bigint') { + return + } + + /** + * Report error when value is not a bigint and neither a string or a number + */ + if (typeof value !== 'string' && typeof value !== 'number') { + errorReporter.report(pointer, RULE_NAME, DEFAULT_MESSAGE, arrayExpressionPointer) + return + } + + /** + * Attempt to cast string or number to a bigint. In case of + * failure report the validation error + */ + try { + const castedValue = BigInt(value) + + /** + * Mutate the value + */ + mutate(castedValue) + } catch (e) { + errorReporter.report(pointer, RULE_NAME, DEFAULT_MESSAGE, arrayExpressionPointer) + } + }, +} diff --git a/src/validations/primitives/number.ts b/src/validations/primitives/number.ts index 80782cc..c707512 100644 --- a/src/validations/primitives/number.ts +++ b/src/validations/primitives/number.ts @@ -42,6 +42,11 @@ export const number: SyncValidation = { return } + if (castedValue === Number.POSITIVE_INFINITY || castedValue === Number.NEGATIVE_INFINITY) { + errorReporter.report(pointer, RULE_NAME, DEFAULT_MESSAGE, arrayExpressionPointer) + return + } + /** * Mutate the value */ diff --git a/tests/schema.spec.ts b/tests/schema.spec.ts index da7b79e..d3bae81 100644 --- a/tests/schema.spec.ts +++ b/tests/schema.spec.ts @@ -647,6 +647,116 @@ test.group('Schema | Date', () => { }) }) +test.group('Schema | BigInt', () => { + test('define schema with bigint rule', ({ assert }) => { + assert.deepEqual( + schema.create({ + username: schema.bigint(), + }).tree, + { + username: { + type: 'literal', + subtype: 'bigint', + optional: false, + nullable: false, + rules: [ + { + name: 'required', + allowUndefineds: true, + async: false, + compiledOptions: [], + }, + { + name: 'bigint', + allowUndefineds: false, + async: false, + compiledOptions: [], + }, + ], + }, + } + ) + }) + + test('define schema with optional bigint rule', ({ assert }) => { + assert.deepEqual( + schema.create({ + username: schema.bigint.optional(), + }).tree, + { + username: { + type: 'literal', + subtype: 'bigint', + optional: true, + nullable: false, + rules: [ + { + name: 'bigint', + allowUndefineds: false, + async: false, + compiledOptions: [], + }, + ], + }, + } + ) + }) + + test('define schema with nullable bigint rule', ({ assert }) => { + assert.deepEqual( + schema.create({ + username: schema.bigint.nullable(), + }).tree, + { + username: { + type: 'literal', + subtype: 'bigint', + optional: false, + nullable: true, + rules: [ + { + name: 'nullable', + allowUndefineds: true, + async: false, + compiledOptions: [], + }, + { + name: 'bigint', + allowUndefineds: false, + async: false, + compiledOptions: [], + }, + ], + }, + } + ) + }) + + test('define schema with both optional and nullable bigint rule', ({ assert }) => { + assert.deepEqual( + schema.create({ + username: schema.bigint.nullableAndOptional(), + }).tree, + { + username: { + type: 'literal', + subtype: 'bigint', + optional: true, + nullable: true, + rules: [ + { + name: 'bigint', + allowUndefineds: false, + async: false, + compiledOptions: [], + }, + ], + }, + } + ) + }) +}) + test.group('Schema | Enum', () => { test('define schema with enum rule', ({ assert }) => { assert.deepEqual( diff --git a/tests/validations/bigint.spec.ts b/tests/validations/bigint.spec.ts new file mode 100644 index 0000000..c5a4afb --- /dev/null +++ b/tests/validations/bigint.spec.ts @@ -0,0 +1,144 @@ +/* + * @adonisjs/validator + * + * (c) Harminder Virk + * + * 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 { rules } from '../../src/rules/index.js' +import { validate } from '../fixtures/rules/index.js' +import { MessagesBag } from '../../src/messages_bag/index.js' +import { ApiErrorReporter } from '../../src/error_reporter/api.js' +import { bigint } from '../../src/validations/primitives/bigint.js' + +function compile() { + // @ts-ignore + return bigint.compile('literal', 'bigint', rules['bigint']().options, {}) +} + +test.group('BigInt', () => { + validate(bigint, test, 'helloworld', 10n, compile()) + + test('work fine when value is near Infinity', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + bigint.validate( + '-3177777777777777777777777777777777777777777777777777777777777777777777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999991111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111', + compile().compiledOptions, + { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: () => {}, + } + ) + + assert.deepEqual(reporter.toJSON(), { errors: [] }) + }) + + test('report error when value is not a valid bigint', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + bigint.validate(null, compile().compiledOptions, { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: () => {}, + }) + + assert.deepEqual(reporter.toJSON(), { + errors: [ + { + field: 'age', + rule: 'bigint', + message: 'bigint validation failed', + }, + ], + }) + }) + + test('cast bigint like string to a valid bigint', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value: any = '21' + + bigint.validate(value, compile().compiledOptions, { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => { + value = newValue + }, + }) + + assert.deepEqual(reporter.toJSON(), { errors: [] }) + assert.equal(value, 21n) + }) + + test('work fine when value is a valid bigint', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + bigint.validate(21n, compile().compiledOptions, { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: () => {}, + }) + + assert.deepEqual(reporter.toJSON(), { errors: [] }) + }) + + test('report error when value is a string that cannot be casted to a bigint', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + bigint.validate('hello-world', compile().compiledOptions, { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: () => {}, + }) + + assert.deepEqual(reporter.toJSON(), { + errors: [ + { + field: 'age', + rule: 'bigint', + message: 'bigint validation failed', + }, + ], + }) + }) + + test('cast number to a valid bigint', ({ assert }) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value: any = 21 + + bigint.validate(value, compile().compiledOptions, { + errorReporter: reporter, + field: 'age', + pointer: 'age', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => { + value = newValue + }, + }) + + assert.deepEqual(reporter.toJSON(), { errors: [] }) + assert.equal(value, 21n) + }) +})