From 138626fa2c6c320d30fd1064790ff981417b4ede Mon Sep 17 00:00:00 2001 From: Sam Williams Date: Wed, 19 Jan 2022 17:54:06 +0100 Subject: [PATCH] fix(types): fixing responses on http, and conflicting types in tests --- README.md | 5 +- src/ServerlessAutoSwagger.ts | 96 +++-- src/resources/functions.ts | 6 +- src/serverlessPlugin.d.ts | 20 +- tests/ServerlessAutoSwagger.test.ts | 546 +++++++++++++++------------- 5 files changed, 366 insertions(+), 307 deletions(-) diff --git a/README.md b/README.md index 6e53ae1..6001b7e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ You can then assign these typescript definitions to requests as `bodyType` on th You can also add expected responses to each of the http endpoint events. This is an object that contains the response code with some example details: ```js -responses: { +responseData: { // response with description and response body 200: { description: 'this went well', @@ -113,7 +113,9 @@ http: { ![Query String Parameters](./doc_images/queryStringParams.png) ### Exclude an endpoint + You can exclude some endpoints from the swagger generation by adding `exclude` to the http event: + ``` http: { path: 'hello', @@ -121,6 +123,7 @@ http: { exclude: true, } ``` + ## with Serverless Offline In the plugin list, you must list serverless-auto-swagger before the serverless-offline plugin. If you don't you won't get the required endpoints added to your local endpoints. diff --git a/src/ServerlessAutoSwagger.ts b/src/ServerlessAutoSwagger.ts index baa77a3..878eb31 100644 --- a/src/ServerlessAutoSwagger.ts +++ b/src/ServerlessAutoSwagger.ts @@ -54,19 +54,18 @@ class ServerlessAutoSwagger { }; } - registerOptions = () => { - this.serverless.configSchemaHandler.defineFunctionEventProperties('aws', 'http', { + this.serverless.configSchemaHandler?.defineFunctionEventProperties('aws', 'http', { properties: { exclude: { type: 'boolean', nullable: true, - defaultValue: false + defaultValue: false, }, swaggerTags: { type: 'array', nullable: true, - items: {type: 'string'} + items: { type: 'string' }, }, responses: { type: 'object', @@ -74,22 +73,22 @@ class ServerlessAutoSwagger { additionalProperties: { anyOf: [ { - type: 'string' + type: 'string', }, { type: 'object', required: [], properties: { description: { - type: 'string' + type: 'string', }, bodyType: { - type: 'string' - } - } - } - ] - } + type: 'string', + }, + }, + }, + ], + }, }, queryStringParameters: { type: 'object', @@ -104,19 +103,19 @@ class ServerlessAutoSwagger { }, type: { type: 'string', - enum: ['string', 'integer'] + enum: ['string', 'integer'], }, description: { type: 'string', - nullable: true + nullable: true, }, minimum: { type: 'number', - nullable: true - } - } - } - } + nullable: true, + }, + }, + }, + }, }, required: [], }); @@ -133,35 +132,34 @@ class ServerlessAutoSwagger { }; gatherSwaggerFiles = async () => { - const swaggerFiles = this.serverless.service.custom?.autoswagger - ?.swaggerFiles as string[]; + const swaggerFiles = this.serverless.service.custom?.autoswagger?.swaggerFiles as string[]; if (!swaggerFiles || swaggerFiles.length < 1) { return; } - await Promise.all(swaggerFiles.map(async (filepath) => { - const fileData = fs.readFileSync(filepath, 'utf8'); - - 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, - } - }; - })); + await Promise.all( + swaggerFiles.map(async filepath => { + const fileData = fs.readFileSync(filepath, 'utf8'); + + 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 () => { @@ -222,7 +220,7 @@ class ServerlessAutoSwagger { await this.gatherTypes(); this.generatePaths(); - + this.serverless.cli.log(`Creating your Swagger File now`); // TODO enable user to specify swagger file path. also needs to update the swagger json endpoint. @@ -230,7 +228,7 @@ class ServerlessAutoSwagger { await fs.copy('./node_modules/serverless-auto-swagger/dist/resources', './swagger'); if (this.serverless.service.provider.runtime.includes('python')) { const swaggerPythonString = `# this file was generated by serverless-auto-swagger -docs = ${JSON.stringify(this.swagger, null, 2)}` +docs = ${JSON.stringify(this.swagger, null, 2)}`; await writeFile('./swagger/swagger.py', swaggerPythonString); } else { const swaggerJavaScriptString = `// this file was generated by serverless-auto-swagger @@ -286,15 +284,15 @@ docs = ${JSON.stringify(this.swagger, null, 2)}` consumes: ['application/json'], produces: ['application/json'], parameters: this.httpEventToParameters(http), - responses: this.formatResponses(http.responses), + responses: this.formatResponses(http.responseData || http.responses), security: this.httpEventToSecurity(http), }; }); }); }; - formatResponses = (responses: HttpResponses | undefined) => { - if (!responses) { + formatResponses = (responseData: HttpResponses | undefined) => { + if (!responseData) { // could throw error return { 200: { @@ -303,7 +301,7 @@ docs = ${JSON.stringify(this.swagger, null, 2)}` }; } const formatted: { [key: string]: Response } = {}; - Object.entries(responses).map(([statusCode, responseDetails]) => { + Object.entries(responseData).map(([statusCode, responseDetails]) => { if (typeof responseDetails == 'string') { formatted[statusCode] = { description: responseDetails, diff --git a/src/resources/functions.ts b/src/resources/functions.ts index eaed739..47ec0e0 100644 --- a/src/resources/functions.ts +++ b/src/resources/functions.ts @@ -2,9 +2,11 @@ import { ServerlessFunction, Serverless } from '../serverlessPlugin'; export default (serverless: Serverless) => { const handlerPath = 'swagger/'; + const configInput = serverless?.configurationInput || serverless.service; const path = serverless.service.custom?.autoswagger?.swaggerPath ?? 'swagger'; - const name = serverless.configurationInput?.service?.name; - const stage = serverless.configurationInput?.provider?.stage; + const name = + typeof configInput?.service == 'object' ? configInput.service.name : configInput.service; + const stage = configInput?.provider?.stage; return { swaggerUI: { name: name && stage ? `${name}-${stage}-swaggerUI` : undefined, diff --git a/src/serverlessPlugin.d.ts b/src/serverlessPlugin.d.ts index c7b0408..8f40641 100644 --- a/src/serverlessPlugin.d.ts +++ b/src/serverlessPlugin.d.ts @@ -4,15 +4,19 @@ export interface Serverless { }; service: ServerlessConfig; configurationInput: { - service?: { name?: string }, + service?: string | { name?: string }; provider?: { - stage?: string - } - }, + stage?: string; + }; + }; configSchemaHandler: { defineCustomProperties(schema: unknown): void; defineFunctionEvent(provider: string, event: string, schema: Record): void; - defineFunctionEventProperties(provider: string, existingEvent: string, schema: unknown): void; + defineFunctionEventProperties( + provider: string, + existingEvent: string, + schema: unknown + ): void; defineFunctionProperties(provider: string, schema: unknown): void; defineProvider(provider: string, options?: Record): void; defineTopLevelProperty(provider: string, schema: Record): void; @@ -54,7 +58,8 @@ export interface FullHttpEvent { cors?: boolean | CorsConfig; swaggerTags?: string[]; description?: string; - responses?: HttpResponses; + responseData?: HttpResponses; + responses?: HttpResponses; // Ideally don't use as it conflicts with serverless offline exclude?: boolean; bodyType?: string; queryStringParameters?: Record< @@ -80,7 +85,8 @@ export interface FullHttpApiEvent { method: string; swaggerTags?: string[]; description?: string; - responses?: HttpResponses; + responseData?: HttpResponses; + responses?: HttpResponses; // Ideally don't use as it conflicts with serverless offline exclude?: boolean; bodyType?: string; queryStringParameterType?: string; diff --git a/tests/ServerlessAutoSwagger.test.ts b/tests/ServerlessAutoSwagger.test.ts index dc7b9cf..d1a5a31 100644 --- a/tests/ServerlessAutoSwagger.test.ts +++ b/tests/ServerlessAutoSwagger.test.ts @@ -1,348 +1,391 @@ import ServerlessAutoSwagger from '../src/ServerlessAutoSwagger'; -import { - FullHttpEvent, - Serverless, -} from '../src/serverlessPlugin'; +import { FullHttpEvent, Serverless } from '../src/serverlessPlugin'; import * as fs from 'fs-extra'; +import { PathOrFileDescriptor } from 'fs-extra'; + +const generateServerlessFromAnEndpoint = ( + events: FullHttpEvent[], + autoswaggerOptions = {} +): Serverless => { + const serviceDetails = { + service: '', + provider: { + name: '', + runtime: '', + stage: '', + region: '', + profile: '', + environment: {}, + }, + plugins: [], + functions: { + mocked: { + handler: 'mocked.handler', + events, + }, + }, + custom: { + autoswagger: autoswaggerOptions, + }, + }; - -const generateServerlessFromAnEndpoint = (events: FullHttpEvent[], autoswaggerOptions = {}): Serverless => ( - { + return { cli: { - log: () => {} + log: () => {}, }, - service: { - service: '', - provider: { - name: '', - runtime: '', - stage: '', - region: '', - profile: '', - environment: { } - }, - plugins: [], - functions: { - mocked: { - handler: 'mocked.handler', - events - } - }, - custom: { - autoswagger: autoswaggerOptions - }, - } - } -) + service: serviceDetails, + configurationInput: serviceDetails, + configSchemaHandler: { + defineCustomProperties: (schema: unknown) => {}, + defineFunctionEvent: ( + provider: string, + event: string, + schema: Record + ) => {}, + defineFunctionEventProperties: ( + provider: string, + existingEvent: string, + schema: unknown + ) => {}, + defineFunctionProperties: (provider: string, schema: unknown) => {}, + defineProvider: (provider: string, options?: Record) => {}, + defineTopLevelProperty: (provider: string, schema: Record) => {}, + }, + }; +}; describe('ServerlessAutoSwagger', () => { describe('generatePaths', () => { it('should generate minimal endpoint', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - } - }]), {}); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'hello', + method: 'post', + }, + }, + ]), + {} + ); serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({ - "/hello": { + '/hello': { post: { - summary: "mocked", - description: "", - operationId: "mocked", - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], + summary: 'mocked', + description: '', + operationId: 'mocked', + consumes: ['application/json'], + produces: ['application/json'], parameters: [], responses: { 200: { - description: "200 response" - } + description: '200 response', + }, }, - } - } + }, + }, }); }); it('should generate an endpoint with a description', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - description: 'I like documentation' - } - }]), {}); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'hello', + method: 'post', + description: 'I like documentation', + }, + }, + ]), + {} + ); serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({ - "/hello": { + '/hello': { post: { - summary: "mocked", - description: "I like documentation", - operationId: "mocked", - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], + summary: 'mocked', + description: 'I like documentation', + operationId: 'mocked', + consumes: ['application/json'], + produces: ['application/json'], parameters: [], responses: { 200: { - description: "200 response" - } + description: '200 response', + }, }, - } - } + }, + }, }); }); it('should generate an endpoint with a response', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - responses: { - // response with description and response body - 200: { - description: 'this went well', - bodyType: 'helloPostResponse', - }, - // response with just a description - 400: { - description: 'failed Post', + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'hello', + method: 'post', + responses: { + // response with description and response body + 200: { + description: 'this went well', + bodyType: 'helloPostResponse', + }, + // response with just a description + 400: { + description: 'failed Post', + }, + // shorthand for just a description + 502: 'server error', + }, }, - // shorthand for just a description - 502: 'server error', - } - } - }]), {}); + }, + ]), + {} + ); serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({ - "/hello": { + '/hello': { post: { - summary: "mocked", - description: "", - operationId: "mocked", - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], + summary: 'mocked', + description: '', + operationId: 'mocked', + consumes: ['application/json'], + produces: ['application/json'], parameters: [], responses: { 200: { - description: "this went well", + description: 'this went well', schema: { - "$ref": "#/definitions/helloPostResponse" - } + $ref: '#/definitions/helloPostResponse', + }, }, 400: { - description: "failed Post" + description: 'failed Post', }, 502: { - description: "server error" - } - } - } - } + description: 'server error', + }, + }, + }, + }, }); }); it('should generate an endpoint with parameters', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'goodbye', - method: 'get', - queryStringParameters: { - bob: { - required: true, - type: 'string', - description: 'bob', - }, - count: { - required: false, - type: 'integer', + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'goodbye', + method: 'get', + queryStringParameters: { + bob: { + required: true, + type: 'string', + description: 'bob', + }, + count: { + required: false, + type: 'integer', + }, + }, }, }, - } - }]), {}); + ]), + {} + ); serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({ - "/goodbye": { + '/goodbye': { get: { - summary: "mocked", - description: "", - operationId: "mocked", - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], + summary: 'mocked', + description: '', + operationId: 'mocked', + consumes: ['application/json'], + produces: ['application/json'], parameters: [ { name: 'bob', type: 'string', description: 'bob', - in: "query" + in: 'query', }, { name: 'count', type: 'integer', - in: "query" - } + in: 'query', + }, ], responses: { 200: { - description: "200 response" - } + description: '200 response', + }, }, - } - } + }, + }, }); }); it('should filter an endpoint with exclude', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - exclude: true - } - }]), {}); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'hello', + method: 'post', + exclude: true, + }, + }, + ]), + {} + ); serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({}); }); it('should add path without remove existings', () => { - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - } - }]), {}); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint([ + { + http: { + path: 'hello', + method: 'post', + }, + }, + ]), + {} + ); serverlessAutoSwagger.swagger.paths = { - "/should": { - "still": { + '/should': { + still: { operationId: 'be here', consumes: [], produces: [], parameters: [], - responses: {} - } - } - } - + responses: {}, + }, + }, + }; serverlessAutoSwagger.generatePaths(); expect(serverlessAutoSwagger.swagger.paths).toEqual({ - "/should": { + '/should': { still: { - operationId: "be here", + operationId: 'be here', consumes: [], produces: [], parameters: [], - responses: {} - } + responses: {}, + }, }, - "/hello": { + '/hello': { post: { - summary: "mocked", - description: "", - operationId: "mocked", - consumes: [ - "application/json" - ], - produces: [ - "application/json" - ], + summary: 'mocked', + description: '', + operationId: 'mocked', + consumes: ['application/json'], + produces: ['application/json'], parameters: [], responses: { 200: { - description: "200 response" - } + description: '200 response', + }, }, - } - } + }, + }, }); - }) + }); }); - describe('gatherSwaggerFiles', () => { const mockedJsonFiles = new Map(); - const spy = jest.spyOn(fs, 'readFileSync').mockImplementation((fileName: string): string => { - const content = mockedJsonFiles.get(fileName); + const spy = jest + .spyOn(fs, 'readFileSync') + .mockImplementation((fileName: PathOrFileDescriptor): string => { + const content = mockedJsonFiles.get(fileName as string); - if (!content) { - throw new Error(`file ${fileName} not mocked`) - } + if (!content) { + throw new Error(`file ${fileName} not mocked`); + } - return content - }); + return content; + }); const mockJsonFile = (fileName: string, content: Record): void => { mockedJsonFiles.set(fileName, JSON.stringify(content)); - } + }; beforeEach(() => { mockedJsonFiles.clear(); - }) + }); it('should add additionalProperties', async () => { mockJsonFile('test.json', { foo: { - bar: true - } + bar: true, + }, }); - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - exclude: true - } - }], { - swaggerFiles: ['test.json'] - }), {}); - + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint( + [ + { + http: { + path: 'hello', + method: 'post', + exclude: true, + }, + }, + ], + { + swaggerFiles: ['test.json'], + } + ), + {} + ); await serverlessAutoSwagger.gatherSwaggerFiles(); expect(serverlessAutoSwagger.swagger).toEqual({ swagger: '2.0', info: { title: '', version: '1' }, - schemes: [ 'https' ], + schemes: ['https'], paths: {}, definitions: {}, - foo: { bar: true } + foo: { bar: true }, }); - }) + }); it('should extend existing property', async () => { mockJsonFile('test.json', { - schemes: ['http'] - }) - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - exclude: true - } - }], { - swaggerFiles: ['test.json'] - }), {}); - + schemes: ['http'], + }); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint( + [ + { + http: { + path: 'hello', + method: 'post', + exclude: true, + }, + }, + ], + { + swaggerFiles: ['test.json'], + } + ), + {} + ); await serverlessAutoSwagger.gatherSwaggerFiles(); @@ -353,43 +396,50 @@ describe('ServerlessAutoSwagger', () => { paths: {}, definitions: {}, }); - }) + }); it('should cumulate files', async () => { mockJsonFile('foobar.json', { paths: { - "/foo": "whatever", - "/bar": "something else" + '/foo': 'whatever', + '/bar': 'something else', }, definitions: { - "World": { - "type": "number" + World: { + type: 'number', }, - } - }) + }, + }); mockJsonFile('helloworld.json', { paths: { - "/hello": "world", + '/hello': 'world', }, definitions: { - "Foo": { - "type": "string" + Foo: { + type: 'string', }, - "Bar": { - "type": "string" + Bar: { + type: 'string', + }, + }, + }); + const serverlessAutoSwagger = new ServerlessAutoSwagger( + generateServerlessFromAnEndpoint( + [ + { + http: { + path: 'hello', + method: 'post', + exclude: true, + }, + }, + ], + { + swaggerFiles: ['helloworld.json', 'foobar.json'], } - } - }) - const serverlessAutoSwagger = new ServerlessAutoSwagger(generateServerlessFromAnEndpoint([{ - http: { - path: 'hello', - method: 'post', - exclude: true - } - }], { - swaggerFiles: ['helloworld.json', 'foobar.json'] - }), {}); - + ), + {} + ); await serverlessAutoSwagger.gatherSwaggerFiles(); @@ -398,22 +448,22 @@ describe('ServerlessAutoSwagger', () => { info: { title: '', version: '1' }, schemes: ['https'], paths: { - "/foo": "whatever", - "/bar": "something else", - "/hello": "world", + '/foo': 'whatever', + '/bar': 'something else', + '/hello': 'world', }, definitions: { Foo: { - type: "string" + type: 'string', }, Bar: { - type: "string" + type: 'string', }, World: { - type: "number" + type: 'number', }, }, }); }); - }) + }); });