Skip to content

Commit

Permalink
feat(validathor): cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Kosai106 committed Oct 12, 2024
1 parent 395b893 commit 36bd210
Show file tree
Hide file tree
Showing 15 changed files with 129 additions and 57 deletions.
2 changes: 1 addition & 1 deletion packages/validathor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export { max } from '@/modifiers/max'
export { min } from '@/modifiers/min'

// Utils
export { assert, TypeError, ValidationError } from '@/utils'
export { assert, assertType, TypeError, ValidationError } from '@/utils'

// Types
export type { Parser, Modifier } from '@/types'
2 changes: 1 addition & 1 deletion packages/validathor/src/modifiers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type Custom<T> = {
validate: (value: T) => T
}

type CustomAssertions<T> = (value: T) => Array<[boolean | (() => boolean), string | Error]>
type CustomAssertions<T> = (value: T) => [boolean | (() => boolean), string | Error][]

export const custom = <T = unknown>(assertions: CustomAssertions<T>): Custom<T> => {
return {
Expand Down
20 changes: 3 additions & 17 deletions packages/validathor/src/modifiers/enumerator.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
import { assert } from '@/utils/assert/assert'
import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'
import { TypeError } from '@/utils/errors/errors'

export type Enumerator<T> = {
name: 'enumerator'
validate: (value: T) => T
}

// Overload definitions
export function enumerator(
input: Array<string>,
message?: {
type_error?: string
error?: string
},
): Enumerator<string>

export function enumerator(
input: Array<number>,
message?: {
type_error?: string
error?: string
},
): Enumerator<number>
export function enumerator(input: string[]): Enumerator<string>
export function enumerator(input: number[]): Enumerator<number>

// TODO: Add support for date and object types maybe?
export function enumerator<T extends string | number>(
Expand Down
25 changes: 23 additions & 2 deletions packages/validathor/src/modifiers/max.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assert } from '@/utils/assert/assert'
import { ERROR_CODES } from '@/utils/errors/errorCodes'
import { TypeError } from '@/utils/errors/errors'

type Input = number | string | Date | (number | string | Date)[]

export type Max<T> = {
name: 'max'
validate: (value: T) => T
Expand All @@ -13,15 +15,18 @@ type ErrorMessages = Partial<{
error: string
}>

export function max<T extends number | string | Date>(
export function max<T extends Input>(
max: T extends Date ? Date : number,
message?: ErrorMessages,
): Max<T> {
return {
name: 'max' as const,
validate: (value: T) => {
assert(
typeof value === 'number' || typeof value === 'string' || value instanceof Date,
typeof value === 'number' ||
typeof value === 'string' ||
value instanceof Date ||
Array.isArray(value),
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_0000.message()),
)

Expand Down Expand Up @@ -79,6 +84,22 @@ export function max<T extends number | string | Date>(
)
}

if (Array.isArray(value)) {
// Type checks
assert(
typeof max === 'number',
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_1000.message()),
)
assert(
isFinite(max),
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_1001.message()),
)

// Validation checks
assert(max >= 0, message?.max_length_error || ERROR_CODES.ERR_VAL_0200.message())
assert(value.length <= max, message?.error || ERROR_CODES.ERR_VAL_8103.message(String(max)))
}

return value
},
}
Expand Down
25 changes: 23 additions & 2 deletions packages/validathor/src/modifiers/min.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assert } from '@/utils/assert/assert'
import { ERROR_CODES } from '@/utils/errors/errorCodes'
import { TypeError } from '@/utils/errors/errors'

type Input = number | string | Date | (number | string | Date)[]

export type Min<T> = {
name: 'min'
validate: (value: T) => T
Expand All @@ -13,15 +15,18 @@ type ErrorMessages = Partial<{
error: string
}>

export function min<T extends number | string | Date>(
export function min<T extends Input>(
min: T extends Date ? Date : number,
message?: ErrorMessages,
): Min<T> {
return {
name: 'min' as const,
validate: (value: T) => {
assert(
typeof value === 'number' || typeof value === 'string' || value instanceof Date,
typeof value === 'number' ||
typeof value === 'string' ||
value instanceof Date ||
Array.isArray(value),
new TypeError(message?.type_error || ERROR_CODES['ERR_TYP_0000'].message()),
)

Expand Down Expand Up @@ -79,6 +84,22 @@ export function min<T extends number | string | Date>(
)
}

if (Array.isArray(value)) {
// Type checks
assert(
typeof min === 'number',
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_1000.message()),
)
assert(
isFinite(min),
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_1001.message()),
)

// Validation checks
assert(min >= 0, message?.min_length_error || ERROR_CODES.ERR_VAL_0100.message())
assert(value.length >= min, message?.error || ERROR_CODES.ERR_VAL_8101.message(String(min)))
}

return value
},
}
Expand Down
4 changes: 2 additions & 2 deletions packages/validathor/src/schemas/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type { Parser } from '@/types'
import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

export type BooleanSchemaModifiers = Array<Custom<boolean>>
export type BooleanSchemaModifiers = Custom<boolean>[]

