From c71191e295fd452d84466a58bd72f340e446be39 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 28 Aug 2021 17:44:25 -0700 Subject: [PATCH] fix: Remove schemas from union() call. --- optimal/CHANGELOG.md | 1 + optimal/src/schemas/union.ts | 9 +- optimal/tests/optimal.test.ts | 61 ++++---- optimal/tests/schemas/union.test.ts | 209 +++++++++++++--------------- optimal/tests/typings.ts | 26 ++-- 5 files changed, 146 insertions(+), 160 deletions(-) diff --git a/optimal/CHANGELOG.md b/optimal/CHANGELOG.md index 1cb7b9b..09f5e76 100644 --- a/optimal/CHANGELOG.md +++ b/optimal/CHANGELOG.md @@ -12,6 +12,7 @@ changelog will use the new verbiage, but may affect previous APIs. - Updated `func()` to not be nullable by default. Instead uses `undefined`. - Updated `instance()` to no longer accept a schema as an argument, use `instance().of()` instead. - Updated `object()` to no longer accept a schema as an argument, use `object().of()` instead. +- Updated `union()` to no longer accept a list of schemas as an argument, use `union().of()` instead. - Renamed `Schema#nonNullable()` method to `notNullable()`. - Removed `Schema#key()` method. - Removed `Schema#message()` method. diff --git a/optimal/src/schemas/union.ts b/optimal/src/schemas/union.ts index e14a97c..d5ababc 100644 --- a/optimal/src/schemas/union.ts +++ b/optimal/src/schemas/union.ts @@ -1,15 +1,16 @@ import { createSchema } from '../createSchema'; import { commonCriteria, unionCriteria } from '../criteria'; -import { AnySchema, CommonCriterias, InferNullable, Schema } from '../types'; +import { AnySchema, CommonCriterias, Schema } from '../types'; export interface UnionSchema extends Schema, CommonCriterias> { never: () => UnionSchema; notNullable: () => UnionSchema>; nullable: () => UnionSchema; - of: (schemas: AnySchema[]) => UnionSchema>; + // Distribute these types in the future. Currently breaks on nulls... + of: (schemas: AnySchema[]) => UnionSchema; } -export function union(schemas: AnySchema[], defaultValue: T): UnionSchema { +export function union(defaultValue: T): UnionSchema { return createSchema>({ criteria: { ...commonCriteria, ...unionCriteria }, defaultValue, @@ -17,5 +18,5 @@ export function union(schemas: AnySchema[], defaultValue: T): Union validateType() { // What to do here? }, - }).of(schemas) as UnionSchema; + }); } diff --git a/optimal/tests/optimal.test.ts b/optimal/tests/optimal.test.ts index 2a85c9e..f121e23 100644 --- a/optimal/tests/optimal.test.ts +++ b/optimal/tests/optimal.test.ts @@ -21,12 +21,15 @@ describe('Optimal', () => { // This blueprint is based on Webpack's configuration: https://webpack.js.org/configuration/ // Webpack provides a pretty robust example of how to use this library. - const primitive = union([string(), number(), bool()], false); + const primitive = union(false).of([string(), number(), bool()]); - const condition = union( - [string(), func(), regex(), array().of(regex()), object().of(regex())], - '', - ); + const condition = union('').of([ + string(), + func(), + regex(), + array().of(regex()), + object().of(regex()), + ]); const rule = shape({ enforce: string('post').oneOf<'post' | 'pre'>(['pre', 'post']), @@ -36,16 +39,13 @@ describe('Optimal', () => { parser: object().of(bool()), resource: condition, use: array().of( - union( - [ - string(), - shape({ - loader: string(), - options: object(primitive), - }), - ], - [], - ), + union([]).of([ + string(), + shape({ + loader: string(), + options: object(primitive), + }), + ]), ), }); @@ -65,32 +65,30 @@ describe('Optimal', () => { const blueprint = { context: string(process.cwd()), - entry: union( - [ + entry: union([]) + .of([ string(), array().of(string()), - object().of(union([string(), array().of(string())], '')), + object().of(union('').of([string(), array().of(string())])), func(), - ], - [], - ).nullable(), + ]) + .nullable(), output: shape({ chunkFilename: string('[id].js'), chunkLoadTimeout: number(120_000), - crossOriginLoading: union( - [ - bool(false).only(), - string('anonymous').oneOf(['anonymous', 'use-credentials']), - ], - false, - ), + crossOriginLoading: union(false).of([ + bool(false).only(), + string('anonymous').oneOf(['anonymous', 'use-credentials']), + ]), filename: string('bundle.js'), hashFunction: string('md5').oneOf(['md5', 'sha256', 'sha512']), path: string(), publicPath: string(), }), module: shape({ - noParse: union([regex(), array().of(regex()), func()], null).nullable(), + noParse: union(null) + .of([regex(), array().of(regex()), func()]) + .nullable(), rules: array().of(rule), }), resolve: shape({ @@ -111,7 +109,10 @@ describe('Optimal', () => { ]), watch: bool(false), node: object().of( - union([bool(), string('mock').oneOf(['mock', 'empty'])], false), + union(false).of([ + bool(), + string('mock').oneOf(['mock', 'empty']), + ]), ), }; diff --git a/optimal/tests/schemas/union.test.ts b/optimal/tests/schemas/union.test.ts index 07d3301..dcb1c8d 100644 --- a/optimal/tests/schemas/union.test.ts +++ b/optimal/tests/schemas/union.test.ts @@ -29,32 +29,26 @@ describe('union()', () => { let schema: UnionSchema; beforeEach(() => { - schema = union( - [ + schema = union('baz').of([ + array().of(string()), + bool(true).onlyTrue(), + number().between(0, 5), + instance().of(Foo), + object().of(number()), + string('foo').oneOf(['foo', 'bar', 'baz']), + ]); + }); + + runCommonTests( + (defaultValue) => + union(defaultValue!).of([ array().of(string()), bool(true).onlyTrue(), number().between(0, 5), instance().of(Foo), object().of(number()), string('foo').oneOf(['foo', 'bar', 'baz']), - ], - 'baz', - ); - }); - - runCommonTests( - (defaultValue) => - union( - [ - array().of(string()), - bool(true).onlyTrue(), - number().between(0, 5), - instance().of(Foo), - object().of(number()), - string('foo').oneOf(['foo', 'bar', 'baz']), - ], - defaultValue!, - ), + ]), 'baz', { defaultValue: 1, @@ -63,7 +57,8 @@ describe('union()', () => { it('errors if a unsupported type is used', () => { expect(() => { - union([bool(), number(), string()], 0) + union(0) + .of([bool(), number(), string()]) // @ts-expect-error Invalid type .validate({}); }).toThrowErrorMatchingInlineSnapshot(`"Value must be one of: boolean, number, string."`); @@ -71,13 +66,16 @@ describe('union()', () => { it('errors if a nested union is used', () => { expect(() => { - union([string('foo'), union([number(), bool()], [])], []).validate([]); + union([]) + .of([string('foo'), union([]).of([number(), bool()])]) + .validate([]); }).toThrowErrorMatchingInlineSnapshot(`"Nested unions are not supported."`); }); it('errors with the class name for instance checks', () => { expect(() => { - union([number(), instance().of(Buffer)], 0) + union(0) + .of([number(), instance().of(Buffer)]) // @ts-expect-error Invalid type .validate(new Foo()); }).toThrowErrorMatchingInlineSnapshot(` @@ -110,17 +108,16 @@ describe('union()', () => { it('runs custom check', () => { expect(() => { - union( - [ + union(0) + .of([ string(), custom((value) => { if (typeof value === 'number') { throw new TypeError('Encountered a number!'); } }, ''), - ], - 0, - ).validate(123); + ]) + .validate(123); }).toThrowErrorMatchingInlineSnapshot(` "Value must be one of: string, custom. Received number with the following invalidations: - Encountered a number!" @@ -156,16 +153,15 @@ describe('union()', () => { it('runs shape check', () => { expect(() => { - union( - [ + union({ foo: '', bar: 0 }) + .of([ shape({ foo: string(), bar: number(), }), - ], - { foo: '', bar: 0 }, + ]) // @ts-expect-error Invalid type - ).validate({ foo: 123 }); + .validate({ foo: 123 }); }).toThrowErrorMatchingInlineSnapshot(` "Value must be one of: shape<{ foo: string, bar: number }>. Received object/shape with the following invalidations: - Invalid field \\"foo\\". Must be a string." @@ -183,11 +179,10 @@ describe('union()', () => { it('runs tuple check', () => { expect(() => { - union( - [tuple<['foo', 'bar', 'baz']>([string(), string(), string()])], - ['foo', 'bar', 'baz'], + union(['foo', 'bar', 'baz']) + .of([tuple<['foo', 'bar', 'baz']>([string(), string(), string()])]) // @ts-expect-error Invalid type - ).validate([1]); + .validate([1]); }).toThrowErrorMatchingInlineSnapshot(` "Value must be one of: tuple. Received array/tuple with the following invalidations: - Invalid field \\"[0]\\". Must be a string." @@ -201,7 +196,10 @@ describe('union()', () => { }); it('supports multiple array schemas', () => { - const arrayUnion = union([array().of(string()), array().of(number())], []); + const arrayUnion = union([]).of([ + array().of(string()), + array().of(number()), + ]); expect(() => { // @ts-expect-error Invalid type @@ -222,10 +220,10 @@ describe('union()', () => { }); it('supports multiple object schemas', () => { - const objectUnion = union>( - [object().of(string()), object().of(number())], - {}, - ); + const objectUnion = union>({}).of([ + object().of(string()), + object().of(number()), + ]); expect(() => { // @ts-expect-error Invalid type @@ -246,10 +244,10 @@ describe('union()', () => { }); it('supports arrays and tuples correctly', () => { - const mixedUnion = union<[string, number][] | string[]>( - [array().of(string()), array().of(tuple<[string, number]>([string(), number()]))], - [], - ); + const mixedUnion = union<[string, number][] | string[]>([]).of([ + array().of(string()), + array().of(tuple<[string, number]>([string(), number()])), + ]); expect(mixedUnion.validate(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); @@ -265,27 +263,21 @@ describe('union()', () => { }); it('supports very complex nested unions', () => { - const options = union([bool(), object()], {}); - const complexUnion = union( - [ - // 'foo' - // ['foo', true] - // ['foo', {}] - array().of( - union( - [ - string().notEmpty(), - tuple<[string, boolean | object]>([string().notEmpty(), options]), - ], - '', - ), - ), - // foo: true - // foo: {} - object().of(options).notNullable(), - ], - {}, - ); + const options = union({}).of([bool(), object()]); + const complexUnion = union({}).of([ + // 'foo' + // ['foo', true] + // ['foo', {}] + array().of( + union('').of([ + string().notEmpty(), + tuple<[string, boolean | object]>([string().notEmpty(), options]), + ]), + ), + // foo: true + // foo: {} + object().of(options).notNullable(), + ]); // array only expect(() => complexUnion.validate([])).not.toThrow(); @@ -322,17 +314,14 @@ describe('union()', () => { }); it('supports object and shape schemas in parallel', () => { - const mixedUnion = union>( - [ - shape({ - foo: string(), - bar: number(), - baz: bool(), - }).exact(), - object().of(string()), - ], - {}, - ); + const mixedUnion = union>({}).of([ + shape({ + foo: string(), + bar: number(), + baz: bool(), + }).exact(), + object().of(string()), + ]); expect(() => { // @ts-expect-error Invalid type @@ -371,17 +360,14 @@ describe('union()', () => { }); it('returns shapes as their full objects', () => { - const shapesUnion = union>( - [ - shape({ - foo: string().required(), - bar: number(), - baz: bool(), - }).exact(), - object().of(number()), - ], - {}, - ); + const shapesUnion = union>({}).of([ + shape({ + foo: string().required(), + bar: number(), + baz: bool(), + }).exact(), + object().of(number()), + ]); expect(shapesUnion.validate({})).toEqual({}); // @ts-expect-error Mixed types @@ -394,18 +380,15 @@ describe('union()', () => { }); it('returns an array of shapes as their full objects', () => { - const arrayShapesUnion = union( - [ - array().of( - shape({ - foo: string(), - bar: number(), - baz: bool(), - }).exact(), - ), - ], - [], - ); + const arrayShapesUnion = union([]).of([ + array().of( + shape({ + foo: string(), + bar: number(), + baz: bool(), + }).exact(), + ), + ]); expect(arrayShapesUnion.validate([])).toEqual([]); // @ts-expect-error Partial types are allowed? @@ -428,28 +411,30 @@ describe('union()', () => { ]); }); - describe('type()', () => { - it('returns list of types', () => { - expect(schema.type()).toBe( - 'array | boolean | number | Foo | object | string', - ); - }); - }); - - describe('validateType()', () => { + describe('of()', () => { it('errors if a non-array is passed', () => { expect(() => { // @ts-expect-error Invalid type - union(123); + union('').of(123); }).toThrow('A non-empty array of schemas are required for a union.'); }); it('errors if an empty array is passed', () => { expect(() => { - union([], ''); + union('').of([]); }).toThrow('A non-empty array of schemas are required for a union.'); }); + }); + + describe('type()', () => { + it('returns list of types', () => { + expect(schema.type()).toBe( + 'array | boolean | number | Foo | object | string', + ); + }); + }); + describe('validateType()', () => { it('errors if an invalid value is passed', () => { expect(() => { schema.validate('not a whitelisted string'); diff --git a/optimal/tests/typings.ts b/optimal/tests/typings.ts index 51ce2dd..357482a 100644 --- a/optimal/tests/typings.ts +++ b/optimal/tests/typings.ts @@ -279,38 +279,36 @@ const unions: { } = optimal( {}, { - a: union([string(), bool(), number()], ''), - an: union([string(), bool(), number()], '').nullable(), - ac: union( - [ + a: union('').of([string(), bool(), number()]), + an: union('').of([string(), bool(), number()]).nullable(), + ac: union(null) + .of([ array().of(object().of(string())), object().of(func()), shape({ a: bool(), b: instance().of(Foo), }), - ], - null, - ), + ]) + .nullable(), }, ); const unionsInferred = optimal( {}, { - a: union([string(), bool(), number()], ''), - an: union([string(), bool(), number()], '').nullable(), - ac: union( - [ + a: union('').of([string(), bool(), number()]), + an: union('').of([string(), bool(), number()]).nullable(), + ac: union(null) + .of([ array().of(object().of(string())), object().of(func()), shape({ a: bool(), b: instance().of(Foo), }), - ], - null, - ), + ]) + .nullable(), }, );