mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Introduce recordOf
schema. Remove redundant declarations. (#26952)
This commit is contained in:
parent
9aecea4dd2
commit
94b2b83bd7
10 changed files with 269 additions and 24 deletions
|
@ -40,6 +40,8 @@ import {
|
|||
NumberType,
|
||||
ObjectType,
|
||||
Props,
|
||||
RecordOfOptions,
|
||||
RecordOfType,
|
||||
StringOptions,
|
||||
StringType,
|
||||
Type,
|
||||
|
@ -105,6 +107,14 @@ function mapOf<K, V>(
|
|||
return new MapOfType(keyType, valueType, options);
|
||||
}
|
||||
|
||||
function recordOf<K extends string, V>(
|
||||
keyType: Type<K>,
|
||||
valueType: Type<V>,
|
||||
options?: RecordOfOptions<K, V>
|
||||
): Type<Record<K, V>> {
|
||||
return new RecordOfType(keyType, valueType, options);
|
||||
}
|
||||
|
||||
function oneOf<A, B, C, D, E, F, G, H, I, J>(
|
||||
types: [Type<A>, Type<B>, Type<C>, Type<D>, Type<E>, Type<F>, Type<G>, Type<H>, Type<I>, Type<J>],
|
||||
options?: TypeOptions<A | B | C | D | E | F | G | H | I | J>
|
||||
|
@ -175,6 +185,7 @@ export const schema = {
|
|||
number,
|
||||
object,
|
||||
oneOf,
|
||||
recordOf,
|
||||
siblingRef,
|
||||
string,
|
||||
};
|
||||
|
|
|
@ -274,6 +274,54 @@ export const internals = Joi.extend([
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'record',
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
if (!isPlainObject(value)) {
|
||||
return this.createError('record.base', { value }, state, options);
|
||||
}
|
||||
|
||||
return value as any;
|
||||
},
|
||||
rules: [
|
||||
anyCustomRule,
|
||||
{
|
||||
name: 'entries',
|
||||
params: { key: Joi.object().schema(), value: Joi.object().schema() },
|
||||
validate(params, value, state, options) {
|
||||
const result = {} as Record<string, any>;
|
||||
for (const [entryKey, entryValue] of Object.entries(value)) {
|
||||
const { value: validatedEntryKey, error: keyError } = Joi.validate(
|
||||
entryKey,
|
||||
params.key
|
||||
);
|
||||
|
||||
if (keyError) {
|
||||
return this.createError('record.key', { entryKey, reason: keyError }, state, options);
|
||||
}
|
||||
|
||||
const { value: validatedEntryValue, error: valueError } = Joi.validate(
|
||||
entryValue,
|
||||
params.value
|
||||
);
|
||||
|
||||
if (valueError) {
|
||||
return this.createError(
|
||||
'record.value',
|
||||
{ entryKey, reason: valueError },
|
||||
state,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
result[validatedEntryKey] = validatedEntryValue;
|
||||
}
|
||||
|
||||
return result as any;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`fails when not receiving expected key type 1`] = `"[name]: expected value of type [number] but got [string]"`;
|
||||
exports[`fails when not receiving expected key type 1`] = `"[key(\\"name\\")]: expected value of type [number] but got [string]"`;
|
||||
|
||||
exports[`fails when not receiving expected value type 1`] = `"[name]: expected value of type [string] but got [number]"`;
|
||||
|
||||
exports[`includes namespace in failure when wrong key type 1`] = `"[foo-namespace.name]: expected value of type [number] but got [string]"`;
|
||||
exports[`includes namespace in failure when wrong key type 1`] = `"[foo-namespace.key(\\"name\\")]: expected value of type [number] but got [string]"`;
|
||||
|
||||
exports[`includes namespace in failure when wrong top-level type 1`] = `"[foo-namespace]: expected value of type [Map] or [object] but got [Array]"`;
|
||||
|
||||
|
|
|
@ -29,5 +29,6 @@ export { MaybeType } from './maybe_type';
|
|||
export { MapOfOptions, MapOfType } from './map_type';
|
||||
export { NumberOptions, NumberType } from './number_type';
|
||||
export { ObjectType, Props, TypeOf } from './object_type';
|
||||
export { RecordOfOptions, RecordOfType } from './record_type';
|
||||
export { StringOptions, StringType } from './string_type';
|
||||
export { UnionType } from './union_type';
|
||||
|
|
|
@ -22,16 +22,14 @@ import { Type } from './type';
|
|||
|
||||
export class LiteralType<T> extends Type<T> {
|
||||
constructor(value: T) {
|
||||
super(internals.any(), {
|
||||
// Before v13.3.0 Joi.any().value() didn't provide raw value if validation
|
||||
// fails, so to display this value in error message we should provide
|
||||
// custom validation function. Once we upgrade Joi, we'll be able to use
|
||||
// `value()` with custom `any.allowOnly` error handler instead.
|
||||
validate(valueToValidate) {
|
||||
if (valueToValidate !== value) {
|
||||
return `expected value to equal [${value}] but got [${valueToValidate}]`;
|
||||
}
|
||||
},
|
||||
});
|
||||
super(internals.any().valid(value));
|
||||
}
|
||||
|
||||
protected handleError(type: string, { value, valids: [expectedValue] }: Record<string, any>) {
|
||||
switch (type) {
|
||||
case 'any.required':
|
||||
case 'any.allowOnly':
|
||||
return `expected value to equal [${expectedValue}] but got [${value}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import typeDetect from 'type-detect';
|
||||
import { SchemaTypeError } from '../errors';
|
||||
import { SchemaTypeError, SchemaTypesError } from '../errors';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions } from './type';
|
||||
|
||||
|
@ -51,9 +51,16 @@ export class MapOfType<K, V> extends Type<Map<K, V>> {
|
|||
case 'map.key':
|
||||
case 'map.value':
|
||||
const childPathWithIndex = reason.path.slice();
|
||||
childPathWithIndex.splice(path.length, 0, entryKey.toString());
|
||||
childPathWithIndex.splice(
|
||||
path.length,
|
||||
0,
|
||||
// If `key` validation failed, let's stress that to make error more obvious.
|
||||
type === 'map.key' ? `key("${entryKey}")` : entryKey.toString()
|
||||
);
|
||||
|
||||
return new SchemaTypeError(reason.message, childPathWithIndex);
|
||||
return reason instanceof SchemaTypesError
|
||||
? new SchemaTypesError(reason, childPathWithIndex, reason.errors)
|
||||
: new SchemaTypeError(reason, childPathWithIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
122
packages/kbn-config-schema/src/types/record_of_type.test.ts
Normal file
122
packages/kbn-config-schema/src/types/record_of_type.test.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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('handles object as input', () => {
|
||||
const type = schema.recordOf(schema.string(), schema.string());
|
||||
const value = {
|
||||
name: 'foo',
|
||||
};
|
||||
expect(type.validate(value)).toEqual({ name: 'foo' });
|
||||
});
|
||||
|
||||
test('fails when not receiving expected value type', () => {
|
||||
const type = schema.recordOf(schema.string(), schema.string());
|
||||
const value = {
|
||||
name: 123,
|
||||
};
|
||||
|
||||
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[name]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('fails when not receiving expected key type', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.oneOf([schema.literal('nickName'), schema.literal('lastName')]),
|
||||
schema.string()
|
||||
);
|
||||
|
||||
const value = {
|
||||
name: 'foo',
|
||||
};
|
||||
|
||||
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key(\\"name\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [nickName] but got [name]
|
||||
- [1]: expected value to equal [lastName] but got [name]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('includes namespace in failure when wrong top-level type', () => {
|
||||
const type = schema.recordOf(schema.string(), schema.string());
|
||||
expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[foo-namespace]: expected value of type [object] but got [Array]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('includes namespace in failure when wrong value type', () => {
|
||||
const type = schema.recordOf(schema.string(), schema.string());
|
||||
const value = {
|
||||
name: 123,
|
||||
};
|
||||
|
||||
expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[foo-namespace.name]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('includes namespace in failure when wrong key type', () => {
|
||||
const type = schema.recordOf(schema.string({ minLength: 10 }), schema.string());
|
||||
const value = {
|
||||
name: 'foo',
|
||||
};
|
||||
|
||||
expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[foo-namespace.key(\\"name\\")]: value is [name] but it must have a minimum length of [10]."`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns default value if undefined', () => {
|
||||
const obj = { foo: 'bar' };
|
||||
|
||||
const type = schema.recordOf(schema.string(), schema.string(), {
|
||||
defaultValue: obj,
|
||||
});
|
||||
|
||||
expect(type.validate(undefined)).toEqual(obj);
|
||||
});
|
||||
|
||||
test('recordOf within recordOf', () => {
|
||||
const type = schema.recordOf(schema.string(), schema.recordOf(schema.string(), schema.number()));
|
||||
const value = {
|
||||
foo: {
|
||||
bar: 123,
|
||||
},
|
||||
};
|
||||
|
||||
expect(type.validate(value)).toEqual({ foo: { bar: 123 } });
|
||||
});
|
||||
|
||||
test('object within recordOf', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
bar: schema.number(),
|
||||
})
|
||||
);
|
||||
const value = {
|
||||
foo: {
|
||||
bar: 123,
|
||||
},
|
||||
};
|
||||
|
||||
expect(type.validate(value)).toEqual({ foo: { bar: 123 } });
|
||||
});
|
58
packages/kbn-config-schema/src/types/record_type.ts
Normal file
58
packages/kbn-config-schema/src/types/record_type.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 typeDetect from 'type-detect';
|
||||
import { SchemaTypeError, SchemaTypesError } from '../errors';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions } from './type';
|
||||
|
||||
export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>>;
|
||||
|
||||
export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
|
||||
constructor(keyType: Type<K>, valueType: Type<V>, options: RecordOfOptions<K, V> = {}) {
|
||||
const schema = internals.record().entries(keyType.getSchema(), valueType.getSchema());
|
||||
|
||||
super(schema, options);
|
||||
}
|
||||
|
||||
protected handleError(
|
||||
type: string,
|
||||
{ entryKey, reason, value }: Record<string, any>,
|
||||
path: string[]
|
||||
) {
|
||||
switch (type) {
|
||||
case 'any.required':
|
||||
case 'record.base':
|
||||
return `expected value of type [object] but got [${typeDetect(value)}]`;
|
||||
case 'record.key':
|
||||
case 'record.value':
|
||||
const childPathWithIndex = reason.path.slice();
|
||||
childPathWithIndex.splice(
|
||||
path.length,
|
||||
0,
|
||||
// If `key` validation failed, let's stress that to make error more obvious.
|
||||
type === 'record.key' ? `key("${entryKey}")` : entryKey.toString()
|
||||
);
|
||||
|
||||
return reason instanceof SchemaTypesError
|
||||
? new SchemaTypesError(reason, childPathWithIndex, reason.errors)
|
||||
: new SchemaTypeError(reason, childPathWithIndex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ export abstract class Type<V> {
|
|||
*/
|
||||
protected readonly internalSchema: AnySchema;
|
||||
|
||||
constructor(schema: AnySchema, options: TypeOptions<V> = {}) {
|
||||
protected constructor(schema: AnySchema, options: TypeOptions<V> = {}) {
|
||||
if (options.defaultValue !== undefined) {
|
||||
schema = schema.optional();
|
||||
|
||||
|
@ -62,7 +62,7 @@ export abstract class Type<V> {
|
|||
// only the last error handler is counted.
|
||||
const schemaFlags = (schema.describe().flags as Record<string, any>) || {};
|
||||
if (schemaFlags.error === undefined) {
|
||||
schema = schema.error!(([error]) => this.onError(error));
|
||||
schema = schema.error(([error]) => this.onError(error));
|
||||
}
|
||||
|
||||
this.internalSchema = schema;
|
||||
|
|
12
packages/kbn-config-schema/types/joi.d.ts
vendored
12
packages/kbn-config-schema/types/joi.d.ts
vendored
|
@ -29,11 +29,15 @@ declare module 'joi' {
|
|||
entries(key: AnySchema, value: AnySchema): this;
|
||||
}
|
||||
|
||||
// In more recent Joi types we can use `Root` type instead of `typeof Joi`.
|
||||
export type JoiRoot = typeof Joi & {
|
||||
interface RecordSchema extends AnySchema {
|
||||
entries(key: AnySchema, value: AnySchema): this;
|
||||
}
|
||||
|
||||
export type JoiRoot = Joi.Root & {
|
||||
bytes: () => BytesSchema;
|
||||
duration: () => AnySchema;
|
||||
map: () => MapSchema;
|
||||
record: () => RecordSchema;
|
||||
};
|
||||
|
||||
interface AnySchema {
|
||||
|
@ -44,8 +48,4 @@ declare module 'joi' {
|
|||
interface ObjectSchema {
|
||||
schema(): this;
|
||||
}
|
||||
|
||||
// Joi types define only signature with single extension, but Joi supports
|
||||
// an array form as well. It's fixed in more recent Joi types.
|
||||
function extend(extension: Joi.Extension | Joi.Extension[]): any;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue