Skip to content

Commit

Permalink
[HTTP/OAS] Prepare @kbn/config-schema for generating OAS (#180184)
Browse files Browse the repository at this point in the history
## Summary

Introduces a set of meta fields that will be used to track metadata lost
in how we currently use `joi` inside of `@kbn/config-schema`.

## Notes

* Related #180056
* Changes cherry-picked from
#156357
* Changes are not used for anything in this PR, they are intended to
enable our OAS generation scripts
  • Loading branch information
jloleysens authored Apr 9, 2024
1 parent 66fcd17 commit 5336a23
Show file tree
Hide file tree
Showing 14 changed files with 1,401 additions and 16 deletions.
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;
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 });
}

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

0 comments on commit 5336a23

Please sign in to comment.