Skip to content

Commit 49121a1

Browse files
authored
Merge pull request #113 from JaredCE/better-model-behaviour
Big rewrite of the schema handling
2 parents 3e581c6 + 219d335 commit 49121a1

File tree

21 files changed

+9686
-586
lines changed

21 files changed

+9686
-586
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "serverless-openapi-documenter",
3-
"version": "0.0.53",
3+
"version": "0.0.60",
44
"description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
55
"main": "index.js",
66
"keywords": [

src/definitionGenerator.js

Lines changed: 19 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ const path = require('path')
44

55
const { v4: uuid } = require('uuid')
66
const validator = require('oas-validator');
7-
const SchemaConvertor = require('json-schema-for-openapi')
8-
const $RefParser = require("@apidevtools/json-schema-ref-parser")
9-
const isEqual = require('lodash.isequal')
7+
8+
const SchemaHandler = require('./schemaHandler')
109

1110
class DefinitionGenerator {
1211
constructor(serverless, options = {}) {
@@ -25,8 +24,13 @@ class DefinitionGenerator {
2524

2625
this.openAPI = {
2726
openapi: this.version,
27+
components: {
28+
schemas: {}
29+
}
2830
}
2931

32+
this.schemaHandler = new SchemaHandler(serverless, this.openAPI)
33+
3034
this.operationIds = []
3135
this.schemaIDs = []
3236

@@ -64,6 +68,11 @@ class DefinitionGenerator {
6468
async parse() {
6569
this.createInfo()
6670

71+
await this.schemaHandler.addModelsToOpenAPI()
72+
.catch(err => {
73+
throw err
74+
})
75+
6776
if (this.serverless.service.custom.documentation.securitySchemes) {
6877
this.createSecuritySchemes(this.serverless.service.custom.documentation.securitySchemes)
6978

@@ -360,7 +369,8 @@ class DefinitionGenerator {
360369
}
361370
}
362371
} else {
363-
obj.headers = corsHeaders
372+
if (Object.keys(corsHeaders).length)
373+
obj.headers = corsHeaders
364374
}
365375

366376
Object.assign(responses, { [response.statusCode]: obj })
@@ -414,7 +424,7 @@ class DefinitionGenerator {
414424
newHeader.description = headers[header].description || ''
415425

416426
if (headers[header].schema) {
417-
const schemaRef = await this.schemaCreator(headers[header].schema, header)
427+
const schemaRef = await this.schemaHandler.createSchema(header, headers[header].schema)
418428
.catch(err => {
419429
throw err
420430
})
@@ -445,7 +455,7 @@ class DefinitionGenerator {
445455

446456
async createMediaTypeObject(models, type) {
447457
const mediaTypeObj = {}
448-
for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
458+
for (const mediaTypeDocumentation of this.schemaHandler.models) {
449459
if (models === undefined || models === null) {
450460
throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
451461
}
@@ -477,10 +487,11 @@ class DefinitionGenerator {
477487
schema = mediaTypeDocumentation.schema
478488
}
479489

480-
const schemaRef = await this.schemaCreator(schema, mediaTypeDocumentation.name)
490+
const schemaRef = await this.schemaHandler.createSchema(mediaTypeDocumentation.name)
481491
.catch(err => {
482492
throw err
483493
})
494+
484495
obj.schema = {
485496
$ref: schemaRef
486497
}
@@ -525,7 +536,7 @@ class DefinitionGenerator {
525536
obj.examples = this.createExamples(param.examples)
526537

527538
if (param.schema) {
528-
const schemaRef = await this.schemaCreator(param.schema, param.name)
539+
const schemaRef = await this.schemaHandler.createSchema(param.name, param.schema)
529540
.catch(err => {
530541
throw err
531542
})
@@ -539,76 +550,6 @@ class DefinitionGenerator {
539550
return params;
540551
}
541552

542-
async dereferenceSchema(schema) {
543-
let originalSchema = await $RefParser.bundle(schema, this.refParserOptions)
544-
.catch(err => {
545-
console.error(err)
546-
throw err
547-
})
548-
549-
let deReferencedSchema = await $RefParser.dereference(originalSchema, this.refParserOptions)
550-
.catch(err => {
551-
console.error(err)
552-
throw err
553-
})
554-
555-
// deal with schemas that have been de-referenced poorly: naive
556-
if (deReferencedSchema?.$ref === '#') {
557-
const oldRef = originalSchema.$ref
558-
const path = oldRef.split('/')
559-
560-
const pathTitle = path[path.length - 1]
561-
const referencedProperties = deReferencedSchema.definitions[pathTitle]
562-
563-
Object.assign(deReferencedSchema, { ...referencedProperties })
564-
565-
delete deReferencedSchema.$ref
566-
deReferencedSchema = await this.dereferenceSchema(deReferencedSchema)
567-
.catch((err) => {
568-
throw err
569-
})
570-
}
571-
572-
return deReferencedSchema
573-
}
574-
575-
async schemaCreator(schema, name) {
576-
let schemaName = name
577-
let finalName = schemaName
578-
const dereferencedSchema = await this.dereferenceSchema(schema)
579-
.catch(err => {
580-
console.error(err)
581-
throw err
582-
})
583-
584-
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, schemaName)
585-
586-
for (const convertedSchemaName of Object.keys(convertedSchemas.schemas)) {
587-
const convertedSchema = convertedSchemas.schemas[convertedSchemaName]
588-
if (this.existsInComponents(convertedSchemaName)) {
589-
if (this.isTheSameSchema(convertedSchema, convertedSchemaName) === false) {
590-
if (convertedSchemaName === schemaName) {
591-
finalName = `${schemaName}-${uuid()}`
592-
this.addToComponents(this.componentTypes.schemas, convertedSchema, finalName)
593-
} else
594-
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
595-
}
596-
} else {
597-
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
598-
}
599-
}
600-
601-
return `#/components/schemas/${finalName}`
602-
}
603-
604-
existsInComponents(name) {
605-
return Boolean(this.openAPI?.components?.schemas?.[name])
606-
}
607-
608-
isTheSameSchema(schema, otherSchemaName) {
609-
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
610-
}
611-
612553
addToComponents(type, schema, name) {
613554
const schemaObj = {
614555
[name]: schema

src/schemaHandler.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
5+
const $RefParser = require("@apidevtools/json-schema-ref-parser")
6+
const SchemaConvertor = require('json-schema-for-openapi')
7+
const isEqual = require('lodash.isequal')
8+
const { v4: uuid } = require('uuid')
9+
10+
class SchemaHandler {
11+
constructor(serverless, openAPI) {
12+
this.documentation = serverless.service.custom.documentation
13+
this.openAPI = openAPI
14+
15+
this.modelReferences = {}
16+
17+
this.__standardiseModels()
18+
19+
try {
20+
this.refParserOptions = require(path.resolve('options', 'ref-parser.js'))
21+
} catch (err) {
22+
this.refParserOptions = {}
23+
}
24+
}
25+
26+
/**
27+
* Standardises the models to a specific format
28+
*/
29+
__standardiseModels() {
30+
const standardModel = (model) => {
31+
if (model.schema) {
32+
return model
33+
}
34+
35+
const contentType = Object.keys(model.content)[0]
36+
model.contentType = contentType
37+
model.schema = model.content[contentType].schema
38+
39+
return model
40+
}
41+
42+
const standardisedModels = this.documentation?.models?.map(standardModel) || []
43+
const standardisedModelsList = this.documentation?.modelsList?.map(standardModel) || []
44+
45+
this.models = standardisedModels.length ? standardisedModels.concat(standardisedModelsList) : standardisedModelsList
46+
}
47+
48+
async addModelsToOpenAPI() {
49+
for (const model of this.models) {
50+
const modelName = model.name
51+
const modelSchema = model.schema
52+
53+
const dereferencedSchema = await this.__dereferenceSchema(modelSchema)
54+
.catch(err => {
55+
if(err.errors) {
56+
for (const error of err?.errors) {
57+
if (error.message.includes('HTTP ERROR')) {
58+
throw err
59+
}
60+
}
61+
}
62+
return modelSchema
63+
})
64+
65+
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, modelName)
66+
67+
for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
68+
if (schemaName === modelName) {
69+
this.modelReferences[schemaName] = `#/components/schemas/${modelName}`
70+
}
71+
72+
this.__addToComponents('schemas', schemaValue, schemaName)
73+
}
74+
}
75+
}
76+
77+
async createSchema(name, schema) {
78+
let originalName = name;
79+
let finalName = name;
80+
81+
if (this.modelReferences[name] && schema === undefined) {
82+
return this.modelReferences[name]
83+
}
84+
85+
const dereferencedSchema = await this.__dereferenceSchema(schema)
86+
.catch(err => {
87+
throw err
88+
})
89+
90+
const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name)
91+
92+
for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
93+
if (this.__existsInComponents(schemaName)) {
94+
if (this.__isTheSameSchema(schemaValue, schemaName) === false) {
95+
if (schemaName === originalName) {
96+
finalName = `${schemaName}-${uuid()}`
97+
this.__addToComponents('schemas', schemaValue, finalName)
98+
} else {
99+
this.__addToComponents('schemas', schemaValue, schemaName)
100+
}
101+
}
102+
} else {
103+
this.__addToComponents('schemas', schemaValue, schemaName)
104+
}
105+
}
106+
107+
return `#/components/schemas/${finalName}`
108+
}
109+
110+
async __dereferenceSchema(schema) {
111+
const bundledSchema = await $RefParser.bundle(schema, this.refParserOptions)
112+
.catch(err => {
113+
throw err
114+
})
115+
116+
let deReferencedSchema = await $RefParser.dereference(bundledSchema, this.refParserOptions)
117+
.catch(err => {
118+
throw err
119+
})
120+
121+
// deal with schemas that have been de-referenced poorly: naive
122+
if (deReferencedSchema?.$ref === '#') {
123+
const oldRef = bundledSchema.$ref
124+
const path = oldRef.split('/')
125+
126+
const pathTitle = path[path.length - 1]
127+
const referencedProperties = deReferencedSchema.definitions[pathTitle]
128+
129+
Object.assign(deReferencedSchema, { ...referencedProperties })
130+
131+
delete deReferencedSchema.$ref
132+
deReferencedSchema = await this.__dereferenceSchema(deReferencedSchema)
133+
.catch((err) => {
134+
throw err
135+
})
136+
}
137+
138+
return deReferencedSchema
139+
}
140+
141+
/**
142+
* @function existsInComponents
143+
* @param {string} name - The name of the Schema
144+
* @returns {boolean} Whether it exists in components already
145+
*/
146+
__existsInComponents(name) {
147+
return Boolean(this.openAPI?.components?.schemas?.[name])
148+
}
149+
150+
/**
151+
* @function isTheSameSchema
152+
* @param {object} schema - The schema value
153+
* @param {string} otherSchemaName - The name of the schema
154+
* @returns {boolean} Whether the schema provided is the same one as in components already
155+
*/
156+
__isTheSameSchema(schema, otherSchemaName) {
157+
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
158+
}
159+
160+
/**
161+
* @function addToComponents
162+
* @param {string} type - The component type
163+
* @param {object} schema - The schema
164+
* @param {string} name - The name of the schema
165+
*/
166+
__addToComponents(type, schema, name) {
167+
const schemaObj = {
168+
[name]: schema
169+
}
170+
171+
if (this.openAPI?.components) {
172+
if (this.openAPI.components[type]) {
173+
Object.assign(this.openAPI.components[type], schemaObj)
174+
} else {
175+
Object.assign(this.openAPI.components, { [type]: schemaObj })
176+
}
177+
} else {
178+
const components = {
179+
components: {
180+
[type]: schemaObj
181+
}
182+
}
183+
184+
Object.assign(this.openAPI, components)
185+
}
186+
}
187+
}
188+
189+
module.exports = SchemaHandler;

test/models/models/models-alt.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"models": [
3+
{
4+
"name": "ErrorResponse",
5+
"description": "The Error Response",
6+
"contentType": "application/json",
7+
"schema": {
8+
"type": "object",
9+
"properties": {
10+
"error": {
11+
"type": "string"
12+
}
13+
}
14+
}
15+
}
16+
]
17+
}

0 commit comments

Comments
 (0)