Skip to content

Commit

Permalink
Extract OpeanAPI codegen to a package
Browse files Browse the repository at this point in the history
  • Loading branch information
xcrzx committed Sep 20, 2023
1 parent 8b21548 commit c783a58
Show file tree
Hide file tree
Showing 35 changed files with 428 additions and 192 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ x-pack/plugins/observability @elastic/actionable-observability
x-pack/plugins/observability_shared @elastic/observability-ui
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
test/common/plugins/otel_metrics @elastic/infra-monitoring-ui
packages/kbn-openapi-generator @elastic/security-detection-rule-management
packages/kbn-optimizer @elastic/kibana-operations
packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations
packages/kbn-osquery-io-ts-types @elastic/security-asset-management
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
"types": "./kibana.d.ts",
"tsdocMetadata": "./build/tsdoc-metadata.json",
"build": {
"date": "2023-05-15T23:12:09+0000",
"number": 8467,
"sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9",
"date": "2023-05-15T23:12:09+0000"
"sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9"
},
"homepage": "https://www.elastic.co/products/kibana",
"bugs": {
Expand Down Expand Up @@ -1204,6 +1204,7 @@
"@kbn/managed-vscode-config": "link:packages/kbn-managed-vscode-config",
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
"@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config",
"@kbn/openapi-generator": "link:packages/kbn-openapi-generator",
"@kbn/optimizer": "link:packages/kbn-optimizer",
"@kbn/optimizer-webpack-helpers": "link:packages/kbn-optimizer-webpack-helpers",
"@kbn/peggy": "link:packages/kbn-peggy",
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-openapi-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# kbn-openapi-generator
9 changes: 9 additions & 0 deletions packages/kbn-openapi-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 * from './src/openapi_generator';
13 changes: 13 additions & 0 deletions packages/kbn-openapi-generator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-openapi-generator'],
};
6 changes: 6 additions & 0 deletions packages/kbn-openapi-generator/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"devOnly": true,
"id": "@kbn/openapi-generator",
"owner": "@elastic/security-detection-engine",
"type": "shared-common"
}
7 changes: 7 additions & 0 deletions packages/kbn-openapi-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "OpenAPI code generator for Kibana",
"license": "SSPL-1.0 OR Elastic License 2.0",
"name": "@kbn/openapi-generator",
"private": true,
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 execa from 'execa';
import { resolve } from 'path';

const KIBANA_ROOT = resolve(__dirname, '../../../../../../');
import { REPO_ROOT } from '@kbn/repo-info';

