From 39ebe7365bc3623b47d3023c2968df4fe3f72fe3 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 4 Jul 2019 10:36:56 +0200 Subject: [PATCH] Extend @kbn/config-schema (#40118) * support null for literal type * support schemas withing right operand of conditional type * add never type to ban usage * remove never options. add when needed * add tests for Reference.isReference --- packages/kbn-config-schema/src/index.ts | 10 ++- .../src/references/reference.test.ts | 61 +++++++++++++++ .../src/references/reference.ts | 2 +- .../conditional_type.test.ts.snap | 8 ++ .../__snapshots__/literal_type.test.ts.snap | 2 + .../__snapshots__/never_type.test.ts.snap | 13 ++++ .../src/types/conditional_type.test.ts | 66 ++++++++++++++++ .../src/types/conditional_type.ts | 9 ++- packages/kbn-config-schema/src/types/index.ts | 1 + .../src/types/literal_type.test.ts | 6 ++ .../src/types/never_type.test.ts | 75 +++++++++++++++++++ .../kbn-config-schema/src/types/never_type.ts | 34 +++++++++ 12 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 packages/kbn-config-schema/src/references/reference.test.ts create mode 100644 packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap create mode 100644 packages/kbn-config-schema/src/types/never_type.test.ts create mode 100644 packages/kbn-config-schema/src/types/never_type.ts diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index d86d4c9dca124..b4308dc3f80d1 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -36,6 +36,7 @@ import { MapOfOptions, MapOfType, MaybeType, + NeverType, NumberOptions, NumberType, ObjectType, @@ -72,7 +73,7 @@ function uri(options?: URIOptions): Type { return new URIType(options); } -function literal(value: T): Type { +function literal(value: T): Type { return new LiteralType(value); } @@ -88,6 +89,10 @@ function duration(options?: DurationOptions): Type { return new DurationType(options); } +function never(): Type { + return new NeverType(); +} + /** * Create an optional type */ @@ -167,7 +172,7 @@ function siblingRef(key: string): SiblingReference { function conditional( leftOperand: Reference, - rightOperand: Reference | A, + rightOperand: Reference | A | Type, equalType: Type, notEqualType: Type, options?: TypeOptions @@ -186,6 +191,7 @@ export const schema = { literal, mapOf, maybe, + never, number, object, oneOf, diff --git a/packages/kbn-config-schema/src/references/reference.test.ts b/packages/kbn-config-schema/src/references/reference.test.ts new file mode 100644 index 0000000000000..e87dfd52f8864 --- /dev/null +++ b/packages/kbn-config-schema/src/references/reference.test.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Reference } from './reference'; +import { schema } from '../'; + +describe('Reference.isReference', () => { + it('handles primitives', () => { + expect(Reference.isReference(undefined)).toBe(false); + expect(Reference.isReference(null)).toBe(false); + expect(Reference.isReference(true)).toBe(false); + expect(Reference.isReference(1)).toBe(false); + expect(Reference.isReference('a')).toBe(false); + expect(Reference.isReference({})).toBe(false); + }); + + it('handles schemas', () => { + expect( + Reference.isReference( + schema.string({ + defaultValue: 'value', + }) + ) + ).toBe(false); + + expect( + Reference.isReference( + schema.conditional( + schema.contextRef('context_value_1'), + schema.contextRef('context_value_2'), + schema.string(), + schema.string() + ) + ) + ).toBe(false); + }); + + it('handles context references', () => { + expect(Reference.isReference(schema.contextRef('ref_1'))).toBe(true); + }); + + it('handles sibling references', () => { + expect(Reference.isReference(schema.siblingRef('ref_1'))).toBe(true); + }); +}); diff --git a/packages/kbn-config-schema/src/references/reference.ts b/packages/kbn-config-schema/src/references/reference.ts index 5dffc990f3b7b..9af1f910053ae 100644 --- a/packages/kbn-config-schema/src/references/reference.ts +++ b/packages/kbn-config-schema/src/references/reference.ts @@ -22,7 +22,7 @@ import { internals, Reference as InternalReference } from '../internals'; export class Reference { public static isReference(value: V | Reference | undefined): value is Reference { return ( - value !== undefined && + value != null && typeof (value as Reference).getSchema === 'function' && internals.isRef((value as Reference).getSchema()) ); diff --git a/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap index 2e0ada23eb5fd..b32db114860f5 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap @@ -18,6 +18,14 @@ exports[`properly validates types according chosen schema 1`] = `"value is [a] b exports[`properly validates types according chosen schema 2`] = `"value is [ab] but it must have a maximum length of [1]."`; +exports[`properly validates when compares with "null" literal Schema 1`] = `"value is [a] but it must have a minimum length of [2]."`; + +exports[`properly validates when compares with "null" literal Schema 2`] = `"value is [ab] but it must have a minimum length of [3]."`; + +exports[`properly validates when compares with Schema 1`] = `"value is [a] but it must have a minimum length of [2]."`; + +exports[`properly validates when compares with Schema 2`] = `"value is [ab] but it must have a minimum length of [3]."`; + exports[`required by default 1`] = `"expected value of type [string] but got [undefined]"`; exports[`works with both context and sibling references 1`] = `"[value]: expected value of type [string] but got [number]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap index 179e3e4251423..14d474b4a516b 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap @@ -9,3 +9,5 @@ exports[`returns error when not correct 2`] = `"expected value to equal [true] b exports[`returns error when not correct 3`] = `"expected value to equal [test] but got [1,2,3]"`; exports[`returns error when not correct 4`] = `"expected value to equal [123] but got [abc]"`; + +exports[`returns error when not correct 5`] = `"expected value to equal [null] but got [42]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap new file mode 100644 index 0000000000000..6eea2a7cefc72 --- /dev/null +++ b/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`throws on any value set 1`] = `"a value wasn't expected to be present"`; + +exports[`throws on any value set 2`] = `"a value wasn't expected to be present"`; + +exports[`throws on any value set 3`] = `"a value wasn't expected to be present"`; + +exports[`throws on any value set 4`] = `"a value wasn't expected to be present"`; + +exports[`throws on value set as object property 1`] = `"[name]: a value wasn't expected to be present"`; + +exports[`works for conditional types 1`] = `"[name]: a value wasn't expected to be present"`; diff --git a/packages/kbn-config-schema/src/types/conditional_type.test.ts b/packages/kbn-config-schema/src/types/conditional_type.test.ts index a72c3463e00cb..354854b864755 100644 --- a/packages/kbn-config-schema/src/types/conditional_type.test.ts +++ b/packages/kbn-config-schema/src/types/conditional_type.test.ts @@ -114,6 +114,72 @@ test('properly validates types according chosen schema', () => { ).toEqual('a'); }); +test('properly validates when compares with Schema', () => { + const type = schema.conditional( + schema.contextRef('context_value_1'), + schema.number(), + schema.string({ minLength: 2 }), + schema.string({ minLength: 3 }) + ); + + expect(() => + type.validate('a', { + context_value_1: 0, + }) + ).toThrowErrorMatchingSnapshot(); + + expect( + type.validate('ab', { + context_value_1: 0, + }) + ).toEqual('ab'); + + expect(() => + type.validate('ab', { + context_value_1: 'b', + }) + ).toThrowErrorMatchingSnapshot(); + + expect( + type.validate('abc', { + context_value_1: 'b', + }) + ).toEqual('abc'); +}); + +test('properly validates when compares with "null" literal Schema', () => { + const type = schema.conditional( + schema.contextRef('context_value_1'), + schema.literal(null), + schema.string({ minLength: 2 }), + schema.string({ minLength: 3 }) + ); + + expect(() => + type.validate('a', { + context_value_1: null, + }) + ).toThrowErrorMatchingSnapshot(); + + expect( + type.validate('ab', { + context_value_1: null, + }) + ).toEqual('ab'); + + expect(() => + type.validate('ab', { + context_value_1: 'b', + }) + ).toThrowErrorMatchingSnapshot(); + + expect( + type.validate('abc', { + context_value_1: 'b', + }) + ).toEqual('abc'); +}); + test('properly handles schemas with incompatible types', () => { const type = schema.conditional( schema.contextRef('context_value_1'), diff --git a/packages/kbn-config-schema/src/types/conditional_type.ts b/packages/kbn-config-schema/src/types/conditional_type.ts index acba5fa2cedfb..fb744082874b2 100644 --- a/packages/kbn-config-schema/src/types/conditional_type.ts +++ b/packages/kbn-config-schema/src/types/conditional_type.ts @@ -27,15 +27,18 @@ export type ConditionalTypeValue = string | number | boolean | object | null; export class ConditionalType extends Type { constructor( leftOperand: Reference, - rightOperand: Reference | A, + rightOperand: Reference | A | Type, equalType: Type, notEqualType: Type, options?: TypeOptions ) { const schema = internals.when(leftOperand.getSchema(), { - is: Reference.isReference(rightOperand) ? rightOperand.getSchema() : rightOperand, - otherwise: notEqualType.getSchema(), + is: + Reference.isReference(rightOperand) || rightOperand instanceof Type + ? rightOperand.getSchema() + : rightOperand, then: equalType.getSchema(), + otherwise: notEqualType.getSchema(), }); super(schema, options); diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index 727e2c6503021..cfa8cc4b7553d 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -33,3 +33,4 @@ export { RecordOfOptions, RecordOfType } from './record_type'; export { StringOptions, StringType } from './string_type'; export { UnionType } from './union_type'; export { URIOptions, URIType } from './uri_type'; +export { NeverType } from './never_type'; diff --git a/packages/kbn-config-schema/src/types/literal_type.test.ts b/packages/kbn-config-schema/src/types/literal_type.test.ts index 5ee0ac4edff68..a5ddff3152368 100644 --- a/packages/kbn-config-schema/src/types/literal_type.test.ts +++ b/packages/kbn-config-schema/src/types/literal_type.test.ts @@ -33,6 +33,10 @@ test('handles number', () => { expect(literal(123).validate(123)).toBe(123); }); +test('handles null', () => { + expect(literal(null).validate(null)).toBe(null); +}); + test('returns error when not correct', () => { expect(() => literal('test').validate('foo')).toThrowErrorMatchingSnapshot(); @@ -41,6 +45,8 @@ test('returns error when not correct', () => { expect(() => literal('test').validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); expect(() => literal(123).validate('abc')).toThrowErrorMatchingSnapshot(); + + expect(() => literal(null).validate(42)).toThrowErrorMatchingSnapshot(); }); test('includes namespace in failure', () => { diff --git a/packages/kbn-config-schema/src/types/never_type.test.ts b/packages/kbn-config-schema/src/types/never_type.test.ts new file mode 100644 index 0000000000000..46f0b47f56ad6 --- /dev/null +++ b/packages/kbn-config-schema/src/types/never_type.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '..'; + +test('throws on any value set', () => { + const type = schema.never(); + + expect(() => type.validate(1)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('a')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(null)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({})).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(undefined)).not.toThrow(); +}); + +test('throws on value set as object property', () => { + const type = schema.object({ + name: schema.never(), + status: schema.string(), + }); + + expect(() => + type.validate({ name: 'name', status: 'in progress' }) + ).toThrowErrorMatchingSnapshot(); + + expect(() => type.validate({ status: 'in progress' })).not.toThrow(); + expect(() => type.validate({ name: undefined, status: 'in progress' })).not.toThrow(); +}); + +test('works for conditional types', () => { + const type = schema.object({ + name: schema.conditional( + schema.contextRef('context_value_1'), + schema.contextRef('context_value_2'), + schema.string(), + schema.never() + ), + }); + + expect( + type.validate( + { name: 'a' }, + { + context_value_1: 0, + context_value_2: 0, + } + ) + ).toEqual({ name: 'a' }); + + expect(() => + type.validate( + { name: 'a' }, + { + context_value_1: 0, + context_value_2: 1, + } + ) + ).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/kbn-config-schema/src/types/never_type.ts b/packages/kbn-config-schema/src/types/never_type.ts new file mode 100644 index 0000000000000..bb175caaaa6ac --- /dev/null +++ b/packages/kbn-config-schema/src/types/never_type.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { internals } from '../internals'; +import { Type } from './type'; + +export class NeverType extends Type { + constructor() { + super(internals.any().forbidden()); + } + + protected handleError(type: string) { + switch (type) { + case 'any.unknown': + return "a value wasn't expected to be present"; + } + } +}