Skip to content

Commit

Permalink
Merge pull request #113 from JaredCE/better-model-behaviour
Browse files Browse the repository at this point in the history
Big rewrite of the schema handling
  • Loading branch information
JaredCE authored May 21, 2023
2 parents 3e581c6 + 219d335 commit 49121a1
Show file tree
Hide file tree
Showing 21 changed files with 9,686 additions and 586 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-openapi-documenter",
"version": "0.0.53",
"version": "0.0.60",
"description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
"main": "index.js",
"keywords": [
Expand Down
97 changes: 19 additions & 78 deletions src/definitionGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ const path = require('path')

const { v4: uuid } = require('uuid')
const validator = require('oas-validator');
const SchemaConvertor = require('json-schema-for-openapi')
const $RefParser = require("@apidevtools/json-schema-ref-parser")
const isEqual = require('lodash.isequal')

const SchemaHandler = require('./schemaHandler')

class DefinitionGenerator {
constructor(serverless, options = {}) {
Expand All @@ -25,8 +24,13 @@ class DefinitionGenerator {

this.openAPI = {
openapi: this.version,
components: {
schemas: {}
}
}

this.schemaHandler = new SchemaHandler(serverless, this.openAPI)

this.operationIds = []
this.schemaIDs = []

Expand Down Expand Up @@ -64,6 +68,11 @@ class DefinitionGenerator {
async parse() {
this.createInfo()

await this.schemaHandler.addModelsToOpenAPI()
.catch(err => {
throw err
})

if (this.serverless.service.custom.documentation.securitySchemes) {
this.createSecuritySchemes(this.serverless.service.custom.documentation.securitySchemes)

Expand Down Expand Up @@ -360,7 +369,8 @@ class DefinitionGenerator {
}
}
} else {
obj.headers = corsHeaders
if (Object.keys(corsHeaders).length)
obj.headers = corsHeaders
}

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

if (headers[header].schema) {
const schemaRef = await this.schemaCreator(headers[header].schema, header)
const schemaRef = await this.schemaHandler.createSchema(header, headers[header].schema)
.catch(err => {
throw err
})
Expand Down Expand Up @@ -445,7 +455,7 @@ class DefinitionGenerator {

async createMediaTypeObject(models, type) {
const mediaTypeObj = {}
for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
for (const mediaTypeDocumentation of this.schemaHandler.models) {
if (models === undefined || models === null) {
throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
}
Expand Down Expand Up @@ -477,10 +487,11 @@ class DefinitionGenerator {
schema = mediaTypeDocumentation.schema
}

const schemaRef = await this.schemaCreator(schema, mediaTypeDocumentation.name)
const schemaRef = await this.schemaHandler.createSchema(mediaTypeDocumentation.name)
.catch(err => {
throw err
})

obj.schema = {
$ref: schemaRef
}
Expand Down Expand Up @@ -525,7 +536,7 @@ class DefinitionGenerator {
obj.examples = this.createExamples(param.examples)

if (param.schema) {
const schemaRef = await this.schemaCreator(param.schema, param.name)
const schemaRef = await this.schemaHandler.createSchema(param.name, param.schema)
.catch(err => {
throw err
})
Expand All @@ -539,76 +550,6 @@ class DefinitionGenerator {
return params;
}

async dereferenceSchema(schema) {
let originalSchema = await $RefParser.bundle(schema, this.refParserOptions)
.catch(err => {
console.error(err)
throw err
})

let deReferencedSchema = await $RefParser.dereference(originalSchema, this.refParserOptions)
.catch(err => {
console.error(err)
throw err
})

// deal with schemas that have been de-referenced poorly: naive
if (deReferencedSchema?.$ref === '#') {
const oldRef = originalSchema.$ref
const path = oldRef.split('/')

const pathTitle = path[path.length - 1]
const referencedProperties = deReferencedSchema.definitions[pathTitle]

Object.assign(deReferencedSchema, { ...referencedProperties })

delete deReferencedSchema.$ref
deReferencedSchema = await this.dereferenceSchema(deReferencedSchema)
.catch((err) => {
throw err
})
}

return deReferencedSchema
}

async schemaCreator(schema, name) {
let schemaName = name
let finalName = schemaName
const dereferencedSchema = await this.dereferenceSchema(schema)
.catch(err => {
console.error(err)
throw err
})

const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, schemaName)

for (const convertedSchemaName of Object.keys(convertedSchemas.schemas)) {
const convertedSchema = convertedSchemas.schemas[convertedSchemaName]
if (this.existsInComponents(convertedSchemaName)) {
if (this.isTheSameSchema(convertedSchema, convertedSchemaName) === false) {
if (convertedSchemaName === schemaName) {
finalName = `${schemaName}-${uuid()}`
this.addToComponents(this.componentTypes.schemas, convertedSchema, finalName)
} else
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
}
} else {
this.addToComponents(this.componentTypes.schemas, convertedSchema, convertedSchemaName)
}
}

return `#/components/schemas/${finalName}`
}

existsInComponents(name) {
return Boolean(this.openAPI?.components?.schemas?.[name])
}

isTheSameSchema(schema, otherSchemaName) {
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
}

addToComponents(type, schema, name) {
const schemaObj = {
[name]: schema
Expand Down
189 changes: 189 additions & 0 deletions src/schemaHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
'use strict'

const path = require('path')

const $RefParser = require("@apidevtools/json-schema-ref-parser")
const SchemaConvertor = require('json-schema-for-openapi')
const isEqual = require('lodash.isequal')
const { v4: uuid } = require('uuid')

class SchemaHandler {
constructor(serverless, openAPI) {
this.documentation = serverless.service.custom.documentation
this.openAPI = openAPI

this.modelReferences = {}

this.__standardiseModels()

try {
this.refParserOptions = require(path.resolve('options', 'ref-parser.js'))
} catch (err) {
this.refParserOptions = {}
}
}

/**
* Standardises the models to a specific format
*/
__standardiseModels() {
const standardModel = (model) => {
if (model.schema) {
return model
}

const contentType = Object.keys(model.content)[0]
model.contentType = contentType
model.schema = model.content[contentType].schema

return model
}

const standardisedModels = this.documentation?.models?.map(standardModel) || []
const standardisedModelsList = this.documentation?.modelsList?.map(standardModel) || []

this.models = standardisedModels.length ? standardisedModels.concat(standardisedModelsList) : standardisedModelsList
}

async addModelsToOpenAPI() {
for (const model of this.models) {
const modelName = model.name
const modelSchema = model.schema

const dereferencedSchema = await this.__dereferenceSchema(modelSchema)
.catch(err => {
if(err.errors) {
for (const error of err?.errors) {
if (error.message.includes('HTTP ERROR')) {
throw err
}
}
}
return modelSchema
})

const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, modelName)

for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
if (schemaName === modelName) {
this.modelReferences[schemaName] = `#/components/schemas/${modelName}`
}

this.__addToComponents('schemas', schemaValue, schemaName)
}
}
}

async createSchema(name, schema) {
let originalName = name;
let finalName = name;

if (this.modelReferences[name] && schema === undefined) {
return this.modelReferences[name]
}

const dereferencedSchema = await this.__dereferenceSchema(schema)
.catch(err => {
throw err
})

const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name)

for (const [schemaName, schemaValue] of Object.entries(convertedSchemas.schemas)) {
if (this.__existsInComponents(schemaName)) {
if (this.__isTheSameSchema(schemaValue, schemaName) === false) {
if (schemaName === originalName) {
finalName = `${schemaName}-${uuid()}`
this.__addToComponents('schemas', schemaValue, finalName)
} else {
this.__addToComponents('schemas', schemaValue, schemaName)
}
}
} else {
this.__addToComponents('schemas', schemaValue, schemaName)
}
}

return `#/components/schemas/${finalName}`
}

async __dereferenceSchema(schema) {
const bundledSchema = await $RefParser.bundle(schema, this.refParserOptions)
.catch(err => {
throw err
})

let deReferencedSchema = await $RefParser.dereference(bundledSchema, this.refParserOptions)
.catch(err => {
throw err
})

// deal with schemas that have been de-referenced poorly: naive
if (deReferencedSchema?.$ref === '#') {
const oldRef = bundledSchema.$ref
const path = oldRef.split('/')

const pathTitle = path[path.length - 1]
const referencedProperties = deReferencedSchema.definitions[pathTitle]

Object.assign(deReferencedSchema, { ...referencedProperties })

delete deReferencedSchema.$ref
deReferencedSchema = await this.__dereferenceSchema(deReferencedSchema)
.catch((err) => {
throw err
})
}

return deReferencedSchema
}

/**
* @function existsInComponents
* @param {string} name - The name of the Schema
* @returns {boolean} Whether it exists in components already
*/
__existsInComponents(name) {
return Boolean(this.openAPI?.components?.schemas?.[name])
}

/**
* @function isTheSameSchema
* @param {object} schema - The schema value
* @param {string} otherSchemaName - The name of the schema
* @returns {boolean} Whether the schema provided is the same one as in components already
*/
__isTheSameSchema(schema, otherSchemaName) {
return isEqual(schema, this.openAPI.components.schemas[otherSchemaName])
}

/**
* @function addToComponents
* @param {string} type - The component type
* @param {object} schema - The schema
* @param {string} name - The name of the schema
*/
__addToComponents(type, schema, name) {
const schemaObj = {
[name]: schema
}

if (this.openAPI?.components) {
if (this.openAPI.components[type]) {
Object.assign(this.openAPI.components[type], schemaObj)
} else {
Object.assign(this.openAPI.components, { [type]: schemaObj })
}
} else {
const components = {
components: {
[type]: schemaObj
}
}

Object.assign(this.openAPI, components)
}
}
}

module.exports = SchemaHandler;
17 changes: 17 additions & 0 deletions test/models/models/models-alt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"models": [
{
"name": "ErrorResponse",
"description": "The Error Response",
"contentType": "application/json",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
]
}
Loading

0 comments on commit 49121a1

Please sign in to comment.