serverless-openapi-documenter 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -350,6 +350,24 @@ cookieParams:
350
350
  type: "string"
351
351
  ```
352
352
 
353
+ #### `headerParams` - Request Headers
354
+
355
+ Request Headers can be described as follow:
356
+
357
+ * `name`: the name of the query variable
358
+ * `description`: a description of the query variable
359
+ * `required`: whether the query parameter is mandatory (boolean)
360
+ * `schema`: JSON schema (inline, file or externally hosted)
361
+
362
+ ```yml
363
+ headerParams:
364
+ - name: "Content-Type"
365
+ description: "The content type"
366
+ required: true
367
+ schema:
368
+ type: "string"
369
+ ```
370
+
353
371
  #### `requestModels`
354
372
 
355
373
  The `requestModels` property allows you to define models for the HTTP Request of the function event. You can define a different model for each different `Content-Type`. You can define a reference to the relevant request model named in the `models` section of your configuration (see [Defining Models](#models) section).
@@ -369,14 +387,24 @@ For an example of a `methodResponses` configuration for an event see below:
369
387
  ```yml
370
388
  methodResponse:
371
389
  - statusCode: 200
372
- responseHeaders:
373
- - name: "Content-Type"
374
- description: "Content Type header"
375
- schema:
376
- type: "string"
390
+ responseBody:
391
+ description: Success
377
392
  responseModels:
378
393
  application/json: "CreateResponse"
379
394
  application/xml: "CreateResponseXML"
395
+ responseHeaders:
396
+ X-Rate-Limit-Limit:
397
+ description: The number of allowed requests in the current period
398
+ schema:
399
+ type: integer
400
+ X-Rate-Limit-Remaining:
401
+ description: The number of remaining requests in the current period
402
+ schema:
403
+ type: integer
404
+ X-Rate-Limit-Reset:
405
+ description: The number of seconds left in the current period
406
+ schema:
407
+ type: integer
380
408
  ```
381
409
 
382
410
  ##### `responseModels`
@@ -389,27 +417,16 @@ responseModels:
389
417
  application/xml: "CreateResponseXML"
390
418
  ```
391
419
 
392
- ##### `responseHeaders` and `requestHeaders`
393
-
394
- The `responseHeaders/requestHeaders` section of the configuration allows you to define the HTTP headers for the function event.
420
+ ##### `responseHeaders`
395
421
 
396
- The attributes for a header are as follow:
397
-
398
- * `name`: the name of the HTTP Header
399
- * `description`: a description of the HTTP Header
400
- * `schema`: JSON schema (inline, file or externally hosted)
422
+ The `responseHeaders` property allows you to define the headers expected in a HTTP Response of the function event. This should only contain a description and a schema, which must be a JSON schema (inline, file or externally hosted).
401
423
 
402
424
  ```yml
403
425
  responseHeaders:
404
- - name: "Content-Type"
405
- description: "Content Type header"
426
+ X-Rate-Limit-Limit:
427
+ description: The number of allowed requests in the current period
406
428
  schema:
407
- type: "string"
408
- requestHeaders:
409
- - name: "Content-Type"
410
- description: "Content Type header"
411
- schema:
412
- type: "string"
429
+ type: integer
413
430
  ```
414
431
 
415
432
  ## Example configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-openapi-documenter",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -79,6 +79,8 @@ class DefinitionGenerator {
79
79
  if (event?.http?.documentation || event?.httpApi?.documentation) {
80
80
  const documentation = event?.http?.documentation || event?.httpApi?.documentation
81
81
 
82
+ this.currentFunctionName = httpFunction.functionInfo.name
83
+
82
84
  let opId
83
85
  if (this.operationIds.includes(httpFunction.functionInfo.name) === false) {
84
86
  opId = httpFunction.functionInfo.name
@@ -262,17 +264,48 @@ class DefinitionGenerator {
262
264
  description: response.responseBody.description || '',
263
265
  }
264
266
 
267
+ this.currentStatusCode = response.statusCode
268
+
265
269
  obj.content = await this.createMediaTypeObject(response.responseModels, 'responses')
266
270
  .catch(err => {
267
271
  throw err
268
272
  })
269
273
 
274
+ if (response.responseHeaders) {
275
+ obj.headers = await this.createResponseHeaders(response.responseHeaders)
276
+ .catch(err => {
277
+ throw err
278
+ })
279
+ }
280
+
270
281
  Object.assign(responses,{[response.statusCode]: obj})
271
282
  }
272
283
 
273
284
  return responses
274
285
  }
275
286
 
