From be6ffc8aecb685245f756f54d261d41100ba4340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Gaul?= Date: Wed, 28 Apr 2021 23:57:39 +0200 Subject: [PATCH 1/2] add discriminatedUnion() --- README.md | 12 +++++- src/discriminated-union.test.ts | 67 +++++++++++++++++++++++++++++++++ src/discriminated-union.ts | 50 ++++++++++++++++++++++++ src/index.ts | 1 + 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/discriminated-union.test.ts create mode 100644 src/discriminated-union.ts diff --git a/README.md b/README.md index 07cb6c8..99106e5 100644 --- a/README.md +++ b/README.md @@ -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>` + +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` Returns a validator that returns `value` if if equals one of the strings `value1`, `value2`, .... and returns an error otherwise. @@ -258,7 +268,7 @@ Options: ### `object(definition, options?): Validator>` -Returns a validator that returns `value` if it is an object and all values pass the validation as specified in `definition`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values. +Returns a validator that returns a copy of `value` if it is an object and all values pass the validation as specified in `definition`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values. Options: * `definition: ObjectDefinition`: an object where each value is a `Validator`. diff --git a/src/discriminated-union.test.ts b/src/discriminated-union.test.ts new file mode 100644 index 0000000..387fb30 --- /dev/null +++ b/src/discriminated-union.test.ts @@ -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 }) + ) + }) +}) diff --git a/src/discriminated-union.ts b/src/discriminated-union.ts new file mode 100644 index 0000000..e88b995 --- /dev/null +++ b/src/discriminated-union.ts @@ -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 + +export type DiscriminatedUnionResult< + K extends string, + D extends DiscriminatedUnionDefinition +> = { + [k in keyof D]: ObjectResult & { [l in K]: k } +}[keyof D] + +export function discriminatedUnion< + K extends string, + D extends DiscriminatedUnionDefinition +>( + key: K, + definition: D, + options: ObjectOptions = {} +): Validator, 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 & { [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) + } +} diff --git a/src/index.ts b/src/index.ts index 71f2c9b..2ee2d2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' From f78838e50524470fde7745f5cf6ea64efc81348b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Gaul?= Date: Wed, 28 Apr 2021 23:58:59 +0200 Subject: [PATCH 2/2] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99106e5..420d31b 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ Options: ### `object(definition, options?): Validator>` -Returns a validator that returns a copy of `value` if it is an object and all values pass the validation as specified in `definition`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values. +Returns a validator that returns `value` if it is an object and all values pass the validation as specified in `definition`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values. Options: * `definition: ObjectDefinition`: an object where each value is a `Validator`.