From 7b72dbbf1baab3be051de2c96b2ab278251a0574 Mon Sep 17 00:00:00 2001 From: Brandon Faulkner <12897328+bfaulk96@users.noreply.github.com> Date: Tue, 8 Feb 2022 16:40:02 -0500 Subject: [PATCH 1/2] feat: add basePath and schemes to custom config --- README.md | 6 ++ src/ServerlessAutoSwagger.ts | 56 ++++++------ src/serverlessPlugin.d.ts | 7 +- src/swagger.d.ts | 2 +- tests/ServerlessAutoSwagger.test.ts | 136 +++++++++++++++++++++------- 5 files changed, 144 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 4ce8548..022c007 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ custom: swaggerPath?: 'string' apiKeyName?: 'string' useStage?: true | false + basePath?: '/string' + schemes?: ['http', 'https'] ``` `generateSwaggerOnDeploy` is a boolean which decides whether to generate a new swagger file on deployment. Default is `true`. @@ -52,6 +54,10 @@ custom: `useStage` is a bool to either use current stage in beginning of path or not. The Default is `false`. For example, if you use it enabled (`true`) and your stage is `dev` the swagger will be in `dev/swagger` +`basePath` is an optional string that can be prepended to every request (i.e. `http://localhost/basePath/my-endpoint`). Should include leading `/`. + +`schemes` is an optional array (containing one of `http`, `https`, `ws`, or `wss`) for specifying schemes. If not specified, uses the same scheme as the API specification (reflecting Swagger's behavior) + ## Adding more details The default swagger file from vanilla Serverless framework will have the correct paths and methods but no details about the requests or responses. diff --git a/src/ServerlessAutoSwagger.ts b/src/ServerlessAutoSwagger.ts index 4b191bc..23d022e 100644 --- a/src/ServerlessAutoSwagger.ts +++ b/src/ServerlessAutoSwagger.ts @@ -25,7 +25,6 @@ class ServerlessAutoSwagger { title: '', version: '1', }, - schemes: ['https'], paths: {}, definitions: {}, securityDefinitions: {}, @@ -160,35 +159,40 @@ class ServerlessAutoSwagger { this.addEndpointsAndLambda(); }; - gatherSwaggerFiles = async () => { - const swaggerFiles = this.serverless.service.custom?.autoswagger?.swaggerFiles; + /** Updates this.swagger with serverless custom.autoswagger overrides */ + gatherSwaggerOverrides = (): void => { + const autoswagger = this.serverless.service.custom?.autoswagger ?? {}; - if (!swaggerFiles || swaggerFiles.length < 1) { - return; - } + if (autoswagger.basePath) this.swagger.basePath = autoswagger.basePath + if (autoswagger.schemes) this.swagger.schemes = autoswagger.schemes - await Promise.all( - swaggerFiles.map(async (filepath) => { - const fileData = fs.readFileSync(filepath, 'utf8'); + // There must be at least one or this `if` will be false + if (autoswagger.swaggerFiles?.length) this.gatherSwaggerFiles(autoswagger.swaggerFiles) - const jsonData = JSON.parse(fileData); + } - const { paths = {}, definitions = {}, ...swagger } = jsonData; + /** Updates this.swagger with swagger file overrides */ + gatherSwaggerFiles = (swaggerFiles: string[]): void => { + swaggerFiles.forEach((filepath) => { + const fileData = fs.readFileSync(filepath, 'utf8'); - this.swagger = { - ...this.swagger, - ...swagger, - paths: { - ...this.swagger.paths, - ...paths, - }, - definitions: { - ...this.swagger.definitions, - ...definitions, - }, - }; - }) - ); + const jsonData = JSON.parse(fileData); + + const { paths = {}, definitions = {}, ...swagger } = jsonData; + + this.swagger = { + ...this.swagger, + ...swagger, + paths: { + ...this.swagger.paths, + ...paths, + }, + definitions: { + ...this.swagger.definitions, + ...definitions, + }, + }; + }); }; gatherTypes = async () => { @@ -259,7 +263,7 @@ class ServerlessAutoSwagger { }; generateSwagger = async () => { - await this.gatherSwaggerFiles(); + this.gatherSwaggerOverrides(); await this.gatherTypes(); this.generateSecurity(); this.generatePaths(); diff --git a/src/serverlessPlugin.d.ts b/src/serverlessPlugin.d.ts index cb3e330..9720571 100644 --- a/src/serverlessPlugin.d.ts +++ b/src/serverlessPlugin.d.ts @@ -8,7 +8,7 @@ export interface Serverless { provider?: { stage?: string } - } + }, configSchemaHandler: { defineCustomProperties(schema: unknown): void defineFunctionEvent( @@ -30,6 +30,9 @@ export interface Serverless { } } +// ws and wss are WebSocket schemas +type SwaggerScheme = 'http' | 'https' | 'ws' | 'wss' + export interface AutoSwaggerCustomConfig { autoswagger?: { apiKeyName?: string @@ -38,6 +41,8 @@ export interface AutoSwaggerCustomConfig { typefiles?: string[] useStage?: boolean swaggerPath?: string + basePath?: string + schemes?: SwaggerScheme[] } } diff --git a/src/swagger.d.ts b/src/swagger.d.ts index eb79ad4..5cf2660 100644 --- a/src/swagger.d.ts +++ b/src/swagger.d.ts @@ -4,7 +4,7 @@ export interface Swagger { host?: string basePath?: string tags?: Tag[] - schemes: string[] + schemes?: string[] paths: Paths securityDefinitions?: Record definitions?: Record diff --git a/tests/ServerlessAutoSwagger.test.ts b/tests/ServerlessAutoSwagger.test.ts index 92ff700..193724d 100644 --- a/tests/ServerlessAutoSwagger.test.ts +++ b/tests/ServerlessAutoSwagger.test.ts @@ -59,6 +59,31 @@ const generateServerlessFromAnEndpoint = ( } describe("ServerlessAutoSwagger", () => { + const mockedJsonFiles = new Map() + + jest + .spyOn(fs, "readFileSync") + .mockImplementation((fileName: PathOrFileDescriptor): string => { + const content = mockedJsonFiles.get(fileName as string) + + if (!content) { + throw new Error(`file ${fileName} not mocked`) + } + + return content + }) + + const mockJsonFile = ( + fileName: string, + content: Record + ): void => { + mockedJsonFiles.set(fileName, JSON.stringify(content)) + } + + beforeEach(() => { + mockedJsonFiles.clear() + }) + describe("generatePaths", () => { it("should generate minimal endpoint", () => { const serverlessAutoSwagger = new ServerlessAutoSwagger( @@ -413,7 +438,7 @@ describe("ServerlessAutoSwagger", () => { expect(serverlessAutoSwagger.swagger.paths).toEqual({}) }) - it("should add path without remove existings", () => { + it("should add path without remove existing", () => { const serverlessAutoSwagger = new ServerlessAutoSwagger( generateServerlessFromAnEndpoint([ { @@ -469,32 +494,75 @@ describe("ServerlessAutoSwagger", () => { }) }) - describe("gatherSwaggerFiles", () => { - const mockedJsonFiles = new Map() - - jest - .spyOn(fs, "readFileSync") - .mockImplementation((fileName: PathOrFileDescriptor): string => { - const content = mockedJsonFiles.get(fileName as string) + describe('gatherSwaggerOverrides', () => { + it('should use defaults if overrides are not specified', () => { + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint( + [ + { + http: { + path: "hello", + method: "post", + exclude: true, + }, + }, + ], + ), + {} + ) - if (!content) { - throw new Error(`file ${fileName} not mocked`) - } + serverlessAutoSwagger.gatherSwaggerOverrides(); - return content + expect(serverlessAutoSwagger.swagger).toEqual({ + definitions: expect.any(Object), + info: expect.any(Object), + paths: expect.any(Object), + securityDefinitions: expect.any(Object), + swagger: '2.0' }) + }); - const mockJsonFile = ( - fileName: string, - content: Record - ): void => { - mockedJsonFiles.set(fileName, JSON.stringify(content)) - } + it('should use overrides if specified', () => { + const fileName = "test.json"; + const swaggerDoc = { foo: { bar: true } }; + mockJsonFile(fileName, swaggerDoc) - beforeEach(() => { - mockedJsonFiles.clear() - }) + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint( + [ + { + http: { + path: "hello", + method: "post", + exclude: true, + }, + }, + ], + { + basePath: '/bp', + schemes: ['ws'], + swaggerFiles: [fileName] + } + ), + {} + ) + serverlessAutoSwagger.gatherSwaggerOverrides(); + + expect(serverlessAutoSwagger.swagger).toEqual({ + definitions: expect.any(Object), + info: expect.any(Object), + paths: expect.any(Object), + securityDefinitions: expect.any(Object), + swagger: '2.0', + schemes: ['ws'], + basePath: '/bp', + ...swaggerDoc + }) + }); + }); + + describe("gatherSwaggerFiles", () => { it("should add additionalProperties", async () => { mockJsonFile("test.json", { foo: { @@ -502,6 +570,7 @@ describe("ServerlessAutoSwagger", () => { }, }) + const swaggerFiles = ["test.json"] const serverlessAutoSwagger = new ServerlessAutoSwagger( generateServerlessFromAnEndpoint( [ @@ -513,19 +582,16 @@ describe("ServerlessAutoSwagger", () => { }, }, ], - { - swaggerFiles: ["test.json"], - } + { swaggerFiles } ), {} ) - await serverlessAutoSwagger.gatherSwaggerFiles() + await serverlessAutoSwagger.gatherSwaggerFiles(swaggerFiles) expect(serverlessAutoSwagger.swagger).toEqual({ swagger: "2.0", info: { title: "", version: "1" }, - schemes: ["https"], paths: {}, definitions: {}, securityDefinitions: {}, @@ -537,6 +603,8 @@ describe("ServerlessAutoSwagger", () => { mockJsonFile("test.json", { schemes: ["http"], }) + + const swaggerFiles = ["test.json"]; const serverlessAutoSwagger = new ServerlessAutoSwagger( generateServerlessFromAnEndpoint( [ @@ -548,14 +616,12 @@ describe("ServerlessAutoSwagger", () => { }, }, ], - { - swaggerFiles: ["test.json"], - } + { swaggerFiles } ), {} ) - await serverlessAutoSwagger.gatherSwaggerFiles() + await serverlessAutoSwagger.gatherSwaggerFiles(swaggerFiles) expect(serverlessAutoSwagger.swagger).toEqual({ swagger: "2.0", @@ -579,6 +645,7 @@ describe("ServerlessAutoSwagger", () => { }, }, }) + mockJsonFile("helloworld.json", { paths: { "/hello": "world", @@ -592,6 +659,8 @@ describe("ServerlessAutoSwagger", () => { }, }, }) + + const swaggerFiles = ["helloworld.json", "foobar.json"]; const serverlessAutoSwagger = new ServerlessAutoSwagger( generateServerlessFromAnEndpoint( [ @@ -603,19 +672,16 @@ describe("ServerlessAutoSwagger", () => { }, }, ], - { - swaggerFiles: ["helloworld.json", "foobar.json"], - } + { swaggerFiles } ), {} ) - await serverlessAutoSwagger.gatherSwaggerFiles() + await serverlessAutoSwagger.gatherSwaggerFiles(swaggerFiles) expect(serverlessAutoSwagger.swagger).toEqual({ swagger: "2.0", info: { title: "", version: "1" }, - schemes: ["https"], paths: { "/foo": "whatever", "/bar": "something else", From 8967ad8ef7ab8395a7bc20bad4ab6b8588768d05 Mon Sep 17 00:00:00 2001 From: Brandon Faulkner <12897328+bfaulk96@users.noreply.github.com> Date: Tue, 8 Feb 2022 16:53:42 -0500 Subject: [PATCH 2/2] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 022c007..b1700b0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ custom: apiKeyName?: 'string' useStage?: true | false basePath?: '/string' - schemes?: ['http', 'https'] + schemes?: ['http', 'https', 'ws', 'wss'] ``` `generateSwaggerOnDeploy` is a boolean which decides whether to generate a new swagger file on deployment. Default is `true`.