Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HTTP/OAS] Prepare @kbn/config-schema for generating OAS #180184

Merged
Merged
14 changes: 14 additions & 0 deletions packages/kbn-config-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,17 @@ export const schema = {
};

export type Schema = typeof schema;

import {
META_FIELD_X_OAS_OPTIONAL,
META_FIELD_X_OAS_MAX_LENGTH,
META_FIELD_X_OAS_MIN_LENGTH,
META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES,
} from './src/oas_meta_fields';

export const metaFields = Object.freeze({
META_FIELD_X_OAS_OPTIONAL,
META_FIELD_X_OAS_MAX_LENGTH,
META_FIELD_X_OAS_MIN_LENGTH,
META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES,
});
17 changes: 17 additions & 0 deletions packages/kbn-config-schema/src/oas_meta_fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
* These fields are used in Joi meta to capture additional information used
* by OpenAPI spec generator.
*/
export const META_FIELD_X_OAS_OPTIONAL = 'x-oas-optional' as const;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the x- prefix is expected by our partner lib https://www.npmjs.com/package/joi-to-json (to be introduced in a follow up PR)

export const META_FIELD_X_OAS_MIN_LENGTH = 'x-oas-min-length' as const;
export const META_FIELD_X_OAS_MAX_LENGTH = 'x-oas-max-length' as const;
export const META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES =
'x-oas-get-additional-properties' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { schema } from '../..';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';

