From 15e17aa2e8349363c80c552a0a517b4a7820e671 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 9 Aug 2018 13:02:40 +0200 Subject: [PATCH] [6.x] Introduce `schema.any` (#21830) --- .../config/__tests__/config_service.test.ts | 4 +- src/core/server/config/config_service.ts | 8 +-- src/core/server/config/config_with_schema.ts | 4 +- src/core/server/config/schema/index.ts | 9 ++- .../__snapshots__/any_type.test.ts.snap | 5 ++ .../schema/types/__tests__/any_type.test.ts | 60 +++++++++++++++++++ .../server/config/schema/types/any_type.ts | 16 ++++- .../server/config/schema/types/object_type.ts | 5 +- .../server/config/schema/types/union_type.ts | 3 +- 9 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap create mode 100644 src/core/server/config/schema/types/__tests__/any_type.test.ts diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/__tests__/config_service.test.ts index e676de9e2c0e6..c2b68d52d0210 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/__tests__/config_service.test.ts @@ -19,7 +19,7 @@ /* tslint:disable max-classes-per-file */ import { BehaviorSubject, first, k$, toPromise } from '../../../lib/kbn_observable'; -import { AnyType, schema, TypeOf } from '../schema'; +import { schema, Type, TypeOf } from '../schema'; import { ConfigService, ObjectToRawConfigAdapter } from '..'; import { logger } from '../../logging/__mocks__'; @@ -268,7 +268,7 @@ test('treats config as enabled if config path is not present in config', async ( expect(unusedPaths).toEqual([]); }); -function createClassWithSchema(s: AnyType) { +function createClassWithSchema(s: Type) { return class ExampleClassWithSchema { public static schema = s; diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index c96c3764fb231..8bccdd4055678 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -24,7 +24,7 @@ import { Logger, LoggerFactory } from '../logging'; import { ConfigWithSchema } from './config_with_schema'; import { Env } from './env'; import { RawConfig } from './raw_config'; -import { AnyType } from './schema'; +import { Type } from './schema'; export type ConfigPath = string | string[]; @@ -61,7 +61,7 @@ export class ConfigService { * @param ConfigClass A class (not an instance of a class) that contains a * static `schema` that we validate the config at the given `path` against. */ - public atPath( + public atPath, Config>( path: ConfigPath, ConfigClass: ConfigWithSchema ) { @@ -76,7 +76,7 @@ export class ConfigService { * * @see atPath */ - public optionalAtPath( + public optionalAtPath, Config>( path: ConfigPath, ConfigClass: ConfigWithSchema ) { @@ -120,7 +120,7 @@ export class ConfigService { return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths)); } - private createConfig( + private createConfig, Config>( path: ConfigPath, rawConfig: {}, ConfigClass: ConfigWithSchema diff --git a/src/core/server/config/config_with_schema.ts b/src/core/server/config/config_with_schema.ts index f049d28fa9787..31de12a95cc9f 100644 --- a/src/core/server/config/config_with_schema.ts +++ b/src/core/server/config/config_with_schema.ts @@ -19,7 +19,7 @@ // TODO inline all of these import { Env } from './env'; -import { AnyType, TypeOf } from './schema'; +import { Type, TypeOf } from './schema'; /** * Interface that defines the static side of a config class. @@ -31,7 +31,7 @@ import { AnyType, TypeOf } from './schema'; * in TypeScript, but it can be used to ensure we have a config class that * matches whenever it's used. */ -export interface ConfigWithSchema { +export interface ConfigWithSchema, Config> { /** * Any config class must define a schema that validates the config, based on * the injected `schema` helper. diff --git a/src/core/server/config/schema/index.ts b/src/core/server/config/schema/index.ts index 4a4baffc4d4a2..8b53f38ac5edb 100644 --- a/src/core/server/config/schema/index.ts +++ b/src/core/server/config/schema/index.ts @@ -48,9 +48,13 @@ import { UnionType, } from './types'; -export { AnyType, ObjectType, TypeOf }; +export { ObjectType, TypeOf, Type }; export { ByteSizeValue } from './byte_size_value'; +function any(options?: TypeOptions) { + return new AnyType(options); +} + function boolean(options?: TypeOptions): Type { return new BooleanType(options); } @@ -135,7 +139,7 @@ function oneOf( ): Type; function oneOf(types: [Type, Type], options?: TypeOptions): Type; function oneOf(types: [Type], options?: TypeOptions): Type; -function oneOf(types: RTS, options?: TypeOptions): Type { +function oneOf>>(types: RTS, options?: TypeOptions): Type { return new UnionType(types, options); } @@ -158,6 +162,7 @@ function conditional( } export const schema = { + any, arrayOf, boolean, byteSize, diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap b/src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap new file mode 100644 index 0000000000000..3a40752d52b6e --- /dev/null +++ b/src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [any] but got [undefined]"`; + +exports[`is required by default 1`] = `"expected value of type [any] but got [undefined]"`; diff --git a/src/core/server/config/schema/types/__tests__/any_type.test.ts b/src/core/server/config/schema/types/__tests__/any_type.test.ts new file mode 100644 index 0000000000000..6f39f3deab5fd --- /dev/null +++ b/src/core/server/config/schema/types/__tests__/any_type.test.ts @@ -0,0 +1,60 @@ +/* + * 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('works for any value', () => { + expect(schema.any().validate(true)).toBe(true); + expect(schema.any().validate(100)).toBe(100); + expect(schema.any().validate('foo')).toBe('foo'); + expect(schema.any().validate(null)).toBe(null); + expect(schema.any().validate({ foo: 'bar', baz: 2 })).toEqual({ foo: 'bar', baz: 2 }); +}); + +test('is required by default', () => { + expect(() => schema.any().validate(undefined)).toThrowErrorMatchingSnapshot(); +}); + +test('includes namespace in failure', () => { + expect(() => + schema.any().validate(undefined, {}, 'foo-namespace') + ).toThrowErrorMatchingSnapshot(); +}); + +describe('#defaultValue', () => { + test('returns default when undefined', () => { + expect(schema.any({ defaultValue: true }).validate(undefined)).toBe(true); + expect(schema.any({ defaultValue: 200 }).validate(undefined)).toBe(200); + expect(schema.any({ defaultValue: 'bar' }).validate(undefined)).toBe('bar'); + expect(schema.any({ defaultValue: { baz: 'foo' } }).validate(undefined)).toEqual({ + baz: 'foo', + }); + }); + + test('returns value when specified', () => { + expect(schema.any({ defaultValue: true }).validate(false)).toBe(false); + expect(schema.any({ defaultValue: 200 }).validate(100)).toBe(100); + expect(schema.any({ defaultValue: 'bar' }).validate('foo')).toBe('foo'); + expect(schema.any({ defaultValue: 'not-null' }).validate(null)).toBe(null); + expect(schema.any({ defaultValue: { baz: 'foo' } }).validate({ foo: 'bar', baz: 2 })).toEqual({ + foo: 'bar', + baz: 2, + }); + }); +}); diff --git a/src/core/server/config/schema/types/any_type.ts b/src/core/server/config/schema/types/any_type.ts index 53a32c58f001b..c4e8b9d3f6f5d 100644 --- a/src/core/server/config/schema/types/any_type.ts +++ b/src/core/server/config/schema/types/any_type.ts @@ -17,6 +17,18 @@ * under the License. */ -import { Type } from './type'; +import typeDetect from 'type-detect'; +import { internals } from '../internals'; +import { Type, TypeOptions } from './type'; -export type AnyType = Type; +export class AnyType extends Type { + constructor(options?: TypeOptions) { + super(internals.any(), options); + } + + protected handleError(type: string, { value }: Record) { + if (type === 'any.required') { + return `expected value of type [any] but got [${typeDetect(value)}]`; + } + } +} diff --git a/src/core/server/config/schema/types/object_type.ts b/src/core/server/config/schema/types/object_type.ts index 7e9e8a796cc72..e61fcd90ef016 100644 --- a/src/core/server/config/schema/types/object_type.ts +++ b/src/core/server/config/schema/types/object_type.ts @@ -19,12 +19,11 @@ import typeDetect from 'type-detect'; import { AnySchema, internals } from '../internals'; -import { AnyType } from './any_type'; import { Type, TypeOptions } from './type'; -export type Props = Record; +export type Props = Record>; -export type TypeOf = RT['type']; +export type TypeOf> = RT['type']; // Because of https://github.com/Microsoft/TypeScript/issues/14041 // this might not have perfect _rendering_ output, but it will be typed. diff --git a/src/core/server/config/schema/types/union_type.ts b/src/core/server/config/schema/types/union_type.ts index 3d39ac0ea212d..e6efb4afb66aa 100644 --- a/src/core/server/config/schema/types/union_type.ts +++ b/src/core/server/config/schema/types/union_type.ts @@ -20,10 +20,9 @@ import typeDetect from 'type-detect'; import { SchemaTypeError, SchemaTypesError } from '../errors'; import { internals } from '../internals'; -import { AnyType } from './any_type'; import { Type, TypeOptions } from './type'; -export class UnionType extends Type { +export class UnionType>, T> extends Type { constructor(types: RTS, options?: TypeOptions) { const schema = internals.alternatives(types.map(type => type.getSchema()));