diff --git a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts index 36671f2089bb25..236e279ef27feb 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts @@ -146,7 +146,7 @@ const fullStatusResponse: () => Type> = () => }, { meta: { - id: 'core.status.response', + id: 'core_status_response', description: `Kibana's operational status as well as a detailed breakdown of plugin statuses indication of various loads (like event loop utilization and network traffic) at time of request.`, }, } @@ -163,7 +163,7 @@ const redactedStatusResponse = () => }, { meta: { - id: 'core.status.redactedResponse', + id: 'core_status_redactedResponse', description: `A minimal representation of Kibana's operational status.`, }, } diff --git a/packages/kbn-config-schema/index.ts b/packages/kbn-config-schema/index.ts index 0ab1fd640d7af8..c6ef8c2ce99dc5 100644 --- a/packages/kbn-config-schema/index.ts +++ b/packages/kbn-config-schema/index.ts @@ -49,6 +49,7 @@ import { URIType, StreamType, UnionTypeOptions, + Lazy, } from './src/types'; export type { AnyType, ConditionalType, TypeOf, Props, SchemaStructureEntry, NullableProps }; @@ -216,6 +217,13 @@ function conditional( return new ConditionalType(leftOperand, rightOperand, equalType, notEqualType, options); } +/** + * Useful for creating recursive schemas. + */ +function lazy(id: string) { + return new Lazy(id); +} + export const schema = { any, arrayOf, @@ -226,6 +234,7 @@ export const schema = { contextRef, duration, ip, + lazy, literal, mapOf, maybe, @@ -245,7 +254,6 @@ export type Schema = typeof schema; import { META_FIELD_X_OAS_ANY, - META_FIELD_X_OAS_REF_ID, META_FIELD_X_OAS_OPTIONAL, META_FIELD_X_OAS_DEPRECATED, META_FIELD_X_OAS_MAX_LENGTH, @@ -255,7 +263,6 @@ import { export const metaFields = Object.freeze({ META_FIELD_X_OAS_ANY, - META_FIELD_X_OAS_REF_ID, META_FIELD_X_OAS_OPTIONAL, META_FIELD_X_OAS_DEPRECATED, META_FIELD_X_OAS_MAX_LENGTH, diff --git a/packages/kbn-config-schema/src/oas_meta_fields.ts b/packages/kbn-config-schema/src/oas_meta_fields.ts index 814bb32e7ea41e..d1846be8ecf143 100644 --- a/packages/kbn-config-schema/src/oas_meta_fields.ts +++ b/packages/kbn-config-schema/src/oas_meta_fields.ts @@ -15,6 +15,5 @@ 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; -export const META_FIELD_X_OAS_REF_ID = 'x-oas-ref-id' as const; export const META_FIELD_X_OAS_DEPRECATED = 'x-oas-deprecated' as const; export const META_FIELD_X_OAS_ANY = 'x-oas-any-type' as const; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index e3d8db0b2302d8..78eab0957557de 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -40,3 +40,4 @@ export { URIType } from './uri_type'; export { NeverType } from './never_type'; export type { IpOptions } from './ip_type'; export { IpType } from './ip_type'; +export { Lazy } from './lazy'; diff --git a/packages/kbn-config-schema/src/types/lazy.test.ts b/packages/kbn-config-schema/src/types/lazy.test.ts new file mode 100644 index 00000000000000..52864a9c825a87 --- /dev/null +++ b/packages/kbn-config-schema/src/types/lazy.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { Type, schema } from '../..'; + +interface RecursiveType { + name: string; + self: undefined | RecursiveType; +} + +// Test our recursive type inference +{ + const id = 'recursive'; + // @ts-expect-error + const testObject: Type = schema.object( + { + name: schema.string(), + notSelf: schema.lazy(id), // this declaration should fail + }, + { meta: { id } } + ); +} + +describe('lazy', () => { + const id = 'recursive'; + const object = schema.object( + { + name: schema.string(), + self: schema.lazy(id), + }, + { meta: { id } } + ); + + it('allows recursive runtime types to be defined', () => { + const self: RecursiveType = { + name: 'self1', + self: { + name: 'self2', + self: { + name: 'self3', + self: { + name: 'self4', + self: undefined, + }, + }, + }, + }; + const { value, error } = object.getSchema().validate(self); + expect(error).toBeUndefined(); + expect(value).toEqual(self); + }); + + it('detects invalid recursive types as expected', () => { + const invalidSelf = { + name: 'self1', + self: { + name: 123, + self: undefined, + }, + }; + + const { error, value } = object.getSchema().validate(invalidSelf); + expect(value).toEqual(invalidSelf); + expect(error?.message).toBe('expected value of type [string] but got [number]'); + }); +}); diff --git a/packages/kbn-config-schema/src/types/lazy.ts b/packages/kbn-config-schema/src/types/lazy.ts new file mode 100644 index 00000000000000..28dda1b701394e --- /dev/null +++ b/packages/kbn-config-schema/src/types/lazy.ts @@ -0,0 +1,19 @@ +/* + * 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 { Type } from './type'; +import { internals } from '../internals'; + +/** + * Use this type to construct recursive runtime schemas. + */ +export class Lazy extends Type { + constructor(id: string) { + super(internals.link(`#${id}`)); + } +} diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 12ed554e11099f..bf92d226155043 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { get } from 'lodash'; import { expectType } from 'tsd'; import { schema } from '../..'; import { TypeOf } from './object_type'; @@ -21,6 +22,16 @@ test('returns value by default', () => { expect(type.validate(value)).toEqual({ name: 'test' }); }); +test('meta', () => { + const type = schema.object( + { + name: schema.string(), + }, + { meta: { id: 'test_id' } } + ); + expect(get(type.getSchema().describe(), 'flags.id')).toEqual('test_id'); +}); + test('returns empty object if undefined', () => { const type = schema.object({}); expect(type.validate(undefined)).toEqual({}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 702d6d100847ec..d5193b5e0fc38c 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -69,8 +69,16 @@ interface UnknownOptions { unknowns?: OptionsForUnknowns; } +interface ObjectTypeOptionsMeta { + /** + * A string that uniquely identifies this schema. Used when generating OAS + * to create refs instead of inline schemas. + */ + id?: string; +} + export type ObjectTypeOptions

= TypeOptions> & - UnknownOptions; + UnknownOptions & { meta?: TypeOptions>['meta'] & ObjectTypeOptionsMeta }; export class ObjectType

extends Type> { private props: P; @@ -83,7 +91,7 @@ export class ObjectType

extends Type> for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); } - const schema = internals + let schema = internals .object() .keys(schemaKeys) .default() @@ -91,6 +99,10 @@ export class ObjectType

extends Type> .unknown(unknowns === 'allow') .options({ stripUnknown: { objects: unknowns === 'ignore' } }); + if (options.meta?.id) { + schema = schema.id(options.meta.id); + } + super(schema, typeOptions); this.props = props; this.propSchemas = schemaKeys; diff --git a/packages/kbn-config-schema/src/types/type.test.ts b/packages/kbn-config-schema/src/types/type.test.ts index a4784af2e8b62f..a0ca8603d46f0d 100644 --- a/packages/kbn-config-schema/src/types/type.test.ts +++ b/packages/kbn-config-schema/src/types/type.test.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { internals } from '../internals'; import { Type, TypeOptions } from './type'; -import { META_FIELD_X_OAS_REF_ID, META_FIELD_X_OAS_DEPRECATED } from '../oas_meta_fields'; +import { META_FIELD_X_OAS_DEPRECATED } from '../oas_meta_fields'; class MyType extends Type { constructor(opts: TypeOptions = {}) { @@ -20,12 +20,11 @@ class MyType extends Type { describe('meta', () => { it('sets meta when provided', () => { const type = new MyType({ - meta: { description: 'my description', id: 'foo', deprecated: true }, + meta: { description: 'my description', deprecated: true }, }); const meta = type.getSchema().describe(); expect(get(meta, 'flags.description')).toBe('my description'); - expect(get(meta, `metas[0].${META_FIELD_X_OAS_REF_ID}`)).toBe('foo'); - expect(get(meta, `metas[1].${META_FIELD_X_OAS_DEPRECATED}`)).toBe(true); + expect(get(meta, `metas[0].${META_FIELD_X_OAS_DEPRECATED}`)).toBe(true); }); it('does not set meta when no provided', () => { diff --git a/packages/kbn-config-schema/src/types/type.ts b/packages/kbn-config-schema/src/types/type.ts index 80ed3f90fdd2a8..652eae077d5aff 100644 --- a/packages/kbn-config-schema/src/types/type.ts +++ b/packages/kbn-config-schema/src/types/type.ts @@ -7,7 +7,7 @@ */ import type { AnySchema, CustomValidator, ErrorReport } from 'joi'; -import { META_FIELD_X_OAS_DEPRECATED, META_FIELD_X_OAS_REF_ID } from '../oas_meta_fields'; +import { META_FIELD_X_OAS_DEPRECATED } from '../oas_meta_fields'; import { SchemaTypeError, ValidationError } from '../errors'; import { Reference } from '../references'; @@ -24,11 +24,6 @@ export interface TypeMeta { * Whether this field is deprecated. */ deprecated?: boolean; - /** - * A string that uniquely identifies this schema. Used when generating OAS - * to create refs instead of inline schemas. - */ - id?: string; } export interface TypeOptions { @@ -112,9 +107,6 @@ export abstract class Type { if (options.meta.description) { schema = schema.description(options.meta.description); } - if (options.meta.id) { - schema = schema.meta({ [META_FIELD_X_OAS_REF_ID]: options.meta.id }); - } if (options.meta.deprecated) { schema = schema.meta({ [META_FIELD_X_OAS_DEPRECATED]: true }); } diff --git a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap index 6caf6b33de6422..fb99225cdaa1a9 100644 --- a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap +++ b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap @@ -3,12 +3,7 @@ exports[`generateOpenApiDocument @kbn/config-schema generates references in the expected format 1`] = ` Object { "components": Object { - "schemas": Object { - "my.name": Object { - "minLength": 1, - "type": "string", - }, - }, + "schemas": Object {}, "securitySchemes": Object { "apiKeyAuth": Object { "in": "header", @@ -46,7 +41,7 @@ Object { }, }, Object { - "description": undefined, + "description": "test", "in": "path", "name": "id", "required": true, @@ -63,7 +58,8 @@ Object { "additionalProperties": false, "properties": Object { "name": Object { - "$ref": "#/components/schemas/my.name", + "minLength": 1, + "type": "string", }, "other": Object { "type": "string", @@ -368,6 +364,105 @@ Object { } `; +exports[`generateOpenApiDocument @kbn/config-schema handles recursive schemas 1`] = ` +Object { + "components": Object { + "schemas": Object { + "recursive": Object { + "additionalProperties": false, + "properties": Object { + "name": Object { + "type": "string", + }, + "self": Object { + "$ref": "#/components/schemas/recursive", + }, + }, + "required": Array [ + "name", + "self", + ], + "type": "object", + }, + }, + "securitySchemes": Object { + "apiKeyAuth": Object { + "in": "header", + "name": "Authorization", + "type": "apiKey", + }, + "basicAuth": Object { + "scheme": "basic", + "type": "http", + }, + }, + }, + "externalDocs": undefined, + "info": Object { + "description": undefined, + "title": "test", + "version": "99.99.99", + }, + "openapi": "3.0.0", + "paths": Object { + "/recursive": Object { + "get": Object { + "operationId": "/recursive#0", + "parameters": Array [ + Object { + "description": "The version of the API to use", + "in": "header", + "name": "elastic-api-version", + "schema": Object { + "default": "2023-10-31", + "enum": Array [ + "2023-10-31", + ], + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json; Elastic-Api-Version=2023-10-31": Object { + "schema": Object { + "$ref": "#/components/schemas/recursive", + }, + }, + }, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json; Elastic-Api-Version=2023-10-31": Object { + "schema": Object { + "maxLength": 10, + "minLength": 1, + "type": "string", + }, + }, + }, + "description": "No description", + }, + }, + "summary": "", + }, + }, + }, + "security": Array [ + Object { + "basicAuth": Array [], + }, + ], + "servers": Array [ + Object { + "url": "https://test.oas", + }, + ], + "tags": undefined, +} +`; + exports[`generateOpenApiDocument unknown schema/validation produces the expected output 1`] = ` Object { "components": Object { diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts index 4ccc02f31c3552..9311faf88dec9d 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts @@ -7,9 +7,14 @@ */ import { generateOpenApiDocument } from './generate_oas'; -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; import { createTestRouters, createRouter, createVersionedRouter } from './generate_oas.test.util'; +interface RecursiveType { + name: string; + self: undefined | RecursiveType; +} + describe('generateOpenApiDocument', () => { describe('@kbn/config-schema', () => { it('generates the expected OpenAPI document', () => { @@ -30,8 +35,8 @@ describe('generateOpenApiDocument', () => { }); it('generates references in the expected format', () => { - const sharedIdSchema = schema.string({ minLength: 1, meta: { id: 'my.id' } }); - const sharedNameSchema = schema.string({ minLength: 1, meta: { id: 'my.name' } }); + const sharedIdSchema = schema.string({ minLength: 1, meta: { description: 'test' } }); + const sharedNameSchema = schema.string({ minLength: 1 }); const otherSchema = schema.object({ name: sharedNameSchema, other: schema.string() }); expect( generateOpenApiDocument( @@ -70,6 +75,52 @@ describe('generateOpenApiDocument', () => { ) ).toMatchSnapshot(); }); + + it('handles recursive schemas', () => { + const id = 'recursive'; + const recursiveSchema: Type = schema.object( + { + name: schema.string(), + self: schema.lazy(id), + }, + { meta: { id } } + ); + expect( + generateOpenApiDocument( + { + routers: [ + createRouter({ + routes: [ + { + isVersioned: false, + path: '/recursive', + method: 'get', + validationSchemas: { + request: { + body: recursiveSchema, + }, + response: { + [200]: { + body: () => schema.string({ maxLength: 10, minLength: 1 }), + }, + }, + }, + options: { tags: ['foo'] }, + handler: jest.fn(), + }, + ], + }), + ], + versionedRouters: [], + }, + { + title: 'test', + baseUrl: 'https://test.oas', + version: '99.99.99', + } + ) + ).toMatchSnapshot(); + }); }); describe('unknown schema/validation', () => { diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.test.ts index 59336057683787..9e43b45ddc64cb 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.test.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.test.ts @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import { schema, metaFields } from '@kbn/config-schema'; -import { set } from '@kbn/safer-lodash-set'; -import { omit } from 'lodash'; -import { OpenAPIV3 } from 'openapi-types'; -import { is, tryConvertToRef, isNullableObjectType } from './lib'; +import { schema } from '@kbn/config-schema'; +import { is, isNullableObjectType } from './lib'; describe('is', () => { test.each([ @@ -34,27 +31,6 @@ describe('is', () => { }); }); -test('tryConvertToRef', () => { - const schemaObject: OpenAPIV3.SchemaObject = { - type: 'object', - properties: { - a: { - type: 'string', - }, - }, - }; - set(schemaObject, metaFields.META_FIELD_X_OAS_REF_ID, 'foo'); - expect(tryConvertToRef(schemaObject)).toEqual({ - idSchema: ['foo', { type: 'object', properties: { a: { type: 'string' } } }], - ref: { - $ref: '#/components/schemas/foo', - }, - }); - - const schemaObject2 = omit(schemaObject, metaFields.META_FIELD_X_OAS_REF_ID); - expect(tryConvertToRef(schemaObject2)).toBeUndefined(); -}); - test('isNullableObjectType', () => { const any = schema.any({}); expect(isNullableObjectType(any.getSchema().describe())).toBe(false); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.ts index f5ab73d69dc533..4c7f8c999cbf4f 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/lib.ts @@ -7,7 +7,7 @@ */ import joi from 'joi'; -import { isConfigSchema, Type, metaFields } from '@kbn/config-schema'; +import { isConfigSchema, Type } from '@kbn/config-schema'; import { get } from 'lodash'; import type { OpenAPIV3 } from 'openapi-types'; import type { KnownParameters } from '../../type'; @@ -16,38 +16,6 @@ import { parse } from './parse'; import { createCtx, IContext } from './post_process_mutations'; -export const getSharedComponentId = (schema: OpenAPIV3.SchemaObject) => { - if (metaFields.META_FIELD_X_OAS_REF_ID in schema) { - return schema[metaFields.META_FIELD_X_OAS_REF_ID] as string; - } -}; - -export const removeSharedComponentId = ( - schema: OpenAPIV3.SchemaObject & { [metaFields.META_FIELD_X_OAS_REF_ID]?: string } -) => { - const { [metaFields.META_FIELD_X_OAS_REF_ID]: id, ...rest } = schema; - return rest; -}; - -export const sharedComponentIdToRef = (id: string): OpenAPIV3.ReferenceObject => { - return { - $ref: `#/components/schemas/${id}`, - }; -}; - -type IdSchemaTuple = [id: string, schema: OpenAPIV3.SchemaObject]; - -export const tryConvertToRef = (schema: OpenAPIV3.SchemaObject) => { - const sharedId = getSharedComponentId(schema); - if (sharedId) { - const idSchema: IdSchemaTuple = [sharedId, removeSharedComponentId(schema)]; - return { - idSchema, - ref: sharedComponentIdToRef(sharedId), - }; - } -}; - const isObjectType = (schema: joi.Schema | joi.Description): boolean => { return schema.type === 'object'; }; @@ -100,8 +68,8 @@ export const unwrapKbnConfigSchema = (schema: unknown): joi.Schema => { export const convert = (kbnConfigSchema: unknown) => { const schema = unwrapKbnConfigSchema(kbnConfigSchema); - const { result, shared } = parse({ schema, ctx: createCtx({ refs: true }) }); - return { schema: result, shared: Object.fromEntries(shared.entries()) }; + const { result, shared } = parse({ schema, ctx: createCtx() }); + return { schema: result, shared }; }; const convertObjectMembersToParameterObjects = ( @@ -152,11 +120,11 @@ const convertObjectMembersToParameterObjects = ( export const convertQuery = (kbnConfigSchema: unknown) => { const schema = unwrapKbnConfigSchema(kbnConfigSchema); - const ctx = createCtx({ refs: false }); // For now context is not shared between body, params and queries + const ctx = createCtx(); const result = convertObjectMembersToParameterObjects(ctx, schema, {}, false); return { query: result, - shared: Object.fromEntries(ctx.sharedSchemas.entries()), + shared: ctx.getSharedSchemas(), }; }; @@ -172,7 +140,7 @@ export const convertPathParameters = ( const result = convertObjectMembersToParameterObjects(ctx, schema, knownParameters, true); return { params: result, - shared: Object.fromEntries(ctx.sharedSchemas.entries()), + shared: ctx.getSharedSchemas(), }; }; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.test.ts new file mode 100644 index 00000000000000..7e9e8419d2869e --- /dev/null +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { isJoiToJsonSpecialSchemas, joi2JsonInternal } from './parse'; + +describe('isJoiToJsonSpecialSchemas', () => { + test.each([ + [joi2JsonInternal(schema.object({ foo: schema.string() }).getSchema()), false], + [ + joi2JsonInternal( + schema.object({ foo: schema.string() }, { meta: { id: 'yes' } }).getSchema() + ), + true, + ], + [{}, false], + ])('correctly detects special schemas %#', (input, output) => { + expect(isJoiToJsonSpecialSchemas(input)).toBe(output); + }); +}); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts index f25cc1a4c354ab..dc2349f511a05d 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/parse.ts @@ -9,6 +9,7 @@ import Joi from 'joi'; import joiToJsonParse from 'joi-to-json'; import type { OpenAPIV3 } from 'openapi-types'; +import { omit } from 'lodash'; import { createCtx, postProcessMutations } from './post_process_mutations'; import type { IContext } from './post_process_mutations'; @@ -17,13 +18,34 @@ interface ParseArgs { ctx?: IContext; } +export interface JoiToJsonReferenceObject extends OpenAPIV3.BaseSchemaObject { + schemas: { [id: string]: OpenAPIV3.SchemaObject }; +} + +type ParseResult = OpenAPIV3.SchemaObject | JoiToJsonReferenceObject; + +export const isJoiToJsonSpecialSchemas = ( + parseResult: ParseResult +): parseResult is JoiToJsonReferenceObject => { + return 'schemas' in parseResult; +}; + export const joi2JsonInternal = (schema: Joi.Schema) => { return joiToJsonParse(schema, 'open-api'); }; export const parse = ({ schema, ctx = createCtx() }: ParseArgs) => { - const parsed: OpenAPIV3.SchemaObject = joi2JsonInternal(schema); - postProcessMutations({ schema: parsed, ctx }); - const result = ctx.processRef(parsed); - return { shared: ctx.sharedSchemas, result }; + const parsed: ParseResult = joi2JsonInternal(schema); + let result: OpenAPIV3.SchemaObject; + if (isJoiToJsonSpecialSchemas(parsed)) { + Object.entries(parsed.schemas).forEach(([id, s]) => { + postProcessMutations({ schema: s, ctx }); + ctx.addSharedSchema(id, s); + }); + result = omit(parsed, 'schemas'); + } else { + result = parsed; + } + postProcessMutations({ schema: result, ctx }); + return { shared: ctx.getSharedSchemas(), result }; }; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.test.ts index 16f499f48b3716..3e2a8e62516c98 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.test.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.test.ts @@ -6,41 +6,22 @@ * Side Public License, v 1. */ -import { schema, metaFields } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { joi2JsonInternal } from '../parse'; import { createCtx } from './context'; -it('does not convert and record refs by default', () => { +it('records schemas as expected', () => { const ctx = createCtx(); - const obj = schema.object({}, { meta: { id: 'foo' } }); - const parsed = joi2JsonInternal(obj.getSchema()); - const result = ctx.processRef(parsed); - expect(result).toMatchObject({ type: 'object', properties: {} }); - expect(ctx.sharedSchemas.get('foo')).toBeUndefined(); - expect(metaFields.META_FIELD_X_OAS_REF_ID in result).toBe(false); -}); - -it('can convert and record refs', () => { - const ctx = createCtx({ refs: true }); - const obj = schema.object({}, { meta: { id: 'foo' } }); - const parsed = joi2JsonInternal(obj.getSchema()); - const result = ctx.processRef(parsed); - expect(result).toEqual({ $ref: '#/components/schemas/foo' }); - expect(ctx.sharedSchemas.get('foo')).toMatchObject({ type: 'object', properties: {} }); - expect(metaFields.META_FIELD_X_OAS_REF_ID in ctx.sharedSchemas.get('foo')!).toBe(false); -}); - -it('can use provided shared schemas Map', () => { - const myMap = new Map(); - const ctx = createCtx({ refs: true, sharedSchemas: myMap }); - const obj = schema.object({}, { meta: { id: 'foo' } }); - const parsed = joi2JsonInternal(obj.getSchema()); - ctx.processRef(parsed); + const objA = schema.object({}); + const objB = schema.object({}); + const a = joi2JsonInternal(objA.getSchema()); + const b = joi2JsonInternal(objB.getSchema()); - const obj2 = schema.object({}, { meta: { id: 'bar' } }); - const parsed2 = joi2JsonInternal(obj2.getSchema()); - ctx.processRef(parsed2); + ctx.addSharedSchema('a', a); + ctx.addSharedSchema('b', b); - expect(myMap.get('foo')).toMatchObject({ type: 'object', properties: {} }); - expect(myMap.get('bar')).toMatchObject({ type: 'object', properties: {} }); + expect(ctx.getSharedSchemas()).toMatchObject({ + a: { properties: {} }, + b: { properties: {} }, + }); }); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.ts index 0ae440edc622ee..2019d7d6224819 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/context.ts @@ -7,42 +7,27 @@ */ import type { OpenAPIV3 } from 'openapi-types'; -import { processRef as processRefMutation } from './mutations/ref'; -import { removeSharedComponentId } from '../lib'; export interface IContext { - sharedSchemas: Map; - /** - * Attempt to convert a schema object to ref, my perform side-effect - * - * Will return the schema sans the ref meta ID if refs are disabled - * - * @note see also {@link Options['refs']} - */ - processRef: ( - schema: OpenAPIV3.SchemaObject - ) => OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; + addSharedSchema: (id: string, schema: OpenAPIV3.SchemaObject) => void; + getSharedSchemas: () => { [id: string]: OpenAPIV3.SchemaObject }; } interface Options { sharedSchemas?: Map; - refs?: boolean; } class Context implements IContext { - readonly sharedSchemas: Map; - readonly refs: boolean; + private readonly sharedSchemas: Map; constructor(opts: Options) { this.sharedSchemas = opts.sharedSchemas ?? new Map(); - this.refs = !!opts.refs; } - public processRef( - schema: OpenAPIV3.SchemaObject - ): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject { - if (this.refs) { - return processRefMutation(this, schema) ?? schema; - } - return removeSharedComponentId(schema); + public addSharedSchema(id: string, schema: OpenAPIV3.SchemaObject): void { + this.sharedSchemas.set(id, schema); + } + + public getSharedSchemas() { + return Object.fromEntries(this.sharedSchemas.entries()); } } diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/index.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/index.ts index b58fbdf80de631..7d693f085ef174 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/index.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/index.ts @@ -10,9 +10,12 @@ import type { OpenAPIV3 } from 'openapi-types'; import * as mutations from './mutations'; import type { IContext } from './context'; import { isAnyType } from './mutations/utils'; +import { isReferenceObject } from '../../common'; + +type Schema = OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; interface PostProcessMutationsArgs { - schema: OpenAPIV3.SchemaObject; + schema: Schema; ctx: IContext; } @@ -23,7 +26,9 @@ export const postProcessMutations = ({ ctx, schema }: PostProcessMutationsArgs) const arrayContainers: Array = ['allOf', 'oneOf', 'anyOf']; -const walkSchema = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { +const walkSchema = (ctx: IContext, schema: Schema): void => { + if (isReferenceObject(schema)) return; + if (isAnyType(schema)) { mutations.processAnyType(schema); return; @@ -41,7 +46,7 @@ const walkSchema = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { walkSchema(ctx, value as OpenAPIV3.SchemaObject); }); } - mutations.processObject(ctx, schema); + mutations.processObject(schema); } else if (type === 'string') { mutations.processString(schema); } else if (type === 'record') { @@ -57,7 +62,6 @@ const walkSchema = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { if (schema[arrayContainer]) { schema[arrayContainer].forEach((s: OpenAPIV3.SchemaObject, idx: number) => { walkSchema(ctx, s); - schema[arrayContainer][idx] = ctx.processRef(s); }); break; } diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.test.ts index cccd06855e8c4e..392bca884e38e3 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.test.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.test.ts @@ -15,18 +15,23 @@ test.each([ [ 'processMap', processMap, - schema.mapOf(schema.string(), schema.object({ a: schema.string() }, { meta: { id: 'myRef' } })), + schema.mapOf( + schema.string(), + schema.object({ a: schema.string() }, { meta: { id: 'myRef1' } }) + ), + 'myRef1', ], [ 'processRecord', processRecord, schema.recordOf( schema.string(), - schema.object({ a: schema.string() }, { meta: { id: 'myRef' } }) + schema.object({ a: schema.string() }, { meta: { id: 'myRef2' } }) ), + 'myRef2', ], -])('%p parses any additional properties specified', (_, processFn, obj) => { - const ctx = createCtx({ refs: true }); +])('%p parses any additional properties specified', (_, processFn, obj, refId) => { + const ctx = createCtx(); const parsed = joi2JsonInternal(obj.getSchema()); processFn(ctx, parsed); @@ -34,17 +39,19 @@ test.each([ expect(parsed).toEqual({ type: 'object', additionalProperties: { - $ref: '#/components/schemas/myRef', + $ref: `#/components/schemas/${refId}`, }, }); - expect(ctx.sharedSchemas.get('myRef')).toEqual({ - type: 'object', - additionalProperties: false, - properties: { - a: { - type: 'string', + expect(ctx.getSharedSchemas()).toEqual({ + [refId]: { + type: 'object', + additionalProperties: false, + properties: { + a: { + type: 'string', + }, }, + required: ['a'], }, - required: ['a'], }); }); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts index 02dd08b928a54a..0dd6cb5dc2f841 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/index.ts @@ -48,21 +48,11 @@ const processAdditionalProperties = (ctx: IContext, schema: OpenAPIV3.SchemaObje export const processRecord = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { schema.type = 'object'; processAdditionalProperties(ctx, schema); - if (schema.additionalProperties) { - schema.additionalProperties = ctx.processRef( - schema.additionalProperties as OpenAPIV3.SchemaObject - ); - } }; export const processMap = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { schema.type = 'object'; processAdditionalProperties(ctx, schema); - if (schema.additionalProperties) { - schema.additionalProperties = ctx.processRef( - schema.additionalProperties as OpenAPIV3.SchemaObject - ); - } }; export const processAllTypes = (schema: OpenAPIV3.SchemaObject): void => { diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.test.ts index ea54a041972774..fd7b127ef87260 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.test.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.test.ts @@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema'; import { joi2JsonInternal } from '../../parse'; -import { createCtx } from '../context'; import { processObject } from './object'; test.each([ @@ -41,36 +40,6 @@ test.each([ ], ])('processObject %#', (input, result) => { const parsed = joi2JsonInternal(input.getSchema()); - processObject(createCtx(), parsed); + processObject(parsed); expect(parsed).toEqual(result); }); - -test('refs', () => { - const fooSchema = schema.object({ n: schema.number() }, { meta: { id: 'foo' } }); - const barSchema = schema.object({ foo: fooSchema, s: schema.string() }); - const parsed = joi2JsonInternal(barSchema.getSchema()); - const ctx = createCtx({ refs: true }); - - // Simulate us walking the schema - processObject(ctx, parsed.properties.foo); - - processObject(ctx, parsed); - expect(parsed).toEqual({ - type: 'object', - additionalProperties: false, - properties: { - foo: { - $ref: '#/components/schemas/foo', - }, - s: { type: 'string' }, - }, - required: ['foo', 's'], - }); - expect(ctx.sharedSchemas.size).toBe(1); - expect(ctx.sharedSchemas.get('foo')).toEqual({ - type: 'object', - additionalProperties: false, - properties: { n: { type: 'number' } }, - required: ['n'], - }); -}); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts index 70b9b2574615b2..ec5888f986bd60 100644 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts +++ b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/object.ts @@ -9,7 +9,6 @@ import type { OpenAPIV3 } from 'openapi-types'; import { metaFields } from '@kbn/config-schema'; import { deleteField, stripBadDefault } from './utils'; -import { IContext } from '../context'; const { META_FIELD_X_OAS_OPTIONAL } = metaFields; @@ -51,17 +50,8 @@ const removeNeverType = (schema: OpenAPIV3.SchemaObject): void => { } }; -const processObjectRefs = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { - if (schema.properties) { - Object.keys(schema.properties).forEach((key) => { - schema.properties![key] = ctx.processRef(schema.properties![key] as OpenAPIV3.SchemaObject); - }); - } -}; - -export const processObject = (ctx: IContext, schema: OpenAPIV3.SchemaObject): void => { +export const processObject = (schema: OpenAPIV3.SchemaObject): void => { stripBadDefault(schema); removeNeverType(schema); populateRequiredFields(schema); - processObjectRefs(ctx, schema); }; diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.test.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.test.ts deleted file mode 100644 index 539e3cd339db6f..00000000000000 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { schema } from '@kbn/config-schema'; -import { createCtx } from '../context'; -import { joi2JsonInternal } from '../../parse'; -import { processRef } from './ref'; - -test('create a new ref entry', () => { - const ctx = createCtx({ refs: true }); - const obj = schema.object({ a: schema.string() }, { meta: { id: 'id' } }); - const parsed = joi2JsonInternal(obj.getSchema()); - const result = processRef(ctx, parsed); - expect(result).toEqual({ - $ref: '#/components/schemas/id', - }); - expect(ctx.sharedSchemas.get('id')).toMatchObject({ - type: 'object', - properties: { - a: { - type: 'string', - }, - }, - }); -}); diff --git a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.ts b/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.ts deleted file mode 100644 index 4b36a0245e96d4..00000000000000 --- a/packages/kbn-router-to-openapispec/src/oas_converter/kbn_config_schema/post_process_mutations/mutations/ref.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 type { OpenAPIV3 } from 'openapi-types'; -import { tryConvertToRef } from '../../lib'; -import type { IContext } from '../context'; - -export const processRef = (ctx: IContext, schema: OpenAPIV3.SchemaObject) => { - const result = tryConvertToRef(schema); - if (result) { - const [id, s] = result.idSchema; - ctx.sharedSchemas.set(id, s); - return result.ref; - } -}; diff --git a/packages/kbn-router-to-openapispec/tsconfig.json b/packages/kbn-router-to-openapispec/tsconfig.json index 7977b83701cfde..b157378320c79a 100644 --- a/packages/kbn-router-to-openapispec/tsconfig.json +++ b/packages/kbn-router-to-openapispec/tsconfig.json @@ -17,6 +17,5 @@ "@kbn/core-http-router-server-internal", "@kbn/config-schema", "@kbn/core-http-server", - "@kbn/safer-lodash-set", ] }