Skip to content

Commit

Permalink
Merge pull request #95 from seasonedcc/remove-error-messages-for-schema
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removes `errorMessagesForSchema` utility
  • Loading branch information
gustavoguichard committed Aug 10, 2023
2 parents 7fbd343 + 90a3fbe commit ad4169c
Show file tree
Hide file tree
Showing 3 changed files with 1 addition and 242 deletions.
23 changes: 0 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ It does this by enforcing the parameters' types at runtime (through [zod](https:
- [Other error constructors](#other-error-constructors)
- [Using error messages in the UI](#using-error-messages-in-the-ui)
- [errorMessagesFor](#errormessagesfor)
- [errorMessagesForSchema](#errormessagesforschema)
- [Tracing](#tracing)
- [Combining domain functions](#combining-domain-functions)
- [all](#all)
Expand Down Expand Up @@ -286,28 +285,6 @@ errorMessagesFor(result.inputErrors, 'email') // will be an empty array: []
errorMessagesFor(result.environmentErrors, 'host')[0] === 'Must not be empty'
```

#### errorMessagesForSchema

Given an array of `SchemaError` -- be it from `inputErrors` or `environmentErrors` -- and a Zod Schema, `errorMessagesForSchema` returns an object with a list of error messages for each key in the schema's shape.

```tsx
const schema = z.object({ email: z.string().nonEmpty(), password: z.string().nonEmpty() })
const result = {
success: false,
errors: [],
inputErrors: [{ message: 'Must not be empty', path: ['email'] }, { message: 'Must be a string', path: ['email'] }, { message: 'Must not be empty', path: ['password'] }],
environmentErrors: []
}

errorMessagesForSchema(result.inputErrors, schema)
/*
{
email: ['Must not be empty', 'Must be a string'],
password: ['Must not be empty']
}
*/
```

### Tracing

Whenever you need to intercept inputs and a domain function result without changing them, there is a function called `trace` that can help you.
Expand Down
162 changes: 1 addition & 161 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { describe, it } from 'https://deno.land/std@0.156.0/testing/bdd.ts'
import { assertEquals } from 'https://deno.land/std@0.117.0/testing/asserts.ts'
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'

import { makeDomainFunction } from './constructor.ts'

import {
errorMessagesFor,
errorMessagesForSchema,
schemaError,
} from './errors.ts'
import { errorMessagesFor, schemaError } from './errors.ts'

const errors = [
{ path: ['a'], message: 'a' },
Expand Down Expand Up @@ -39,156 +32,3 @@ describe('errorMessagesFor', () => {
assertEquals(errorMessagesFor(err, 'person.0.email'), ['Invalid email'])
})
})

const schema = z.object({
a: z.string(),
b: z.string(),
})
describe('errorMessagesForSchema', () => {
it('returns an object with error messages for every key of the given schema', () => {
assertEquals(errorMessagesForSchema(errors, schema), {
a: ['a'],
b: ['b', 'c'],
})
})

it('has type inference for the results of this function', () => {
assertEquals(errorMessagesForSchema(errors, schema).a, ['a'])
})

it('handles nested data errors', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: 'c2 bar',
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.string()),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: { c2: ['Expected array, received string'] },
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})

it('handles nested data errors inside arrays of strings', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: ['c2 bar', 6, { foo: 'test' }],
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.string()),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: {
c2: {
'1': ['Expected string, received number'],
'2': ['Expected string, received object'],
},
},
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})

it('handles nested data errors inside arrays of objects', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: ['c2 bar', { c2a: 'test' }, { c2a: 1 }],
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.object({ c2a: z.number() })),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: {
c2: {
'0': ['Expected object, received string'],
'1': { c2a: ['Expected number, received string'] },
},
},
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})
})
58 changes: 0 additions & 58 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
import type {
ErrorWithMessage,
SchemaError,
Expand Down Expand Up @@ -59,62 +58,6 @@ function errorMessagesFor(errors: SchemaError[], name: string) {
.map(({ message }) => message)
}

type NestedErrors<SchemaType> = {
[Property in keyof SchemaType]: string[] | NestedErrors<SchemaType[Property]>
}

function errorMessagesForSchema<T extends z.ZodTypeAny>(
errors: SchemaError[],
_schema: T,
): NestedErrors<z.infer<T>> {
type SchemaType = z.infer<T>
type ErrorObject = { path: string[]; messages: string[] }

const nest = (
{ path, messages }: ErrorObject,
root: Record<string, unknown>,
) => {
const [head, ...tail] = path
root[head] =
tail.length === 0
? messages
: nest(
{ path: tail, messages },
(root[head] as Record<string, unknown>) ?? {},
)
return root
}

const compareStringArrays = (a: string[]) => (b: string[]) =>
JSON.stringify(a) === JSON.stringify(b)

const toErrorObject = (errors: SchemaError[]): ErrorObject[] =>
errors.map(({ path, message }) => ({
path,
messages: [message],
}))

const unifyPaths = (errors: SchemaError[]) =>
toErrorObject(errors).reduce((memo, error) => {
const comparePath = compareStringArrays(error.path)
const mergeErrorMessages = ({ path, messages }: ErrorObject) =>
comparePath(path)
? { path, messages: [...messages, ...error.messages] }
: { path, messages }
const existingPath = memo.find(({ path }) => comparePath(path))

return existingPath ? memo.map(mergeErrorMessages) : [...memo, error]
}, [] as ErrorObject[])

const errorTree = unifyPaths(errors).reduce((memo, schemaError) => {
const errorBranch = nest(schemaError, memo)

return { ...memo, ...errorBranch }
}, {}) as NestedErrors<SchemaType>

return errorTree
}

/**
* A custom error class for input errors.
* @example
Expand Down Expand Up @@ -186,7 +129,6 @@ class ResultError extends Error {

export {
errorMessagesFor,
errorMessagesForSchema,
schemaError,
toErrorWithMessage,
InputError,
Expand Down

0 comments on commit ad4169c

Please sign in to comment.