Skip to content

Commit

Permalink
Merge pull request #34 from paperhive/feature/discrimiated-union
Browse files Browse the repository at this point in the history
add discriminatedUnion()
  • Loading branch information
andrenarchy authored Apr 28, 2021
2 parents 89790e0 + f78838e commit d8aa360
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ Returns a validator that returns `value` if it is a Date and returns an error ot
Options:
* `options.min?: Date`, `options.max?: Date`: restrict date
### `discriminatedUnion(key, definition, options?): Validator<ObjectResult<D>>`
Returns a validator that returns `value` if:
* it is an object and
* the `value[key]` is a key of `definition`
* `value` (without `key`) passes the validation as specified in `definition[key]`.
Otherwise it returns an error. A new object is returned that has the results of the validator functions as values.
Options: see `object()`.
### `enumerate(value1, value2, ...): Validator<value1 | value2 | ...>`
Returns a validator that returns `value` if if equals one of the strings `value1`, `value2`, .... and returns an error otherwise.
Expand Down
67 changes: 67 additions & 0 deletions src/discriminated-union.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { assert } from 'chai'

import { branchError, leafError } from './errors'
import { failure, success } from './result'
import { discriminatedUnion } from './discriminated-union'
import { number } from './number'
import { optional } from './object'

describe('discriminatedUnion()', () => {
const validateVehicle = discriminatedUnion('type', {
person: { age: number(), height: optional(number()) },
robot: { battery: number() },
})

it('should return an error if not an object', () =>
assert.deepStrictEqual(
validateVehicle(1),
failure(leafError(1, 'Not an object.'))
))

it('should return an error if type does not exist.', () => {
const value = { type: 'cat', age: 12 }
assert.deepStrictEqual(
validateVehicle(value),
failure(
branchError(value, [
{ key: 'type', error: leafError('cat', 'Not one of person, robot.') },
])
)
)
})

it('should return an error if key does not exist for type.', () => {
const value = { type: 'robot', age: 12 }
assert.deepStrictEqual(
validateVehicle(value),
failure(leafError(value, 'Properties not allowed: age.'))
)
})

it('should return an error if key does not validate.', () => {
const value = { type: 'person', age: 'foo' }
assert.deepStrictEqual(
validateVehicle(value),
failure(
branchError(value, [
{ key: 'age', error: leafError('foo', 'Not a number.') },
])
)
)
})

it('should validate a person and a robot', () => {
assert.deepStrictEqual(
validateVehicle({ type: 'person', age: 2 }),
success({ type: 'person' as const, age: 2 })
)
assert.deepStrictEqual(
validateVehicle({ type: 'person', age: 2, height: 180 }),
success({ type: 'person' as const, age: 2, height: 180 })
)
assert.deepStrictEqual(
validateVehicle({ type: 'robot', battery: 99 }),
success({ type: 'robot' as const, battery: 99 })
)
})
})
50 changes: 50 additions & 0 deletions src/discriminated-union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { enumerate } from './enumerate'
import { FefeError } from './errors'
import { object, ObjectDefinition, ObjectOptions, ObjectResult } from './object'
import { isFailure } from './result'
import { Validator } from './transformer'

export type DiscriminatedUnionDefinition = Record<string, ObjectDefinition>

export type DiscriminatedUnionResult<
K extends string,
D extends DiscriminatedUnionDefinition
> = {
[k in keyof D]: ObjectResult<D[k]> & { [l in K]: k }
}[keyof D]

export function discriminatedUnion<
K extends string,
D extends DiscriminatedUnionDefinition
>(
key: K,
definition: D,
options: ObjectOptions = {}
): Validator<DiscriminatedUnionResult<K, D>, FefeError> {
const prevalidate = object(
{
[key]: enumerate(...Object.keys(definition)),
},
{ allowExcessProperties: true }
) as Validator<{ [l in K]: keyof D }>

const validators = Object.fromEntries(
Object.entries(definition).map(([k, v]) => [
k,
object(
{
[key]: enumerate(k),
...v,
},
options
),
])
) as { [k in keyof D]: Validator<ObjectResult<D[k]> & { [l in K]: k }> }

return (value: unknown) => {
const prevalidated = prevalidate(value)
if (isFailure(prevalidated)) return prevalidated
const v = prevalidated.right[key] as keyof D
return validators[v](value)
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './transformer'
export * from './array'
export * from './boolean'
export * from './date'
export * from './discriminated-union'
export * from './enumerate'
export * from './map-object-keys'
export * from './number'
Expand Down

0 comments on commit d8aa360

Please sign in to comment.