Skip to content

Commit

Permalink
chore(lib): Improve types (#21)
Browse files Browse the repository at this point in the history
* chore(lib): improve types

* chore(lib): correct README
  • Loading branch information
Kosai106 authored Nov 23, 2024
1 parent 53825c5 commit 6e7c355
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const exampleSchema = v.object({
v.min(new Date('2021/01/01')),
v.max(new Date()),
]),
tags: v.array(string())
tags: v.array(v.string())
});

// If the input data matches the schema, nothing will happen,
Expand Down
2 changes: 1 addition & 1 deletion packages/validathor/src/core/validateModifiers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Modifier } from '@/types'

export const validateModifiers = <T>(value: T, modifiers?: Modifier<T>[]) => {
modifiers?.forEach((arg) => arg.validate(value))
modifiers?.forEach((modifier) => modifier.validate(value))
}
21 changes: 21 additions & 0 deletions packages/validathor/src/guards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Parser } from '@/types'

export const isParser = <T>(input: unknown): input is T => {
return input !== null && typeof input === 'object' && 'parse' in input && 'name' in input
}

export const isParserRecords = <T extends Record<string, Parser<unknown>>>(
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)
// && Object.values(input).every((value) => isParser(value))
// 'parse' in input &&
// 'name' in input
2 changes: 1 addition & 1 deletion packages/validathor/src/modifiers/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const email = (
): Email => {
return {
name: 'email' as const,
validate: (value: string) => {
validate: (value) => {
const emailRegex = new RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
// Type checks
assert(
Expand Down
2 changes: 1 addition & 1 deletion packages/validathor/src/modifiers/enumerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function enumerator<T extends string | number>(
): Enumerator<T> {
return {
name: 'enumerator' as const,
validate: (value: T) => {
validate: (value) => {
// Type checks
assert(
input.includes(value),
Expand Down
4 changes: 2 additions & 2 deletions packages/validathor/src/modifiers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { custom, type Custom } from './custom'
export { min, type Min } from './min'
export { max, type Max } from './max'
export { email, type Email } from './email'
export { enumerator, type Enumerator } from './enumerator'
export { max, type Max } from './max'
export { min, type Min } from './min'
2 changes: 1 addition & 1 deletion packages/validathor/src/modifiers/max.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function max<T extends Input>(
): Max<T> {
return {
name: 'max' as const,
validate: (value: T) => {
validate: (value) => {
assert(
typeof value === 'number' ||
typeof value === 'string' ||
Expand Down
2 changes: 1 addition & 1 deletion packages/validathor/src/modifiers/min.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function min<T extends Input>(
): Min<T> {
return {
name: 'min' as const,
validate: (value: T) => {
validate: (value) => {
assert(
typeof value === 'number' ||
typeof value === 'string' ||
Expand Down
35 changes: 30 additions & 5 deletions packages/validathor/src/schemas/__tests__/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parse } from '@/core/parse'
import { max, min } from '@/modifiers'
import { boolean, string, number, object } from '@/schemas'
import { boolean, string, number, object, date } from '@/schemas'

import { array } from '../array'

Expand Down Expand Up @@ -47,14 +47,38 @@ describe('array()', () => {
const schema1 = array(string())
const schema2 = array(number())
const schema3 = array(boolean())
const schema4 = array(date())
const schema5 = array(object({ name: string() }))
const schema6 = array(array([number()]))

expect(parse(schema1, ['hello', 'world'])).toEqual(['hello', 'world'])
expect(parse(schema2, [1, 2, 3])).toEqual([1, 2, 3])
expect(parse(schema3, [true, false])).toEqual([true, false])
expect(
parse(schema4, [new Date('2018-03-06T09:00:00.000Z'), new Date('2024-11-16T17:07:39.128Z')]),
).toEqual([new Date('2018-03-06T09:00:00.000Z'), new Date('2024-11-16T17:07:39.128Z')])
expect(parse(schema5, [{ name: 'Obi-Wan Kenobi' }, { name: 'Anakin Skywalker' }])).toEqual([
{ name: 'Obi-Wan Kenobi' },
{ name: 'Anakin Skywalker' },
])
expect(
parse(schema6, [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]),
).toEqual([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])

expect(parse(schema1, [])).toEqual([])
expect(parse(schema2, [])).toEqual([])
expect(parse(schema3, [])).toEqual([])
expect(parse(schema4, [])).toEqual([])
expect(parse(schema5, [])).toEqual([])
expect(parse(schema6, [])).toEqual([])
})

it('should work with modifiers', () => {
Expand Down Expand Up @@ -101,10 +125,11 @@ describe('array()', () => {
})
})

describe('[FUTURE]', () => {
it.fails('should work with mixed schemas', () => {
const schema = array<string | number>([string(), number()])

describe('[FUTURE] mixed schemas', () => {
it.fails.each([
array<string | number>([string(), number()]),
array<number | string>([number(), string()]),
])('should work with mixed schemas', (schema) => {
expect(parse(schema, ['hello', 'world', 123])).toEqual(['hello', 'world', 123])
expect(() => parse(schema, ['foo', true, 'baz'])).toThrowError(
new TypeError('Value must be at most 2 x long'),
Expand Down
62 changes: 51 additions & 11 deletions packages/validathor/src/schemas/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,72 @@ import { ERROR_CODES } from '@/utils/errors/errorCodes'
/** An array of the accepted modifier types */
export type ArraySchemaModifiers = (Min<number[]> | Max<number[]> | Custom<unknown>)[]

export const array = <T>(
schema: MaybeArray<Parser<T>>,
type AcceptedParserPrimitives =
| string
| number
| boolean
| Date
| object
| AcceptedParserPrimitives[]

type ParserMap = {
string: Parser<string>
number: Parser<number>
boolean: Parser<boolean>
Date: Parser<Date>
object: Parser<object>
Array: Parser<AcceptedParserPrimitives[]>
}

type PrimitiveTypeMap = {
string: 'string'
number: 'number'
boolean: 'boolean'
Date: 'Date'
object: 'object'
}

// Ensure PrimitiveTypeName returns only valid keys of ParserMap
type PrimitiveTypeName<T extends AcceptedParserPrimitives> = T extends keyof PrimitiveTypeMap
? PrimitiveTypeMap[T]
: never

// Now, define the GetParser type function that retrieves the correct parser based on T
type GetParser<T extends AcceptedParserPrimitives> = T extends (infer U)[]
? Parser<U[]>
: PrimitiveTypeName<T> extends keyof ParserMap
? ParserMap[PrimitiveTypeName<T>] extends Parser<infer U>
? Parser<U>
: never
: never

type MixedParser<T extends AcceptedParserPrimitives> = GetParser<T>

export const array = <T extends AcceptedParserPrimitives>(
schema: MaybeArray<MixedParser<T>>,
modifiers: ArraySchemaModifiers = [],
message?: { type_error?: string },
): Parser<T[]> => {
const _schema = Array.isArray(schema) ? schema : [schema]

assert(
Array.isArray(_schema),
new TypeError(message?.type_error || ERROR_CODES.ERR_VAL_8000.message()),
)

return {
name: 'array' as const,
parse: (value: unknown): T[] => {
parse: (value): T[] => {
assert(
Array.isArray(value),
new TypeError(message?.type_error || ERROR_CODES.ERR_VAL_8000.message()),
)

validateModifiers(value, modifiers)

return value.reduce((result: T[], item: unknown) => {
_schema.forEach((s) => result.push(s.parse(item)))
return result
// Use reduce to ensure the type is correctly inferred
const result: T[] = value.reduce((acc: unknown[], item: AcceptedParserPrimitives, index) => {
const parser = _schema[index % _schema.length] // Handle cyclic schema application
acc.push(parser.parse(item))
return acc
}, [])

return result
},
}
}
14 changes: 2 additions & 12 deletions packages/validathor/src/schemas/object.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
import { isParserRecords } from '@/guards'
import type { InferSchemaType, Parser } from '@/types'
import { assert, assertType, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

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,
message?: {
Expand All @@ -27,7 +17,7 @@ export const object = <T extends Record<string, Parser<unknown>>>(
)
assertType<Record<string, unknown>>(
value,
isRecord<T>,
isParserRecords<T>,
new TypeError(message?.type_error || ERROR_CODES.ERR_TYP_5000.message()),
)

Expand Down
5 changes: 1 addition & 4 deletions packages/validathor/src/schemas/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { isParser } from '@/guards'
import type { Parser } from '@/types'
import { assert, assertType, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

const isParser = <T>(input: unknown): input is T => {
return input !== null && typeof input === 'object' && 'parse' in input
}

export const tuple = <T extends Parser<unknown>[]>(
schema: T,
message?: {
Expand Down

0 comments on commit 6e7c355

Please sign in to comment.