Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(lib): Improve types #21

Merged
merged 3 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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