Skip to content

Commit

Permalink
feat(validathor): add array schema
Browse files Browse the repository at this point in the history
  • Loading branch information
Kosai106 committed Jul 30, 2024
1 parent 831ebc3 commit cbf67f5
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 25 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const exampleSchema = v.object({
v.min(new Date('2021/01/01')),
v.max(new Date()),
]),
tags: v.array(string())
});

// If the input data matches the schema, nothing will happen,
Expand All @@ -47,7 +48,8 @@ try {
size: 2048
},
acceptedTerms: true,
createdAt: new Date('01/08/2023')
createdAt: new Date('01/08/2023'),
tags: ['tag1', 'tag2', 'tag3']
}
);
} catch(err) {
Expand Down
37 changes: 16 additions & 21 deletions packages/validathor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ValidaThor is a lightweight library that allows you to validate your data using

### Features

- Define custom schema shapes using `object`, `string`, `number`, `boolean`, and `date` types
- Define custom schema shapes using `array`, `object`, `string`, `number`, `boolean`, and `date` types
- Use modifiers (e.g., `min`, `max`, `email`, or even custom modifiers) to add constraints to your schema
- Validate input data against your defined schema

Expand All @@ -42,35 +42,29 @@ Then, import the library and start defining your schemas and modifiers!
Here's a basic example of how you can use ValidaThor to validate some input data:

```ts
import {
// Core
parse,
// Schemas
object, string, number, boolean, date,
// Modifiers
min, max, email,
} from '@nordic-ui/validathor';
import * as v from '@nordic-ui/validathor';

// Define your schema shape
const exampleSchema = object({
name: string([min(2)]),
age: number([min(13), max(100)]),
email: string([email()]),
avatar: object({
path: string(),
size: number(),
const exampleSchema = v.object({
name: v.string([v.min(2)]),
age: v.number([v.min(13), v.max(100)]),
email: v.string([v.email()]),
avatar: v.object({
path: v.string(),
size: v.number(),
}),
acceptedTerms: boolean(),
createdAt: date([
min(new Date('2021/01/01')),
max(new Date()),
acceptedTerms: v.boolean(),
createdAt: v.date([
v.min(new Date('2021/01/01')),
v.max(new Date()),
]),
tags: v.array(string())
});

// If the input data matches the schema, nothing will happen,
// Otherwise an error will be thrown to help the user
try {
parse(
v.parse(
exampleSchema,
{
name: 'John Doe',
Expand All @@ -79,6 +73,7 @@ try {
avatar: { path: 'https://placekeanu.com/200/200', size: 2048 },
acceptedTerms: true,
createdAt: new Date('01/08/2023'),
tags: ['tag1', 'tag2', 'tag3']
}
);
} catch (err) {
Expand Down
74 changes: 74 additions & 0 deletions packages/validathor/src/schemas/__tests__/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { parse } from '@/core/parse'
import { boolean, string, number, object } from '@/schemas'

import { array } from '../array'

describe('array()', () => {
it('should be named correctly', () => {
// @ts-expect-error - We are just testing the name here, so the schema can be empty
expect(array().name).toEqual('array')
})

it('should work with no modifiers', () => {
const schema1 = array(string())
const schema2 = array(number())
const schema3 = array(boolean())

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(schema1, [])).toEqual([])
expect(parse(schema2, [])).toEqual([])
expect(parse(schema3, [])).toEqual([])
})

// TODO: modifiers are not yet implemented
it.skip('should work with modifiers', () => {
const schema1 = array(string(), [])
const schema2 = array(number(), [])
const schema3 = array(boolean(), [])

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(schema1, [])).toThrowError()
expect(() => parse(schema2, [])).toThrowError()
expect(() => parse(schema3, [])).toThrowError()
})

it('should work', () => {
const schema = array(object({ name: string(), age: number(), isAdmin: boolean() }))

expect(
parse(schema, [
{ name: 'John Doe', age: 34, isAdmin: true },
{ name: 'Obi-Wan Kenobi', age: 38, isAdmin: true },
]),
).toEqual([
{ name: 'John Doe', age: 34, isAdmin: true },
{ name: 'Obi-Wan Kenobi', age: 38, isAdmin: true },
])
})

it('should fail', () => {
const schema1 = array(object({ name: string() }))
const schema2 = array(object({ age: number() }))

expect(() => parse(schema1, 'fail')).toThrowError(new TypeError('Value must be an array'))
expect(() => parse(schema1, [{ foo: 123 }])).toThrowError(new TypeError('Expected a string'))
expect(() => parse(schema2, [{ foo: 'foo' }])).toThrowError(new TypeError('Expected a number'))
})

it('should also fail', () => {
const schema = array(object({ name: string(), age: number(), isAdmin: boolean() }))

expect(() =>
parse(schema, [
{ name: 'John Doe', age: 34, isAdmin: true },
{ name: 'Obi-Wan Kenobi', age: 38, isAdmin: 'false' },
]),
).toThrowError(new TypeError('Expected a boolean'))
})
})
38 changes: 35 additions & 3 deletions packages/validathor/src/schemas/__tests__/object.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 } from '@/schemas'
import { boolean, string, number, array } from '@/schemas'

import { enum_ } from '../enum'
import { object } from '../object'
Expand Down Expand Up @@ -45,6 +45,7 @@ describe('object()', () => {
lat: number(),
lng: number(),
}),
eventIds: array(number([min(1)])),
}),
})

Expand All @@ -55,7 +56,11 @@ describe('object()', () => {
})
expect(
parse(schema3, {
venue: { name: 'Petit Bain', location: { lat: 48.8355263, lng: 2.3741375 } },
venue: {
name: 'Petit Bain',
location: { lat: 48.8355263, lng: 2.3741375 },
eventIds: [4, 8, 15, 16, 23, 42],
},
}),
).toEqual({
venue: {
Expand All @@ -64,6 +69,7 @@ describe('object()', () => {
lat: 48.8355263,
lng: 2.3741375,
},
eventIds: [4, 8, 15, 16, 23, 42],
},
})
expect(schema1.parse({ name: 'John', age: 31, isAdmin: false })).toEqual({
Expand All @@ -73,7 +79,11 @@ describe('object()', () => {
})
expect(
schema3.parse({
venue: { name: 'Petit Bain', location: { lat: 48.8355263, lng: 2.3741375 } },
venue: {
name: 'Petit Bain',
location: { lat: 48.8355263, lng: 2.3741375 },
eventIds: [4, 8, 15, 16, 23, 42],
},
}),
).toEqual({
venue: {
Expand All @@ -82,6 +92,7 @@ describe('object()', () => {
lat: 48.8355263,
lng: 2.3741375,
},
eventIds: [4, 8, 15, 16, 23, 42],
},
})

Expand All @@ -106,16 +117,25 @@ describe('[FUTURE]', () => {
const getUserResponseSchema = object({
id: number(),
name: string([min(1)]),
obj: object({
foo: object({ bar: string() }),
}),
})

expect(
getUserResponseSchema.parse({
id: 1,
name: 'John',
obj: {
foo: { bar: 'baz' },
},
}),
).toEqual({
id: 1,
name: 'John',
obj: {
foo: { bar: 'baz' },
},
})
})

Expand Down Expand Up @@ -150,25 +170,37 @@ describe('[FUTURE]', () => {
VITE_ENABLE_DEV_TOOLS: 'false',
}),
).toThrowError('Expected a boolean')

expect(() =>
envSchema.parse({
VITE_ENVIRONMENT: 'fake',
VITE_API_BASE_URL: 'http://localhost:3000',
VITE_ENABLE_MOCK_APIS: true,
VITE_ENABLE_DEV_TOOLS: false,
}),
).toThrowError('Expected a valid value')
})

it('should do more stuff', () => {
const addProductFormSchema = object({
name: string([min(1), max(50)]),
description: string([min(2), max(50)]),
price: number([min(2), max(100)]),
imageIds: array(number([min(1)])),
})

expect(
addProductFormSchema.parse({
name: 'Foo',
description: 'Bar',
price: 10,
imageIds: [1, 2, 3],
}),
).toEqual({
name: 'Foo',
description: 'Bar',
price: 10,
imageIds: [1, 2, 3],
})
})
})
26 changes: 26 additions & 0 deletions packages/validathor/src/schemas/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Custom, Max, Min } from '@/modifiers'
import type { Parser } from '@/types'
import { assert, TypeError } from '@/utils'
import { ERROR_CODES } from '@/utils/errors/errorCodes'

/** An array of the accepted modifier types */
export type ArraySchemaModifiers = Array<Min<string> | Max<string> | Custom<string>>

export const array = <T extends Parser<unknown>>(
schema: T,
/** @todo implement */
modifiers?: ArraySchemaModifiers,
message?: {
type_error?: string
},
): Parser<T[]> => ({
name: 'array' as const,
parse: (value: unknown): T[] => {
assert(
Array.isArray(value),
new TypeError(message?.type_error || ERROR_CODES.ERR_VAL_8000.message()),
)

return value.map((item) => schema.parse(item)) as T[]
},
})
1 change: 1 addition & 0 deletions packages/validathor/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { array } from './array'
export { boolean } from './boolean'
export { date } from './date'
export { number } from './number'
Expand Down

0 comments on commit cbf67f5

Please sign in to comment.