serverless-openapi-documenter 0.0.7 → 0.0.11

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.
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "npm" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "daily"
package/README.md CHANGED
@@ -153,7 +153,7 @@ The *required* directives for the models section are as follow:
153
153
  * `name`: the name of the schema
154
154
  * `description`: a description of the schema
155
155
  * `contentType`: the content type of the described request/response (ie. `application/json` or `application/xml`).
156
- * `schema`: The JSON Schema ([website](http://json-schema.org/)) that describes the model. You can either use inline `YAML` to define these, or refer to an external schema file as below
156
+ * `schema`: The JSON Schema ([website](http://json-schema.org/)) that describes the model. You can either use inline `YAML` to define these or use either an external file schema that serverless will resolve (as below), or a reference to an externally hosted schema that will be attempted to be resolved.
157
157
 
158
158
  ```yml
159
159
  custom:
@@ -262,7 +262,7 @@ Query parameters can be described as follow:
262
262
  * `name`: the name of the query variable
263
263
  * `description`: a description of the query variable
264
264
  * `required`: whether the query parameter is mandatory (boolean)
265
- * `schema`: JSON schema (inline or file)
265
+ * `schema`: JSON schema (inline, file or externally hosted)
266
266
 
267
267
  ```yml
268
268
  queryParams:
@@ -279,7 +279,7 @@ Path parameters can be described as follow:
279
279
 
280
280
  * `name`: the name of the query variable
281
281
  * `description`: a description of the query variable
282
- * `schema`: JSON schema (inline or file)
282
+ * `schema`: JSON schema (inline, file or externally hosted)
283
283
 
284
284
  ```yml
285
285
  pathParams:
@@ -296,7 +296,7 @@ Cookie parameters can be described as follow:
296
296
  * `name`: the name of the query variable
297
297
  * `description`: a description of the query variable
298
298
  * `required`: whether the query parameter is mandatory (boolean)
299
- * `schema`: JSON schema (inline or file)
299
+ * `schema`: JSON schema (inline, file or externally hosted)
300
300
 
301
301
  ```yml
302
302
  cookieParams:
@@ -354,7 +354,7 @@ The attributes for a header are as follow:
354
354
 
355
355
  * `name`: the name of the HTTP Header
356
356
  * `description`: a description of the HTTP Header
357
- * `schema`: JSON schema (inline or file)
357
+ * `schema`: JSON schema (inline, file or externally hosted)
358
358
 
359
359
  ```yml
360
360
  responseHeaders:
@@ -373,6 +373,82 @@ requestHeaders:
373
373
 
374
374
  Please view the example [serverless.yml](test/serverless\ 2/serverless.yml).
375
375
 
376
+ ## Notes on schemas
377
+
378
+ Schemas can be either: inline, in file or externally hosted. If they're inline or in file, the plugin will attempt to normalise the schema to [OpenAPI 3.0.X specification](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schemaObject).
379
+
380
+ If they exist as an external reference, for instance:
381
+
382
+ ```yaml
383
+ schema: https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/bettercodehub.json
384
+ ```
385
+
386
+ We use the plugin [JSON Schema $Ref Parser](https://apitools.dev/json-schema-ref-parser/) to attempt to parse and resolve the references. There are limitations to this. Consider the schema:
387
+
388
+ ```json
389
+ {
390
+ "$schema": "https://json-schema.org/draft-04/schema",
391
+ "title": "Reusable Definitions",
392
+ "type": "object",
393
+ "id": "https://raw.githubusercontent.com/json-editor/json-editor/master/tests/fixtures/definitions.json",
394
+ "definitions": {
395
+ "address": {
396
+ "title": "Address",
397
+ "type": "object",
398
+ "properties": {
399
+ "street_address": { "type": "string" },
400
+ "city": { "type": "string" },
401
+ "state": { "type": "string" }
402
+ },
403
+ "required": ["street_address"]
404
+ },
405
+ "link" : {"$refs": "./properties.json#/properties/title"}
406
+ },
407
+ "properties": {
408
+ "address" : {"$refs": "#/definitions/address"}
409
+ }
410
+ }
411
+ ```
412
+ Where the definition "link" refers to a schema held in a directory that the resolver does not know about, we will not be able to fully resolve the schema which will likely cause errors in validation of the openAPI 3.0.X specification.
413
+
414
+ Because of the dependency we use to parse externally linked schemas, we can supply our own options to resolve schemas that are more difficult than a straight forward example.
415
+
416
+ You can create your own options file: https://apitools.dev/json-schema-ref-parser/docs/options.html to pass into the dependency that contains it's own resolver to allow you to resolve references that might be in hard to reach places. In your main project folder, you should have a folder called `options` with a file called `ref-parser.js` that looks like:
417
+
418
+ ```js
419
+ 'use strict'
420
+
421
+ // options from: https://apitools.dev/json-schema-ref-parser/docs/options.html
422
+
423
+ module.exports = {
424
+ continueOnError: true, // Don't throw on the first error
425
+ parse: {
426
+ json: false, // Disable the JSON parser
427
+ yaml: {
428
+ allowEmpty: false // Don't allow empty YAML files
429
+ },
430
+ text: {
431
+ canParse: [".txt", ".html"], // Parse .txt and .html files as plain text (strings)
432
+ encoding: 'utf16' // Use UTF-16 encoding
433
+ }
434
+ },
435
+ resolve: {
436
+ file: false, // Don't resolve local file references
437
+ http: {
438
+ timeout: 2000, // 2 second timeout
439
+ withCredentials: true, // Include auth credentials when resolving HTTP references
440
+ }
441
+ },
442
+ dereference: {
443
+ circular: false, // Don't allow circular $refs
444
+ excludedPathMatcher: (path) => // Skip dereferencing content under any 'example' key
445
+ path.includes("/example/")
446
+ }
447
+ }
448
+ ```
449
+
450
+ If you don't supply this file, it will use the default options.
451
+
376
452
  ## License
377
453
 
378
454
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-openapi-documenter",
3
- "version": "0.0.7",
3
+ "version": "0.0.11",
4
4
  "description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -28,9 +28,10 @@
28
28
  },
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
+ "@apidevtools/json-schema-ref-parser": "^9.0.9",
31
32
  "chalk": "^4.1.2",
32
33
  "js-yaml": "^4.1.0",
33
- "json-schema-for-openapi": "^0.1.0",
34
+ "json-schema-for-openapi": "^0.1.4",
34
35
  "oas-validator": "^5.0.8",
35
36
  "openapi-to-postmanv2": "^3.2.0",
36
37
  "swagger2openapi": "^7.0.8",
@@ -1,8 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const path = require('path')
4
+
3
5
  const { v4: uuid } = require('uuid')
4
6
  const validator = require('oas-validator');
5
7
  const SchemaConvertor = require('json-schema-for-openapi')
8
+ const $RefParser = require("@apidevtools/json-schema-ref-parser");
6
9
 
7
10
  class DefinitionGenerator {
8
11
  constructor(serverless, options = {}) {
@@ -24,11 +27,21 @@ class DefinitionGenerator {
24
27
  }
25
28
 
26
29
  this.operationIds = []
30
+
31
+ try {
32
+ this.refParserOptions = require(path.resolve('options', 'ref-parser.js'))
33
+ } catch (err) {
34
+ this.refParserOptions = {}
35
+ }
36
+
27
37
  }
28
38
 
29
- parse() {
39
+ async parse() {
30
40
  this.createInfo()
31
- this.createPaths()
41
+ await this.createPaths()
42
+ .catch(err => {
43
+ throw err
44
+ })
32
45
 
33
46
  if (this.serverless.service.custom.documentation.servers) {
34
47
  const servers = this.createServers(this.serverless.service.custom.documentation.servers)
@@ -57,7 +70,7 @@ class DefinitionGenerator {
57
70
  Object.assign(this.openAPI, {info})
58
71
  }
59
72
 
60
- createPaths() {
73
+ async createPaths() {
61
74
  const paths = {}
62
75
  const httpFunctions = this.getHTTPFunctions()
63
76
 
@@ -74,7 +87,11 @@ class DefinitionGenerator {
74
87
  opId = `${httpFunction.functionInfo.name}-${uuid()}`
75
88
  }
76
89
 
77
- const path = this.createOperationObject(event.http.method || event.httpApi.method, documentation, opId)
90
+ const path = await this.createOperationObject(event.http.method || event.httpApi.method, documentation, opId)
91
+ .catch(err => {
92
+ throw err
93
+ })
94
+
78
95
  if (httpFunction.functionInfo?.summary)
79
96
  path.summary = httpFunction.functionInfo.summary
80
97
 
@@ -158,7 +175,7 @@ class DefinitionGenerator {
158
175
  Object.assign(this.openAPI, {tags: tags})
159
176
  }
160
177
 
161
- createOperationObject(method, documentation, name = uuid()) {
178
+ async createOperationObject(method, documentation, name = uuid()) {
162
179
  const obj = {
163
180
  summary: documentation.summary || '',
164
181
  description: documentation.description || '',
@@ -168,22 +185,34 @@ class DefinitionGenerator {
168
185
  }
169
186
 
170
187
  if (documentation.pathParams) {
171
- const paramObject = this.createParamObject('path', documentation)
188
+ const paramObject = await this.createParamObject('path', documentation)
189
+ .catch(err => {
190
+ throw err
191
+ })
172
192
  obj.parameters = obj.parameters.concat(paramObject)
173
193
  }
174
194
 
175
195
  if (documentation.queryParams) {
176
- const paramObject = this.createParamObject('query', documentation)
196
+ const paramObject = await this.createParamObject('query', documentation)
197
+ .catch(err => {
198
+ throw err
199
+ })
177
200
  obj.parameters = obj.parameters.concat(paramObject)
178
201
  }
179
202
 
180
203
  if (documentation.headerParams) {
181
- const paramObject = this.createParamObject('header', documentation)
204
+ const paramObject = await this.createParamObject('header', documentation)
205
+ .catch(err => {
206
+ throw err
207
+ })
182
208
  obj.parameters = obj.parameters.concat(paramObject)
183
209
  }
184
210
 
185
211
  if (documentation.cookieParams) {
186
- const paramObject = this.createParamObject('cookie', documentation)
212
+ const paramObject = await this.createParamObject('cookie', documentation)
213
+ .catch(err => {
214
+ throw err
215
+ })
187
216
  obj.parameters = obj.parameters.concat(paramObject)
188
217
  }
189
218
 
@@ -195,10 +224,16 @@ class DefinitionGenerator {
195
224
  obj.deprecated = documentation.deprecated
196
225
 
197
226
  if (documentation.requestBody)
198
- obj.requestBody = this.createRequestBody(documentation)
227
+ obj.requestBody = await this.createRequestBody(documentation)
228
+ .catch(err => {
229
+ throw err
230
+ })
199
231
 
200
232
  if (documentation.methodResponses)
201
- obj.responses = this.createResponses(documentation)
233
+ obj.responses = await this.createResponses(documentation)
234
+ .catch(err => {
235
+ throw err
236
+ })
202
237
 
203
238
  if (documentation.servers) {
204
239
  const servers = this.createServers(documentation.servers)
@@ -208,14 +243,17 @@ class DefinitionGenerator {
208
243
  return {[method]: obj}
209
244
  }
210
245
 
211
- createResponses(documentation) {
246
+ async createResponses(documentation) {
212
247
  const responses = {}
213
248
  for (const response of documentation.methodResponses) {
214
249
  const obj = {
215
250
  description: response.responseBody.description || '',
216
251
  }
217
252
 
218
- obj.content = this.createMediaTypeObject(response.responseModels, 'responses')
253
+ obj.content = await this.createMediaTypeObject(response.responseModels, 'responses')
254
+ .catch(err => {
255
+ throw err
256
+ })
219
257
 
220
258
  Object.assign(responses,{[response.statusCode]: obj})
221
259
  }
@@ -223,18 +261,21 @@ class DefinitionGenerator {
223
261
  return responses
224
262
  }
225
263
 
226
- createRequestBody(documentation) {
264
+ async createRequestBody(documentation) {
227
265
  const obj = {
228
266
  description: documentation.requestBody.description,
229
267
  required: documentation.requestBody.required || false,
230
268
  }
231
269
 
232
- obj.content = this.createMediaTypeObject(documentation.requestModels, 'requestBody')
270
+ obj.content = await this.createMediaTypeObject(documentation.requestModels, 'requestBody')
271
+ .catch(err => {
272
+ throw err
273
+ })
233
274
 
234
275
  return obj
235
276
  }
236
277
 
237
- createMediaTypeObject(models, type) {
278
+ async createMediaTypeObject(models, type) {
238
279
  const mediaTypeObj = {}
239
280
  for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
240
281
  if (Object.values(models).includes(mediaTypeDocumentation.name)) {
@@ -252,59 +293,12 @@ class DefinitionGenerator {
252
293
  obj.examples = this.createExamples(mediaTypeDocumentation.examples)
253
294
 
254
295
  if (mediaTypeDocumentation.content[contentKey].schema) {
255
- const schema = SchemaConvertor.convert(mediaTypeDocumentation.content[contentKey].schema)
256
- for (const key of Object.keys(schema.schemas)) {
257
- if (key === 'main' || key.split('-')[0] === 'main') {
258
- obj.schema = {
259
- $ref: `#/components/schemas/${mediaTypeDocumentation.name}`
260
- }
261
-
262
- if (this.openAPI?.components) {
263
- if (this.openAPI.components?.schemas) {
264
- const schemaObj = {
265
- [mediaTypeDocumentation.name]: schema.schemas[key]
266
- }
267
- Object.assign(this.openAPI.components.schemas, schemaObj)
268
- } else {
269
- const schemaObj = {
270
- [mediaTypeDocumentation.name]: schema.schemas[key]
271
- }
272
- Object.assign(this.openAPI.components, {schemas: schemaObj})
273
- }
274
- } else {
275
- const components = {
276
- components: {
277
- schemas: {
278
- [mediaTypeDocumentation.name]: schema.schemas[key]
279
- }
280
- }
281
- }
282
- Object.assign(this.openAPI, components)
283
- }
284
- } else {
285
- if (this.openAPI?.components) {
286
- if (this.openAPI.components?.schemas) {
287
- const schemaObj = {
288
- [key]: schema.schemas[key]
289
- }
290
- Object.assign(this.openAPI.components.schemas, schemaObj)
291
- } else {
292
- const schemaObj = {
293
- [key]: schema.schemas[key]
294
- }
295
- Object.assign(this.openAPI.components, {schemas: schemaObj})
296
- }
297
- } else {
298
- const components = {
299
- components: {
300
- schemas: {
301
- [key]: schema.schemas[key]
302
- }
303
- }
304
- }
305
- Object.assign(this.openAPI, components)
306
- }
307
- }
296
+ const schemaRef = await this.schemaCreator(mediaTypeDocumentation.content[contentKey].schema, mediaTypeDocumentation.name)
297
+ .catch(err => {
298
+ throw err
299
+ })
300
+ obj.schema = {
301
+ $ref: schemaRef
308
302
  }
309
303
  }
310
304
 
@@ -314,7 +308,7 @@ class DefinitionGenerator {
314
308
  return mediaTypeObj
315
309
  }
316
310
 
317
- createParamObject(paramIn, documentation) {
311
+ async createParamObject(paramIn, documentation) {
318
312
  const params = []
319
313
  for (const param of documentation[`${paramIn}Params`]) {
320
314
  const obj = {
@@ -335,7 +329,7 @@ class DefinitionGenerator {
335
329
  if (param.style)
336
330
  obj.style = param.style
337
331
 
338
- if (param.explode)
332
+ if (Object.keys(param).includes('explode'))
339
333
  obj.explode = param.explode
340
334
 
341
335
  if (paramIn === 'query' && param.allowReserved)
@@ -348,10 +342,12 @@ class DefinitionGenerator {
348
342
  obj.examples = this.createExamples(param.examples)
349
343
 
350
344
  if (param.schema) {
351
- const schema = SchemaConvertor.convert(param.schema)
352
- if (schema.schemas.main) {
353
- Object.assign(obj,{schema: schema.schemas.main})
354
-
345
+ const schemaRef = await this.schemaCreator(param.schema, param.name)
346
+ .catch(err => {
347
+ throw err
348
+ })
349
+ obj.schema = {
350
+ $ref: schemaRef
355
351
  }
356
352
  }
357
353
 
@@ -360,6 +356,55 @@ class DefinitionGenerator {
360
356
  return params;
361
357
  }
362
358
 
359
+ async schemaCreator(schema, name) {
360
+ const addToComponents = (schema, name) => {
361
+ const schemaObj = {
362
+ [name]: schema
363
+ }
364
+
365
+ if (this.openAPI?.components) {
366
+ if (this.openAPI.components?.schemas) {
367
+ Object.assign(this.openAPI.components.schemas, schemaObj)
368
+ } else {
369
+ Object.assign(this.openAPI.components, {schemas: schemaObj})
370
+ }
371
+ } else {
372
+ const components = {
373
+ components: {
374
+ schemas: schemaObj
375
+ }
376
+ }
377
+
378
+ Object.assign(this.openAPI, components)
379
+ }
380
+ }
381
+
382
+ if (typeof schema !== 'string' && Object.keys(schema).length > 0) {
383
+ const convertedSchema = SchemaConvertor.convert(schema)
384
+ for (const key of Object.keys(convertedSchema.schemas)) {
385
+ if (key === 'main' || key.split('-')[0] === 'main') {
386
+ const ref = `#/components/schemas/${name}`
387
+
388
+ addToComponents(convertedSchema.schemas[key], name)
389
+ return ref
390
+ } else {
391
+ addToComponents(convertedSchema.schemas[key], key)
392
+ }
393
+ }
394
+ } else {
395
+ const combinedSchema = await $RefParser.dereference(schema, this.refParserOptions)
396
+ .catch(err => {
397
+ console.error(err)
398
+ throw err
399
+ })
400
+
401
+ return await this.schemaCreator(combinedSchema, name)
402
+ .catch(err => {
403
+ throw err
404
+ })
405
+ }
406
+ }
407
+
363
408
  createExamples(examples) {
364
409
  const examplesObj = {}
365
410
 
@@ -104,12 +104,15 @@ class OpenAPIGenerator {
104
104
  const config = this.processCliInput()
105
105
  const generator = new DefinitionGenerator(this.serverless);
106
106
 
107
- generator.parse();
107
+ await generator.parse()
108
+ .catch(err => {
109
+ this.log('error', chalk.bold.red(`ERROR: An error was thrown generating the OpenAPI v3 documentation`))
110
+ throw new this.serverless.classes.Error(err)
111
+ })
108
112
 
109
113
  const valid = await generator.validate()
110
114
  .catch(err => {
111
-
112
- this.log('error', chalk.bold.red(`ERROR: An error was thrown generation the OpenAPI v3 documentation`))
115
+ this.log('error', chalk.bold.red(`ERROR: An error was thrown validating the OpenAPI v3 documentation`))
113
116
  throw new this.serverless.classes.Error(err)
114
117
  })
115
118