export const boolean = (
modifiers?: BooleanSchemaModifiers,
modifiers: BooleanSchemaModifiers = [],
message?: {
type_error?: string
},
Expand Down
4 changes: 2 additions & 2 deletions packages/validathor/src/schemas/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type { Parser } from '@/types'
import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

export type DateSchemaModifiers = Array<Min<Date> | Max<Date> | Custom<Date>>
export type DateSchemaModifiers = (Min<Date> | Max<Date> | Custom<Date>)[]

export const date = (
modifiers?: DateSchemaModifiers,
modifiers: DateSchemaModifiers = [],
message?: { type_error?: string },
): Parser<Date> => ({
name: 'date' as const,
Expand Down
6 changes: 3 additions & 3 deletions packages/validathor/src/schemas/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { Parser } from '@/types'
import { assert, TypeError, ValidationError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

export type EnumSchemaModifiers = Array<string | number>
export type EnumSchemaModifiers = (string | number)[]

export const enum_ = (
modifiers?: EnumSchemaModifiers,
modifiers: EnumSchemaModifiers = [],
message?: {
type_error?: string
error?: string | ((value: string | number) => string)
Expand All @@ -19,7 +19,7 @@ export const enum_ = (
)

assert(
(modifiers ?? []).includes(value),
modifiers.includes(value),
new ValidationError(
typeof message?.error === 'function'
? message?.error(value)
Expand Down
11 changes: 7 additions & 4 deletions packages/validathor/src/schemas/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import type { Parser } from '@/types'
import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

export type NumberSchemaModifiers = Array<
Min<number> | Max<number> | Enumerator<number> | Custom<number>
>
export type NumberSchemaModifiers = (
| Min<number>
| Max<number>
| Enumerator<number>
| Custom<number>
)[]

export const number = (
modifiers?: NumberSchemaModifiers,
modifiers: NumberSchemaModifiers = [],
message?: {
type_error?: string
},
Expand Down
36 changes: 19 additions & 17 deletions packages/validathor/src/schemas/object.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { Parser } from '@/types'
import { assert, TypeError } from '@/utils'
import type { InferSchemaType, Parser } from '@/types'
import { assert, assertType, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

// Helper type to infer the schema shape and convert to the correct types
type InferSchemaType<T> = {
[P in keyof T]: T[P] extends Parser<infer U> ? U : never
}
const isRecord = <T>(input: unknown): input is T =>
input instanceof Object &&
!(input instanceof Array) &&
!(input instanceof Boolean) &&
!(input instanceof Date) &&
!(input instanceof Error) &&
!(input instanceof Function) &&
!(input instanceof Number) &&
!(input instanceof RegExp) &&
!(input instanceof String)

export const object = <T extends Record<string, Parser<unknown>>>(
schema: T,
Expand All @@ -16,23 +22,19 @@ export const object = <T extends Record<string, Parser<unknown>>>(
name: 'object' as const,
parse: (value: unknown): InferSchemaType<T> => {
assert(
value !== null &&
value instanceof Object &&
!(value instanceof Array) &&
!(value instanceof Boolean) &&
!(value instanceof Date) &&
!(value instanceof Error) &&
!(value instanceof Function) &&
!(value instanceof Number) &&
!(value instanceof RegExp) &&
!(value instanceof String),
typeof value !== 'undefined' && value !== null,
new TypeError(ERROR_CODES.ERR_TYP_0001.message()),
)
assertType<Record<string, unknown>>(
value,
isRecord<T>,
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_5000.message()),
)

const result: Record<string, unknown> = {}
Object.entries(schema).forEach(([key, parser]) => {
// Safely use the parser on each property
result[key] = parser.parse((value as Record<string, unknown>)[key])
result[key] = parser.parse(value[key])
})
return result as InferSchemaType<T>
},
Expand Down
12 changes: 8 additions & 4 deletions packages/validathor/src/schemas/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

/** An array of the accepted modifier types */
export type StringSchemaModifiers = Array<
Min<string> | Max<string> | Email | Enumerator<string> | Custom<string>
>
export type StringSchemaModifiers = (
| Min<string>
| Max<string>
| Email
| Enumerator<string>
| Custom<string>
)[]

export const string = (
modifiers?: StringSchemaModifiers,
modifiers: StringSchemaModifiers = [],
message?: {
type_error?: string
},
Expand Down
5 changes: 5 additions & 0 deletions packages/validathor/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export type Parser<T, U = unknown> = {
parse: (input: U) => T
}

// Helper type to infer the schema shape and convert to the correct types
export type InferSchemaType<T> = {
[P in keyof T]: T[P] extends Parser<infer U> ? U : never
}

export type MaybeArray<T> = T | T[]
28 changes: 27 additions & 1 deletion packages/validathor/src/utils/assert/assert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidationError } from '../errors/errors'
import { TypeError, ValidationError } from '../errors/errors'

/**
* Asserts that a condition is true, otherwise throws a ValidationError
Expand All @@ -14,3 +14,29 @@ export function assert(
throw typeof message === 'string' ? new ValidationError(message) : message
}
}

/**
* Type guard function type.
*
* @internal Should not be exported.
*/
type TypeGuard<T> = (input: unknown) => input is T

/**
* Asserts the type of input to be of `T`.
*
* This is preferable to using `as` for type casting.
* @param input - The input to be checked.
* @param guard - The type guard function to validate the input.
* @param message A string or an Error
* @throws {Error} - Will throw an error if the input does not match the type `T`.
*/
export function assertType<T>(
input: unknown,
guard: TypeGuard<T>,
message: string | Error,
): asserts input is T {
if (!guard(input)) {
throw typeof message === 'string' ? new TypeError(message) : message
}
}
4 changes: 4 additions & 0 deletions packages/validathor/src/utils/errors/errorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const TYPE_ERROR_CODE: ErrorCodes<TypeErrorCode> = {
code: 'ERR_TYP_1000',
message: () => 'Expected a valid value',
},
ERR_TYP_0001: {
code: 'ERR_TYP_1000',
message: () => "Value can't be null or undefined",
},

// Number type errors
ERR_TYP_1000: {
Expand Down
2 changes: 1 addition & 1 deletion packages/validathor/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { assert } from './assert/assert'
export { assert, assertType } from './assert/assert'
export { ValidationError, TypeError } from './errors/errors'

0 comments on commit 36bd210

Please sign in to comment.