287
+ async createResponseHeaders(headers) {
288
+ const obj = {}
289
+ for (const header of Object.keys(headers)) {
290
+ const newHeader = {}
291
+ newHeader.description = headers[header].description || ''
292
+
293
+ if (headers[header].schema) {
294
+ const schemaRef = await this.schemaCreator(headers[header].schema, header)
295
+ .catch(err => {
296
+ throw err
297
+ })
298
+ newHeader.schema = {
299
+ $ref: schemaRef
300
+ }
301
+ }
302
+
303
+ Object.assign(obj, {[header]: newHeader})
304
+ }
305
+
306
+ return obj
307
+ }
308
+
276
309
  async createRequestBody(documentation) {
277
310
  const obj = {
278
311
  description: documentation.requestBody.description,
@@ -290,6 +323,10 @@ class DefinitionGenerator {
290
323
  async createMediaTypeObject(models, type) {
291
324
  const mediaTypeObj = {}
292
325
  for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
326
+ if (models === undefined || models === null) {
327
+ throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
328
+ }
329
+
293
330
  if (Object.values(models).includes(mediaTypeDocumentation.name)) {
294
331
  let contentKey = ''
295
332
  for (const [key, value] of Object.entries(models)) {
@@ -107,35 +107,24 @@ class OpenAPIGenerator {
107
107
  async generate() {
108
108
  this.log(this.defaultLog, chalk.bold.underline('OpenAPI v3 Document Generation'))
109
109
  this.processCliInput()
110
- const generator = new DefinitionGenerator(this.serverless);
111
110
 
112
- await generator.parse()
111
+ const validOpenAPI = await this.generationAndValidation()
113
112
  .catch(err => {
114
- this.log('error', `ERROR: An error was thrown generating the OpenAPI v3 documentation`)
115
113
  throw new this.serverless.classes.Error(err)
116
114
  })
117
115
 
118
- const valid = await generator.validate()
119
- .catch(err => {
120
- this.log('error', `ERROR: An error was thrown validating the OpenAPI v3 documentation`)
121
- throw new this.serverless.classes.Error(err)
122
- })
123
-
124
- if (valid)
125
- this.log('success', 'OpenAPI v3 Documentation Successfully Generated')
126
-
127
116
  if (this.config.postmanCollection) {
128
- this.createPostman(generator.openAPI)
117
+ this.createPostman(validOpenAPI)
129
118
  }
130
119
 
131
120
  let output
132
121
  switch (this.config.format.toLowerCase()) {
133
122
  case 'json':
134
- output = JSON.stringify(generator.openAPI, null, this.config.indent);
123
+ output = JSON.stringify(validOpenAPI, null, this.config.indent);
135
124
  break;
136
125
  case 'yaml':
137
126
  default:
138
- output = yaml.dump(generator.openAPI, { indent: this.config.indent });
127
+ output = yaml.dump(validOpenAPI, { indent: this.config.indent });
139
128
  break;
140
129
  }
141
130
  try {
@@ -147,6 +136,28 @@ class OpenAPIGenerator {
147
136
  }
148
137
  }
149
138
 
139
+ async generationAndValidation() {
140
+ const generator = new DefinitionGenerator(this.serverless);
141
+
142
+ await generator.parse()
143
+ .catch(err => {
144
+ this.log('error', `ERROR: An error was thrown generating the OpenAPI v3 documentation`)
145
+ throw new this.serverless.classes.Error(err)
146
+ })
147
+
148
+ await generator.validate()
149
+ .catch(err => {
150
+ this.log('error', `ERROR: An error was thrown validating the OpenAPI v3 documentation`)
151
+ this.validationErrorDetails(err)
152
+ throw new this.serverless.classes.Error(err)
153
+ })
154
+
155
+
156
+ this.log('success', 'OpenAPI v3 Documentation Successfully Generated')
157
+
158
+ return generator.openAPI
159
+ }
160
+
150
161
  createPostman(openAPI) {
151
162
  const postmanGeneration = (err, result) => {
152
163
  if (err) {
@@ -205,25 +216,10 @@ class OpenAPIGenerator {
205
216
  this.config = config
206
217
  }
207
218
 
208
- validateDetails(validation) {
209
- if (validation.valid) {
210
- this.log(this.defaultLog, `${ chalk.bold.green('[VALIDATION]') } OpenAPI valid: ${chalk.bold.green('true')}\n\n`);
211
- } else {
212
- this.log(this.defaultLog, `${chalk.bold.red('[VALIDATION]')} Failed to validate OpenAPI document: \n\n`);
213
- this.log(this.defaultLog, `${chalk.bold.green('Context:')} ${JSON.stringify(validation.context, null, 2)}\n`);
214
- this.log(this.defaultLog, `${chalk.bold.green('Error Message:')} ${JSON.stringify(validation.error, null, 2)}\n`);
215
- if (typeof validation.error === 'string') {
216
- this.log(this.defaultLog, `${validation.error}\n\n`);
217
- } else {
218
- for (const info of validation.error) {
219
- this.log(this.defaultLog, chalk.grey('\n\n--------\n\n'));
220
- this.log(this.defaultLog, ' ', chalk.blue(info.dataPath), '\n');
221
- this.log(this.defaultLog, ' ', info.schemaPath, chalk.bold.yellow(info.message));
222
- this.log(this.defaultLog, chalk.grey('\n\n--------\n\n'));
223
- this.log(this.defaultLog, `${inspect(info, { colors: true, depth: 2 })}\n\n`);
224
- }
225
- }
226
- }
219
+ validationErrorDetails(validationError) {
220
+ this.log('error', `${chalk.bold.yellow('[VALIDATION]')} Failed to validate OpenAPI document: \n`);
221
+ this.log('error', `${chalk.bold.yellow('Context:')} ${JSON.stringify(validationError.options.context[validationError.options.context.length-1], null, 2)}\n`);
222
+ this.log('error', `${chalk.bold.yellow('Error Message:')} ${JSON.stringify(validationError.message, null, 2)}\n`);
227
223
  }
228
224
  }
229
225
 
@@ -0,0 +1,30 @@
1
+ {
2
+ "custom": {
3
+ "documentation": {
4
+ "title": "test-service",
5
+ "models": [
6
+ {
7
+ "name": "SuccessResponse",
8
+ "description": "Success response",
9
+ "content": {
10
+ "application/json": {
11
+ "schema": {
12
+ "$schema": "http://json-schema.org/draft-04/schema#",
13
+ "properties": {
14
+ "SomeObject": {
15
+ "type": "object",
16
+ "properties": {
17
+ "SomeAttribute": {
18
+ "type": "string"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ]
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "createUser": {
3
+ "name": "createUser",
4
+ "handler": "handler.create",
5
+ "events": [
6
+ {
7
+ "http": {
8
+ "path": "find/{name}",
9
+ "method": "get",
10
+ "documentation": {
11
+ "pathParams": [
12
+ {
13
+ "name": "name",
14
+ "schema": {
15
+ "type": "string"
16
+ }
17
+ }
18
+ ],
19
+ "methodResponses": [
20
+ {
21
+ "statusCode": 200,
22
+ "responseBody": {
23
+ "description": "A user object along with generated API Keys"
24
+ },
25
+ "responseModels": {
26
+ "application/json": "SuccessResponse"
27
+ }
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -7,12 +7,20 @@ const expect = require('chai').expect
7
7
 
8
8
  const validOpenAPI = require('../json/valid-openAPI.json')
9
9
 
10
+ const basicDocumentation = require('../models/BasicDocumentation.json')
11
+ const basicValidFunction = require('../models/BasicValidFunction.json')
12
+
10
13
  const OpenAPIGenerator = require('../../src/openAPIGenerator')
11
14
 
12
15
  describe('OpenAPIGenerator', () => {
13
16
  let sls, logOutput
14
17
  beforeEach(function() {
15
18
  sls = {
19
+ service: {
20
+ service: 'test-service',
21
+ getAllFunctions: () => {},
22
+ getFunction: () => {}
23
+ },
16
24
  version: '3.0.0',
17
25
  variables: {
18
26
  service: {
@@ -32,7 +40,7 @@ describe('OpenAPIGenerator', () => {
32
40
  options: {
33
41
  postmanCollection: 'postman.json'
34
42
  }
35
- }
43
+ },
36
44
  }
37
45
 
38
46
  logOutput = {
@@ -43,6 +51,119 @@ describe('OpenAPIGenerator', () => {
43
51
  }
44
52
  }
45
53
  });
54
+
55
+ describe('generationAndValidation', () => {
56
+ it('should correctly generate a valid openAPI document', async function() {
57
+ const succSpy = sinon.spy(logOutput.log, 'success')
58
+ const errSpy = sinon.spy(logOutput.log, 'error')
59
+
60
+ Object.assign(sls.service, basicDocumentation)
61
+ const getAllFuncsStub = sinon.stub(sls.service, 'getAllFunctions').returns(['createUser'])
62
+
63
+ const getFuncStub = sinon.stub(sls.service, 'getFunction').returns(basicValidFunction.createUser)
64
+
65
+ const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
66
+ openAPIGenerator.processCliInput()
67
+
68
+ const validOpenAPIDocument = await openAPIGenerator.generationAndValidation()
69
+ .catch(err => {
70
+ expect(err).to.be.undefined
71
+ })
72
+
73
+ expect(succSpy.called).to.be.true
74
+ expect(errSpy.called).to.be.false
75
+
76
+ succSpy.restore()
77
+ errSpy.restore()
78
+ getAllFuncsStub.reset()
79
+ getFuncStub.reset()
80
+ });
81
+
82
+ it('should throw an error when trying to generate an invalid openAPI document', async function() {
83
+ const succSpy = sinon.spy(logOutput.log, 'success')
84
+ const errSpy = sinon.spy(logOutput.log, 'error')
85
+
86
+ Object.assign(sls.service, basicDocumentation)
87
+ const getAllFuncsStub = sinon.stub(sls.service, 'getAllFunctions').returns(['createUser'])
88
+ const basicInvalidFunction = JSON.parse(JSON.stringify(basicValidFunction))
89
+
90
+ delete basicInvalidFunction.createUser.events[0].http.documentation.methodResponses[0].responseModels
91
+ const getFuncStub = sinon.stub(sls.service, 'getFunction').returns(basicInvalidFunction.createUser)
92
+
93
+ const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
94
+ openAPIGenerator.processCliInput()
95
+
96
+ const validOpenAPIDocument = await openAPIGenerator.generationAndValidation()
97
+ .catch(err => {
98
+ expect(err.message).to.be.equal('Error: createUser is missing a Response Model for statusCode 200')
99
+ })
100
+
101
+ expect(succSpy.called).to.be.false
102
+ expect(errSpy.called).to.be.true
103
+
104
+ succSpy.restore()
105
+ errSpy.restore()
106
+ getAllFuncsStub.reset()
107
+ getFuncStub.reset()
108
+ });
109
+
110
+ it('should correctly validate a valid openAPI document', async function() {
111
+ const succSpy = sinon.spy(logOutput.log, 'success')
112
+ const errSpy = sinon.spy(logOutput.log, 'error')
113
+
114
+ Object.assign(sls.service, basicDocumentation)
115
+ const getAllFuncsStub = sinon.stub(sls.service, 'getAllFunctions').returns(['createUser'])
116
+ const basicInvalidFunction = JSON.parse(JSON.stringify(basicValidFunction))
117
+
118
+ const getFuncStub = sinon.stub(sls.service, 'getFunction').returns(basicInvalidFunction.createUser)
119
+
120
+ const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
121
+ openAPIGenerator.processCliInput()
122
+
123
+ const validOpenAPIDocument = await openAPIGenerator.generationAndValidation()
124
+ .catch(err => {
125
+ expect(err).to.be.undefined
126
+ })
127
+
128
+ expect(succSpy.called).to.be.true
129
+ expect(errSpy.called).to.be.false
130
+ expect(validOpenAPIDocument).to.have.property('openapi')
131
+
132
+ succSpy.restore()
133
+ errSpy.restore()
134
+ getAllFuncsStub.reset()
135
+ getFuncStub.reset()
136
+ });
137
+
138
+ it('should throw an error when trying to validate an invalid openAPI document', async function() {
139
+ const succSpy = sinon.spy(logOutput.log, 'success')
140
+ const errSpy = sinon.spy(logOutput.log, 'error')
141
+
142
+ Object.assign(sls.service, basicDocumentation)
143
+ const getAllFuncsStub = sinon.stub(sls.service, 'getAllFunctions').returns(['createUser'])
144
+ const basicInvalidFunction = JSON.parse(JSON.stringify(basicValidFunction))
145
+
146
+ delete basicInvalidFunction.createUser.events[0].http.documentation.pathParams
147
+ const getFuncStub = sinon.stub(sls.service, 'getFunction').returns(basicInvalidFunction.createUser)
148
+
149
+ const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
150
+ openAPIGenerator.processCliInput()
151
+
152
+ const validOpenAPIDocument = await openAPIGenerator.generationAndValidation()
153
+ .catch(err => {
154
+ expect(err.message).to.be.equal('AssertionError: Templated parameter name not found')
155
+ })
156
+
157
+ expect(succSpy.called).to.be.false
158
+ expect(errSpy.called).to.be.true
159
+
160
+ succSpy.restore()
161
+ errSpy.restore()
162
+ getAllFuncsStub.reset()
163
+ getFuncStub.reset()
164
+ });
165
+ });
166
+
46
167
  describe('createPostman', () => {
47
168
  it('should generate a postman collection when a valid openAPI file is generated', function() {
48
169
  const fsStub = sinon.stub(fs, 'writeFileSync').returns(true)