diff --git a/docs/usage.md b/docs/usage.md index 6410d93c33..e37dd4a4c8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -143,6 +143,7 @@ There are one way to generate models from an OpenAPI document - [Generate from OpenAPI 3.0 JS object](../examples/openapi-from-object) - [Generate from OpenAPI 3.1 JS object](../examples/openapi-v3_1-from-object) +- [Generate from OpenAPI components](../examples/openapi-include-components) The OpenAPI input processor expects that the property `openapi` is defined in order to know it should be processed. diff --git a/examples/README.md b/examples/README.md index 6a0f7056e5..750a0c08a1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,9 @@ These examples show a specific input and how they can be used: - [json-schema-single-enum-as-const](./json-schema-single-enum-as-const) - An advanced example that shows how to change how `enum` are interpreted when containing a single value. - [json-schema-additional-properties-representation](./json-schema-additional-properties-representation) - An advanced example that shows how to change the property name for additional properties - [swagger2.0-from-object](./swagger2.0-from-object) - A basic example where a Swagger 2.0 JS object is used to generate models. +- [openapi-from-object](./openapi-from-object) - A basic example where an OpenAPI v3.0 JS object is used to generate models. +- [openapi-include-components](./openapi-include-components) - A basic example where an OpenAPI document without paths is used to generate models by iterating the components. +- [openapi-v3_1-from-object](./openapi-v3_1-from-object) - A basic example where an OpenAPI v3.1 JS object is used to generate models. - [meta-model](./meta-model) - A basic example how to provide a meta model for the generator ## General examples diff --git a/examples/openapi-include-components/README.md b/examples/openapi-include-components/README.md new file mode 100644 index 0000000000..c2fde6c48b --- /dev/null +++ b/examples/openapi-include-components/README.md @@ -0,0 +1,17 @@ +# OpenAPI include components + +A basic example of how to use Modelina with an OpenAPI document that iterates the components section to determine models. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/openapi-include-components/__snapshots__/index.spec.ts.snap b/examples/openapi-include-components/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..6e0b07de01 --- /dev/null +++ b/examples/openapi-include-components/__snapshots__/index.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to process a OpenAPI document that has no paths and should log expected output to console 1`] = ` +Array [ + "class TestSchema { + private _email?: string; + + constructor(input: { + email?: string, + }) { + this._email = input.email; + } + + get email(): string | undefined { return this._email; } + set email(email: string | undefined) { this._email = email; } +}", +] +`; diff --git a/examples/openapi-include-components/index.spec.ts b/examples/openapi-include-components/index.spec.ts new file mode 100644 index 0000000000..50a91746fd --- /dev/null +++ b/examples/openapi-include-components/index.spec.ts @@ -0,0 +1,14 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { + return; +}); +import { generate } from './index'; +describe('Should be able to process a OpenAPI document that has no paths', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/openapi-include-components/index.ts b/examples/openapi-include-components/index.ts new file mode 100644 index 0000000000..7c94ed2bea --- /dev/null +++ b/examples/openapi-include-components/index.ts @@ -0,0 +1,39 @@ +import { TypeScriptGenerator } from '../../src'; + +const generator = new TypeScriptGenerator({ + processorOptions: { openapi: { includeComponentSchemas: true } } +}); +const swaggerDocument = { + openapi: '3.0.3', + info: { + version: '0.1', + title: 'Simple basic api' + }, + paths: {}, + components: { + schemas: { + TestSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + additionalProperties: false, + properties: { + email: { + type: 'string', + format: 'email' + } + } + } + } + } +}; + +export async function generate(): Promise { + const models = await generator.generate(swaggerDocument); + for (const model of models) { + console.log(model.result); + } +} + +if (require.main === module) { + generate(); +} diff --git a/examples/openapi-include-components/package-lock.json b/examples/openapi-include-components/package-lock.json new file mode 100644 index 0000000000..4a70e6387e --- /dev/null +++ b/examples/openapi-include-components/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "openapi-include-components", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/openapi-include-components/package.json b/examples/openapi-include-components/package.json new file mode 100644 index 0000000000..3449a167dc --- /dev/null +++ b/examples/openapi-include-components/package.json @@ -0,0 +1,10 @@ +{ + "config" : { "example_name" : "openapi-include-components" }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} diff --git a/src/models/ProcessorOptions.ts b/src/models/ProcessorOptions.ts index abdb729699..a4024ffd17 100644 --- a/src/models/ProcessorOptions.ts +++ b/src/models/ProcessorOptions.ts @@ -2,11 +2,13 @@ import { ParseOptions } from '@asyncapi/parser'; import { InterpreterOptions } from '../interpreter/Interpreter'; import { JsonSchemaProcessorOptions, + OpenAPIInputProcessorOptions, TypeScriptInputProcessorOptions } from '../processors/index'; export interface ProcessorOptions { asyncapi?: ParseOptions; + openapi?: OpenAPIInputProcessorOptions; typescript?: TypeScriptInputProcessorOptions; jsonSchema?: JsonSchemaProcessorOptions; /** diff --git a/src/processors/OpenAPIInputProcessor.ts b/src/processors/OpenAPIInputProcessor.ts index bfbf3707de..09a505e9c9 100644 --- a/src/processors/OpenAPIInputProcessor.ts +++ b/src/processors/OpenAPIInputProcessor.ts @@ -4,15 +4,18 @@ import { InputMetaModel, OpenapiV3Schema, ProcessorOptions } from '../models'; import { Logger } from '../utils'; import SwaggerParser from '@apidevtools/swagger-parser'; import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; +export interface OpenAPIInputProcessorOptions { + includeComponentSchemas?: boolean; +} /** - * Class for processing OpenAPI V3.0 inputs + * Class for processing OpenAPI inputs */ export class OpenAPIInputProcessor extends AbstractInputProcessor { static supportedVersions = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']; /** - * Process the input as a OpenAPI V3.0 document + * Process the input as a OpenAPI document * * @param input */ @@ -32,12 +35,25 @@ export class OpenAPIInputProcessor extends AbstractInputProcessor { const api = (await SwaggerParser.dereference(input as any)) as unknown as | OpenAPIV3.Document | OpenAPIV3_1.Document; - if (api && api.paths) { for (const [path, pathObject] of Object.entries(api.paths)) { this.processPath(pathObject, path, inputModel, options); } } + if (options?.openapi?.includeComponentSchemas) { + for (const [schemaName, schemaObject] of Object.entries( + api.components?.schemas ?? {} + )) { + if ((schemaObject as any)['$ref'] === undefined) { + this.includeSchema( + schemaObject as OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject, + schemaName, + inputModel, + options + ); + } + } + } return inputModel; } diff --git a/test/processors/OpenAPIInputProcessor.spec.ts b/test/processors/OpenAPIInputProcessor.spec.ts index cbe6f0eebe..8bfcaa4bdd 100644 --- a/test/processors/OpenAPIInputProcessor.spec.ts +++ b/test/processors/OpenAPIInputProcessor.spec.ts @@ -8,6 +8,12 @@ const basicDoc = JSON.parse( 'utf8' ) ); +const noPathsDoc = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, './OpenAPIInputProcessor/no_paths.json'), + 'utf8' + ) +); const circularDoc = JSON.parse( fs.readFileSync( path.resolve(__dirname, './OpenAPIInputProcessor/references_circular.json'), @@ -20,7 +26,7 @@ const processorSpy = jest.spyOn( 'convertToInternalSchema' ); const mockedReturnModels = [new CommonModel()]; -const mockedMetaModel = new AnyModel('', undefined); +const mockedMetaModel = new AnyModel('', undefined, {}); jest.mock('../../src/helpers/CommonModelToMetaModel', () => { return { convertToMetaModel: jest.fn().mockImplementation(() => { @@ -100,6 +106,14 @@ describe('OpenAPIInputProcessor', () => { expect(commonInputModel).toMatchSnapshot(); expect(processorSpy.mock.calls).toMatchSnapshot(); }); + test('should process the OpenAPI document with no paths', async () => { + const processor = new OpenAPIInputProcessor(); + const commonInputModel = await processor.process(noPathsDoc, { + openapi: { includeComponentSchemas: true } + }); + expect(commonInputModel).toMatchSnapshot(); + expect(processorSpy.mock.calls).toMatchSnapshot(); + }); test('should include schema for parameters', async () => { const doc = { openapi: '3.0.3', diff --git a/test/processors/OpenAPIInputProcessor/basic.json b/test/processors/OpenAPIInputProcessor/basic.json index 597df89552..44f0800d91 100644 --- a/test/processors/OpenAPIInputProcessor/basic.json +++ b/test/processors/OpenAPIInputProcessor/basic.json @@ -33,7 +33,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -44,7 +44,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -58,7 +58,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -68,7 +68,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -84,7 +84,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -96,7 +96,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -108,7 +108,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -120,7 +120,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -132,7 +132,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -144,7 +144,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -156,7 +156,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -168,7 +168,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -182,7 +182,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -194,7 +194,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -206,7 +206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -218,7 +218,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } }, @@ -230,7 +230,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/ApiResponse" + "$ref": "#/components/schemas/ApiResponse" } } } @@ -240,18 +240,20 @@ } }, "components": { - "ApiResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string" - }, - "message": { - "type": "string" + "schemas": { + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } } } } diff --git a/test/processors/OpenAPIInputProcessor/no_paths.json b/test/processors/OpenAPIInputProcessor/no_paths.json new file mode 100644 index 0000000000..f70fd5ca55 --- /dev/null +++ b/test/processors/OpenAPIInputProcessor/no_paths.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple basic api with no paths" + }, + "paths": {}, + "components": { + "schemas": { + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/processors/__snapshots__/OpenAPIInputProcessor.spec.ts.snap b/test/processors/__snapshots__/OpenAPIInputProcessor.spec.ts.snap index 9995a62b95..3a027eb60f 100644 --- a/test/processors/__snapshots__/OpenAPIInputProcessor.spec.ts.snap +++ b/test/processors/__snapshots__/OpenAPIInputProcessor.spec.ts.snap @@ -5,7 +5,7 @@ InputMetaModel { "models": Object { "": AnyModel { "name": "", - "options": undefined, + "options": Object {}, "originalInput": undefined, }, }, @@ -138,26 +138,28 @@ InputMetaModel { "models": Object { "": AnyModel { "name": "", - "options": undefined, + "options": Object {}, "originalInput": undefined, }, }, "originalInput": Object { "components": Object { - "ApiResponse": Object { - "properties": Object { - "code": Object { - "format": "int32", - "type": "integer", - }, - "message": Object { - "type": "string", - }, - "type": Object { - "type": "string", + "schemas": Object { + "ApiResponse": Object { + "properties": Object { + "code": Object { + "format": "int32", + "type": "integer", + }, + "message": Object { + "type": "string", + }, + "type": Object { + "type": "string", + }, }, + "type": "object", }, - "type": "object", }, }, "info": Object { @@ -933,3 +935,64 @@ Array [ ], ] `; + +exports[`OpenAPIInputProcessor process() should process the OpenAPI document with no paths 1`] = ` +InputMetaModel { + "models": Object { + "": AnyModel { + "name": "", + "options": Object {}, + "originalInput": undefined, + }, + }, + "originalInput": Object { + "components": Object { + "schemas": Object { + "ApiResponse": Object { + "properties": Object { + "code": Object { + "format": "int32", + "type": "integer", + }, + "message": Object { + "type": "string", + }, + "type": Object { + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "info": Object { + "title": "Simple basic api with no paths", + }, + "openapi": "3.0.3", + "paths": Object {}, + }, +} +`; + +exports[`OpenAPIInputProcessor process() should process the OpenAPI document with no paths 2`] = ` +Array [ + Array [ + Object { + "properties": Object { + "code": Object { + "format": "int32", + "type": "integer", + }, + "message": Object { + "type": "string", + }, + "type": Object { + "type": "string", + }, + }, + "type": "object", + }, + "ApiResponse", + ], +] +`;