Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,32 @@ import { OpenAPIV3 } from 'openapi-types';

export function createBlankOpenApiDocument(
oasVersion: string,
info: OpenAPIV3.InfoObject
overrides?: Partial<OpenAPIV3.Document>
): OpenAPIV3.Document {
return {
openapi: oasVersion,
info,
servers: [
info: overrides?.info ?? {
title: 'Merged OpenAPI specs',
version: 'not specified',
},
paths: overrides?.paths ?? {},
components: {
schemas: overrides?.components?.schemas,
responses: overrides?.components?.responses,
parameters: overrides?.components?.parameters,
examples: overrides?.components?.examples,
requestBodies: overrides?.components?.requestBodies,
headers: overrides?.components?.headers,
securitySchemes: overrides?.components?.securitySchemes ?? {
BasicAuth: {
type: 'http',
scheme: 'basic',
},
},
links: overrides?.components?.links,
callbacks: overrides?.components?.callbacks,
},
servers: overrides?.servers ?? [
{
url: 'http://{kibana_host}:{port}',
variables: {
Expand All @@ -28,19 +48,12 @@ export function createBlankOpenApiDocument(
},
},
],
security: [
security: overrides?.security ?? [
{
BasicAuth: [],
},
],
paths: {},
components: {
securitySchemes: {
BasicAuth: {
type: 'http',
scheme: 'basic',
},
},
},
tags: overrides?.tags,
externalDocs: overrides?.externalDocs,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import { mergeTags } from './merge_tags';
import { getOasVersion } from '../../utils/get_oas_version';
import { getOasDocumentVersion } from '../../utils/get_oas_document_version';
import { enrichWithVersionMimeParam } from './enrich_with_version_mime_param';
import { MergeOptions } from './merge_options';

export interface MergeDocumentsOptions {
interface MergeDocumentsOptions extends MergeOptions {
splitDocumentsByVersion: boolean;
}

Expand Down Expand Up @@ -52,10 +53,20 @@ export async function mergeDocuments(
...documentsGroup,
];

mergedDocument.servers = mergeServers(documentsToMerge);
mergedDocument.paths = mergePaths(documentsToMerge);
mergedDocument.components = mergeSharedComponents(documentsToMerge);
mergedDocument.security = mergeSecurityRequirements(documentsToMerge);
mergedDocument.paths = mergePaths(documentsToMerge, options);
mergedDocument.components = {
...mergedDocument.components,
...mergeSharedComponents(documentsToMerge, options),
};

if (!options.skipServers) {
mergedDocument.servers = mergeServers(documentsToMerge);
}

if (!options.skipSecurity) {
mergedDocument.security = mergeSecurityRequirements(documentsToMerge);
}

mergedDocument.tags = mergeTags(documentsToMerge);

mergedByVersion.set(mergedDocument.info.version, mergedDocument);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import deepEqual from 'fast-deep-equal';
import { OpenAPIV3 } from 'openapi-types';
import { KNOWN_HTTP_METHODS } from './http_methods';
import { isRefNode } from '../process_document';
import { MergeOptions } from './merge_options';

export function mergeOperations(
sourcePathItem: OpenAPIV3.PathItemObject,
mergedPathItem: OpenAPIV3.PathItemObject
mergedPathItem: OpenAPIV3.PathItemObject,
options: MergeOptions
) {
for (const httpMethod of KNOWN_HTTP_METHODS) {
const sourceOperation = sourcePathItem[httpMethod];
Expand All @@ -24,12 +26,18 @@ export function mergeOperations(
continue;
}

if (!mergedOperation || deepEqual(sourceOperation, mergedOperation)) {
mergedPathItem[httpMethod] = sourceOperation;
const normalizedSourceOperation = {
...sourceOperation,
...(options.skipServers ? { servers: undefined } : { servers: sourceOperation.servers }),
...(options.skipSecurity ? { security: undefined } : { security: sourceOperation.security }),
};

if (!mergedOperation || deepEqual(normalizedSourceOperation, mergedOperation)) {
mergedPathItem[httpMethod] = normalizedSourceOperation;
continue;
}

mergeOperation(sourceOperation, mergedOperation);
mergeOperation(normalizedSourceOperation, mergedOperation);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.
*/

export interface MergeOptions {
skipServers: boolean;
skipSecurity: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import { ResolvedDocument } from '../ref_resolver/resolved_document';
import { isRefNode } from '../process_document';
import { mergeOperations } from './merge_operations';
import { mergeArrays } from './merge_arrays';
import { MergeOptions } from './merge_options';

export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.PathsObject {
export function mergePaths(
resolvedDocuments: ResolvedDocument[],
options: MergeOptions
): OpenAPIV3.PathsObject {
const mergedPaths: Record<string, OpenAPIV3.PathItemObject> = {};

for (const { absolutePath, document } of resolvedDocuments) {
Expand Down Expand Up @@ -60,7 +64,7 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat
}

try {
mergeOperations(sourcePathItem, mergedPathItem);
mergeOperations(sourcePathItem, mergedPathItem, options);
} catch (e) {
throw new Error(
`❌ Unable to merge ${chalk.bold(absolutePath)} due to an error in ${chalk.bold(
Expand All @@ -69,7 +73,9 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat
);
}

mergePathItemServers(sourcePathItem, mergedPathItem);
if (!options.skipServers) {
mergePathItemServers(sourcePathItem, mergedPathItem);
}

try {
mergeParameters(sourcePathItem, mergedPathItem);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OpenAPIV3 } from 'openapi-types';
import { ResolvedDocument } from '../ref_resolver/resolved_document';
import { extractObjectByJsonPointer } from '../../utils/extract_by_json_pointer';
import { logger } from '../../logger';
import { MergeOptions } from './merge_options';

const MERGEABLE_COMPONENT_TYPES = [
'schemas',
Expand All @@ -26,11 +27,16 @@ const MERGEABLE_COMPONENT_TYPES = [
] as const;

export function mergeSharedComponents(
bundledDocuments: ResolvedDocument[]
bundledDocuments: ResolvedDocument[],
options: MergeOptions
): OpenAPIV3.ComponentsObject {
const mergedComponents: Record<string, unknown> = {};

for (const componentsType of MERGEABLE_COMPONENT_TYPES) {
if (options.skipSecurity && componentsType === 'securitySchemes') {
continue;
}

const mergedTypedComponents = mergeObjects(bundledDocuments, `/components/${componentsType}`);

if (Object.keys(mergedTypedComponents).length === 0) {
Expand Down
46 changes: 30 additions & 16 deletions packages/kbn-openapi-bundler/src/openapi_bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
*/

import chalk from 'chalk';
import { isUndefined, omitBy } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
import { basename, dirname } from 'path';
import { bundleDocument, SkipException } from './bundler/bundle_document';
import { mergeDocuments } from './bundler/merge_documents';
Expand All @@ -19,6 +17,8 @@ import { writeDocuments } from './utils/write_documents';
import { ResolvedDocument } from './bundler/ref_resolver/resolved_document';
import { resolveGlobs } from './utils/resolve_globs';
import { DEFAULT_BUNDLING_PROCESSORS, withIncludeLabelsProcessor } from './bundler/processor_sets';
import { PrototypeDocument } from './prototype_document';
import { validatePrototypeDocument } from './validate_prototype_document';

export interface BundlerConfig {
sourceGlob: string;
Expand All @@ -27,15 +27,26 @@ export interface BundlerConfig {
}

interface BundleOptions {
/**
* OpenAPI document itself or path to the document
*/
prototypeDocument?: PrototypeDocument | string;
/**
* When specified the produced bundle will contain only
* operations objects with matching labels
*/
includeLabels?: string[];
specInfo?: Omit<Partial<OpenAPIV3.InfoObject>, 'version'>;
}

export const bundle = async ({
sourceGlob,
outputFilePath = 'bundled-{version}.schema.yaml',
options,
}: BundlerConfig) => {
const prototypeDocument = options?.prototypeDocument
? await validatePrototypeDocument(options?.prototypeDocument)
: undefined;

logger.debug(chalk.bold(`Bundling API route schemas`));
logger.debug(`👀 Searching for source files in ${chalk.underline(sourceGlob)}`);

Expand All @@ -56,22 +67,21 @@ export const bundle = async ({

logger.success(`Processed ${bundledDocuments.length} schemas`);

const blankOasFactory = (oasVersion: string, apiVersion: string) =>
const blankOasDocumentFactory = (oasVersion: string, apiVersion: string) =>
createBlankOpenApiDocument(oasVersion, {
version: apiVersion,
title: options?.specInfo?.title ?? 'Bundled OpenAPI specs',
...omitBy(
{
description: options?.specInfo?.description,
termsOfService: options?.specInfo?.termsOfService,
contact: options?.specInfo?.contact,
license: options?.specInfo?.license,
},
isUndefined
),
info: prototypeDocument?.info
? { ...DEFAULT_INFO, ...prototypeDocument.info, version: apiVersion }
: { ...DEFAULT_INFO, version: apiVersion },
servers: prototypeDocument?.servers,
security: prototypeDocument?.security,
components: {
securitySchemes: prototypeDocument?.components?.securitySchemes,
},
});
const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasFactory, {
const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, {
splitDocumentsByVersion: true,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
});

await writeDocuments(resultDocumentsMap, outputFilePath);
Expand Down Expand Up @@ -130,3 +140,7 @@ function filterOutSkippedDocuments(

return processedDocuments;
}

const DEFAULT_INFO = {
title: 'Bundled OpenAPI specs',
} as const;
35 changes: 28 additions & 7 deletions packages/kbn-openapi-bundler/src/openapi_merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import chalk from 'chalk';
import { OpenAPIV3 } from 'openapi-types';

import { mergeDocuments } from './bundler/merge_documents';
import { logger } from './logger';
import { createBlankOpenApiDocument } from './bundler/merge_documents/create_blank_oas_document';
Expand All @@ -16,13 +16,20 @@ import { writeDocuments } from './utils/write_documents';
import { resolveGlobs } from './utils/resolve_globs';
import { bundleDocument } from './bundler/bundle_document';
import { withNamespaceComponentsProcessor } from './bundler/processor_sets';
import { PrototypeDocument } from './prototype_document';
import { validatePrototypeDocument } from './validate_prototype_document';

export interface MergerConfig {
sourceGlobs: string[];
outputFilePath: string;
options?: {
mergedSpecInfo?: Partial<OpenAPIV3.InfoObject>;
};
options?: MergerOptions;
}

interface MergerOptions {
/**
* OpenAPI document itself or path to the document
*/
prototypeDocument?: PrototypeDocument | string;
}

export const merge = async ({
Expand All @@ -34,6 +41,10 @@ export const merge = async ({
throw new Error('As minimum one source glob is expected');
}

const prototypeDocument = options?.prototypeDocument
? await validatePrototypeDocument(options?.prototypeDocument)
: undefined;

logger.info(chalk.bold(`Merging OpenAPI specs`));
logger.info(
`👀 Searching for source files in ${sourceGlobs
Expand All @@ -52,13 +63,18 @@ export const merge = async ({

const blankOasDocumentFactory = (oasVersion: string) =>
createBlankOpenApiDocument(oasVersion, {
title: 'Merged OpenAPI specs',
version: 'not specified',
...(options?.mergedSpecInfo ?? {}),
info: prototypeDocument?.info ? { ...DEFAULT_INFO, ...prototypeDocument.info } : DEFAULT_INFO,
servers: prototypeDocument?.servers,
security: prototypeDocument?.security,
components: {
securitySchemes: prototypeDocument?.components?.securitySchemes,
},
});

const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, {
splitDocumentsByVersion: false,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
});
// Only one document is expected when `splitDocumentsByVersion` is set to `false`
const mergedDocument = Array.from(resultDocumentsMap.values())[0];
Expand All @@ -80,3 +96,8 @@ async function bundleDocuments(schemaFilePaths: string[]): Promise<ResolvedDocum
)
);
}

const DEFAULT_INFO = {
title: 'Merged OpenAPI specs',
version: 'not specified',
} as const;
29 changes: 29 additions & 0 deletions packages/kbn-openapi-bundler/src/prototype_document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { OpenAPIV3 } from 'openapi-types';

/**
* `PrototypeDocument` is used as a prototype for the result file. In the other words
* it provides a way to specify the following properties
*
* - `info` info object
* - `servers` servers used to replace `servers` in the source OpenAPI specs
* - `security` security requirements used to replace `security` in the source OpenAPI specs
* It must be specified together with `components.securitySchemes`.
*
* All the other properties will be ignored.
*/
export interface PrototypeDocument {
info?: Partial<OpenAPIV3.InfoObject>;
servers?: OpenAPIV3.ServerObject[];
security?: OpenAPIV3.SecurityRequirementObject[];
components?: {
securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject>;
};
}
Loading