export async function fixEslint(path: string) {
await execa('npx', ['eslint', '--fix', path], {
// Need to run eslint from the Kibana root directory, otherwise it will not
// be able to pick up the right config
cwd: KIBANA_ROOT,
cwd: REPO_ROOT,
});
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 execa from 'execa';
Expand Down
11 changes: 11 additions & 0 deletions packages/kbn-openapi-generator/src/lib/get_generated_file_path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 function getGeneratedFilePath(sourcePath: string) {
return sourcePath.replace(/\..+$/, '.gen.ts');
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 fs from 'fs/promises';
Expand Down
77 changes: 77 additions & 0 deletions packages/kbn-openapi-generator/src/openapi_generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.
*/

/* eslint-disable no-console */

import SwaggerParser from '@apidevtools/swagger-parser';
import chalk from 'chalk';
import fs from 'fs/promises';
import globby from 'globby';
import { resolve } from 'path';
import { fixEslint } from './lib/fix_eslint';
import { formatOutput } from './lib/format_output';
import { getGeneratedFilePath } from './lib/get_generated_file_path';
import { removeGenArtifacts } from './lib/remove_gen_artifacts';
import { getGenerationContext } from './parser/get_generation_context';
import type { OpenApiDocument } from './parser/openapi_types';
import { initTemplateService, TemplateName } from './template_service/template_service';

export interface GeneratorConfig {
rootDir: string;
sourceGlob: string;
templateName: TemplateName;
}

export const generate = async (config: GeneratorConfig) => {
const { rootDir, sourceGlob, templateName } = config;

console.log(chalk.bold(`Generating API route schemas`));
console.log(chalk.bold(`Working directory: ${chalk.underline(rootDir)}`));

console.log(`👀 Searching for source files`);
const sourceFilesGlob = resolve(rootDir, sourceGlob);
const schemaPaths = await globby([sourceFilesGlob]);

console.log(`🕵️‍♀️ Found ${schemaPaths.length} schemas, parsing`);
const parsedSources = await Promise.all(
schemaPaths.map(async (sourcePath) => {
const parsedSchema = (await SwaggerParser.parse(sourcePath)) as OpenApiDocument;
return { sourcePath, parsedSchema };
})
);

console.log(`🧹 Cleaning up any previously generated artifacts`);
await removeGenArtifacts(rootDir);

console.log(`🪄 Generating new artifacts`);
const TemplateService = await initTemplateService();
await Promise.all(
parsedSources.map(async ({ sourcePath, parsedSchema }) => {
const generationContext = getGenerationContext(parsedSchema);

// If there are no operations or components to generate, skip this file
const shouldGenerate =
generationContext.operations.length > 0 || generationContext.components !== undefined;
if (!shouldGenerate) {
return;
}

const result = TemplateService.compileTemplate(templateName, generationContext);

// Write the generation result to disk
await fs.writeFile(getGeneratedFilePath(sourcePath), result);
})
);

// Format the output folder using prettier as the generator produces
// unformatted code and fix any eslint errors
console.log(`💅 Formatting output`);
const generatedArtifactsGlob = resolve(rootDir, './**/*.gen.ts');
await formatOutput(generatedArtifactsGlob);
await fixEslint(generatedArtifactsGlob);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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';
import { getApiOperationsList } from './lib/get_api_operations_list';
import { getComponents } from './lib/get_components';
import { getImportsMap, ImportsMap } from './lib/get_imports_map';
import { normalizeSchema } from './lib/normalize_schema';
import { NormalizedOperation, OpenApiDocument } from './openapi_types';

export interface GenerationContext {
components: OpenAPIV3.ComponentsObject | undefined;
operations: NormalizedOperation[];
imports: ImportsMap;
}

export function getGenerationContext(document: OpenApiDocument): GenerationContext {
const normalizedDocument = normalizeSchema(document);

const components = getComponents(normalizedDocument);
const operations = getApiOperationsList(normalizedDocument);
const imports = getImportsMap(normalizedDocument);

return {
components,
operations,
imports,
};
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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';
import type { NormalizedOperation, ObjectSchema, OpenApiDocument } from './openapi_types';
import type {
NormalizedOperation,
NormalizedSchemaItem,
ObjectSchema,
OpenApiDocument,
} from '../openapi_types';

const HTTP_METHODS = Object.values(OpenAPIV3.HttpMethods);

Expand Down Expand Up @@ -61,14 +67,12 @@ export function getApiOperationsList(parsedSchema: OpenApiDocument): NormalizedO
`Cannot generate response for ${method} ${path}: $ref in response is not supported`
);
}
const response = operation.responses?.['200']?.content?.['application/json']?.schema;

if (operation.requestBody && '$ref' in operation.requestBody) {
throw new Error(
`Cannot generate request for ${method} ${path}: $ref in request body is not supported`
);
}
const requestBody = operation.requestBody?.content?.['application/json']?.schema;

const { operationId, description, tags, deprecated } = operation;

Expand All @@ -78,7 +82,13 @@ export function getApiOperationsList(parsedSchema: OpenApiDocument): NormalizedO
throw new Error(`Missing operationId for ${method} ${path}`);
}

return {
const response = operation.responses?.['200']?.content?.['application/json']?.schema as
| NormalizedSchemaItem
| undefined;
const requestBody = operation.requestBody?.content?.['application/json']?.schema as
| NormalizedSchemaItem
| undefined;
const normalizedOperation: NormalizedOperation = {
path,
method,
operationId,
Expand All @@ -90,6 +100,8 @@ export function getApiOperationsList(parsedSchema: OpenApiDocument): NormalizedO
requestBody,
response,
};

return normalizedOperation;
});
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 { OpenApiDocument } from './openapi_types';
import type { OpenApiDocument } from '../openapi_types';

export function getComponents(parsedSchema: OpenApiDocument) {
if (parsedSchema.components?.['x-codegen-enabled'] === false) {
return undefined;
}

return parsedSchema.components;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
* 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 { uniq } from 'lodash';
import type { OpenApiDocument } from './openapi_types';
import type { OpenApiDocument } from '../openapi_types';
import { traverseObject } from './traverse_object';

export interface ImportsMap {
[importPath: string]: string[];
Expand Down Expand Up @@ -55,23 +57,11 @@ const hasRef = (obj: unknown): obj is { $ref: string } => {
function findRefs(obj: unknown): string[] {
const refs: string[] = [];

function search(element: unknown) {
if (typeof element === 'object' && element !== null) {
if (hasRef(element)) {
refs.push(element.$ref);
}

Object.values(element).forEach((value) => {
if (Array.isArray(value)) {
value.forEach(search);
} else {
search(value);
}
});
traverseObject(obj, (element) => {
if (hasRef(element)) {
refs.push(element.$ref);
}
}

search(obj);
});

return refs;
}
Loading

0 comments on commit c783a58

Please sign in to comment.