test('handles object as input', () => {
const type = schema.mapOf(schema.string(), schema.string());
Expand Down Expand Up @@ -186,6 +187,17 @@ test('error preserves full path', () => {
);
});

test('meta', () => {
const stringSchema = schema.string();
const type = schema.mapOf(schema.string(), stringSchema);
const result = type
.getSchema()
.describe()
.metas![0][META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]();

expect(result).toBe(stringSchema.getSchema());
});

describe('#extendsDeep', () => {
describe('#keyType', () => {
const type = schema.mapOf(schema.string(), schema.object({ foo: schema.string() }));
Expand Down
8 changes: 7 additions & 1 deletion packages/kbn-config-schema/src/types/map_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import typeDetect from 'type-detect';
import { SchemaTypeError, SchemaTypesError } from '../errors';
import { internals } from '../internals';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';
import { Type, TypeOptions, ExtendsDeepOptions } from './type';

export type MapOfOptions<K, V> = TypeOptions<Map<K, V>>;
Expand All @@ -20,7 +21,12 @@ export class MapOfType<K, V> extends Type<Map<K, V>> {

constructor(keyType: Type<K>, valueType: Type<V>, options: MapOfOptions<K, V> = {}) {
const defaultValue = options.defaultValue;
const schema = internals.map().entries(keyType.getSchema(), valueType.getSchema());
const schema = internals
.map()
.entries(keyType.getSchema(), valueType.getSchema())
.meta({
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
});

super(schema, {
...options,
Expand Down
7 changes: 7 additions & 0 deletions packages/kbn-config-schema/src/types/maybe_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { schema } from '../..';
import { META_FIELD_X_OAS_OPTIONAL } from '../oas_meta_fields';

test('returns value if specified', () => {
const type = schema.maybe(schema.string());
Expand Down Expand Up @@ -96,6 +97,12 @@ describe('maybe + object', () => {
});
});

test('meta', () => {
const maybeString = schema.maybe(schema.string());
const result = maybeString.getSchema().describe().metas[0];
expect(result).toEqual({ [META_FIELD_X_OAS_OPTIONAL]: true });
});

describe('#extendsDeep', () => {
const type = schema.maybe(schema.object({ foo: schema.string() }));

Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-config-schema/src/types/maybe_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { Type, ExtendsDeepOptions } from './type';

import { META_FIELD_X_OAS_OPTIONAL } from '../oas_meta_fields';
export class MaybeType<V> extends Type<V | undefined> {
private readonly maybeType: Type<V>;

Expand All @@ -16,6 +17,7 @@ export class MaybeType<V> extends Type<V | undefined> {
type
.getSchema()
.optional()
.meta({ [META_FIELD_X_OAS_OPTIONAL]: true })
.default(() => undefined)
);
this.maybeType = type;
Expand Down
12 changes: 12 additions & 0 deletions packages/kbn-config-schema/src/types/record_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { schema } from '../..';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';

test('handles object as input', () => {
const type = schema.recordOf(schema.string(), schema.string());
Expand Down Expand Up @@ -208,3 +209,14 @@ describe('#extendsDeep', () => {
).toThrowErrorMatchingInlineSnapshot(`"[key.bar]: definition for this key is missing"`);
});
});

test('meta', () => {
const stringSchema = schema.string();
const type = schema.mapOf(schema.string(), stringSchema);
const result = type
.getSchema()
.describe()
.metas![0][META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]();

expect(result).toBe(stringSchema.getSchema());
});
8 changes: 7 additions & 1 deletion packages/kbn-config-schema/src/types/record_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import typeDetect from 'type-detect';
import { SchemaTypeError, SchemaTypesError } from '../errors';
import { internals } from '../internals';
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';

export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>>;

Expand All @@ -19,7 +20,12 @@ export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
private readonly options: RecordOfOptions<K, V>;

constructor(keyType: Type<K>, valueType: Type<V>, options: RecordOfOptions<K, V> = {}) {
const schema = internals.record().entries(keyType.getSchema(), valueType.getSchema());
const schema = internals
.record()
.entries(keyType.getSchema(), valueType.getSchema())
.meta({
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
});

super(schema, options);
this.keyType = keyType;
Expand Down
12 changes: 12 additions & 0 deletions packages/kbn-config-schema/src/types/string_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { schema } from '../..';
import { META_FIELD_X_OAS_MAX_LENGTH, META_FIELD_X_OAS_MIN_LENGTH } from '../oas_meta_fields';

test('returns value is string and defined', () => {
expect(schema.string().validate('test')).toBe('test');
Expand Down Expand Up @@ -166,6 +167,17 @@ describe('#defaultValue', () => {
});
});

test('meta', () => {
const string = schema.string({ minLength: 1, maxLength: 3 });
const [meta1, meta2] = string.getSchema().describe().metas;
expect(meta1).toEqual({
[META_FIELD_X_OAS_MIN_LENGTH]: 1,
});
expect(meta2).toEqual({
[META_FIELD_X_OAS_MAX_LENGTH]: 3,
});
});

describe('#validate', () => {
test('is called with input value', () => {
let calledWith;
Expand Down
35 changes: 21 additions & 14 deletions packages/kbn-config-schema/src/types/string_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import typeDetect from 'type-detect';
import { internals } from '../internals';
import { Type, TypeOptions, convertValidationFunction } from './type';

import { META_FIELD_X_OAS_MIN_LENGTH, META_FIELD_X_OAS_MAX_LENGTH } from '../oas_meta_fields';

export type StringOptions = TypeOptions<string> & {
minLength?: number;
maxLength?: number;
Expand Down Expand Up @@ -37,24 +39,29 @@ export class StringType extends Type<string> {
}
return value;
});

if (options.minLength !== undefined) {
schema = schema.custom(
convertValidationFunction((value) => {
if (value.length < options.minLength!) {
return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`;
}
})
);
schema = schema
.custom(
convertValidationFunction((value) => {
if (value.length < options.minLength!) {
return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`;
}
})
)
.meta({ [META_FIELD_X_OAS_MIN_LENGTH]: options.minLength });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to our custom handling of string length validation, these values are currently lost. In order to avoid changing behaviour we use the meta function (docs) to attach a new piece of metadata containing string min/max length will propagated to through Joi.schema.describe (docs)

}

if (options.maxLength !== undefined) {
schema = schema.custom(
convertValidationFunction((value) => {
if (value.length > options.maxLength!) {
return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`;
}
})
);
schema = schema
.custom(
convertValidationFunction((value) => {
if (value.length > options.maxLength!) {
return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`;
}
})
)
.meta({ [META_FIELD_X_OAS_MAX_LENGTH]: options.maxLength });
}

schema.type = 'string';
Expand Down
23 changes: 23 additions & 0 deletions packages/kbn-config-schema/src/types/type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { get } from 'lodash';
import { internals } from '../internals';
import { Type, TypeOptions } from './type';

class MyType extends Type<any> {
constructor(opts: TypeOptions<any> = {}) {
super(internals.any(), opts);
}
}

test('describe', () => {
const type = new MyType({ description: 'my description' });
const meta = type.getSchema().describe();
expect(get(meta, 'flags.description')).toBe('my description');
});
6 changes: 6 additions & 0 deletions packages/kbn-config-schema/src/types/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Reference } from '../references';
export interface TypeOptions<T> {
defaultValue?: T | Reference<T> | (() => T);
validate?: (value: T) => string | void;
/** A human-friendly description of this type to be used in documentation */
description?: string;
}

export interface SchemaStructureEntry {
Expand Down Expand Up @@ -86,6 +88,10 @@ export abstract class Type<V> {
schema = schema.custom(convertValidationFunction(options.validate));
}

if (options.description) {
schema = schema.description(options.description);
}

// Attach generic error handler only if it hasn't been attached yet since
// only the last error handler is counted.
if (schema.$_getFlag('error') === undefined) {
Expand Down
Loading