Skip to content

Commit 8c79a06

Browse files
authored
Merge pull request #53 from JaredCE/better-generation-error-handling
Better generation error handling
2 parents d2bf7f1 + 49627ba commit 8c79a06

File tree

7 files changed

+227
-37
lines changed

7 files changed

+227
-37
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
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.24",
3+
"version": "0.0.25",
44
"description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
55
"main": "index.js",
66
"keywords": [

src/definitionGenerator.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class DefinitionGenerator {
7979
if (event?.http?.documentation || event?.httpApi?.documentation) {
8080
const documentation = event?.http?.documentation || event?.httpApi?.documentation
8181

82+
this.currentFunctionName = httpFunction.functionInfo.name
83+
8284
let opId
8385
if (this.operationIds.includes(httpFunction.functionInfo.name) === false) {
8486
opId = httpFunction.functionInfo.name
@@ -262,6 +264,8 @@ class DefinitionGenerator {
262264
description: response.responseBody.description || '',
263265
}
264266

267+
this.currentStatusCode = response.statusCode
268+
265269
obj.content = await this.createMediaTypeObject(response.responseModels, 'responses')
266270
.catch(err => {
267271
throw err
@@ -290,6 +294,10 @@ class DefinitionGenerator {
290294
async createMediaTypeObject(models, type) {
291295
const mediaTypeObj = {}
292296
for (const mediaTypeDocumentation of this.serverless.service.custom.documentation.models) {
297+
if (models === undefined || models === null) {
298+
throw new Error(`${this.currentFunctionName} is missing a Response Model for statusCode ${this.currentStatusCode}`)
299+
}
300+
293301
if (Object.values(models).includes(mediaTypeDocumentation.name)) {
294302
let contentKey = ''
295303
for (const [key, value] of Object.entries(models)) {

src/openAPIGenerator.js

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,35 +107,24 @@ class OpenAPIGenerator {
107107
async generate() {
108108
this.log(this.defaultLog, chalk.bold.underline('OpenAPI v3 Document Generation'))
109109
this.processCliInput()
110-
const generator = new DefinitionGenerator(this.serverless);
111110

112-
await generator.parse()
111+
const validOpenAPI = await this.generationAndValidation()
113112
.catch(err => {
114-
this.log('error', `ERROR: An error was thrown generating the OpenAPI v3 documentation`)
115113
throw new this.serverless.classes.Error(err)
116114
})
117115

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-
127116
if (this.config.postmanCollection) {
128-
this.createPostman(generator.openAPI)
117+
this.createPostman(validOpenAPI)
129118
}
130119

131120
let output
132121
switch (this.config.format.toLowerCase()) {
133122
case 'json':
134-
output = JSON.stringify(generator.openAPI, null, this.config.indent);
123+
output = JSON.stringify(validOpenAPI, null, this.config.indent);
135124
break;
136125
case 'yaml':
137126
default:
138-
output = yaml.dump(generator.openAPI, { indent: this.config.indent });
127+
output = yaml.dump(validOpenAPI, { indent: this.config.indent });
139128
break;
140129
}
141130
try {
@@ -147,6 +136,28 @@ class OpenAPIGenerator {
147136
}
148137
}
149138

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+
150161
createPostman(openAPI) {
151162
const postmanGeneration = (err, result) => {
152163
if (err) {
@@ -205,25 +216,10 @@ class OpenAPIGenerator {
205216
this.config = config
206217
}
207218

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`);
227223
}
228224
}
229225

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

test/unit/openAPIGenerator.spec.js

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ const expect = require('chai').expect
77

88
const validOpenAPI = require('../json/valid-openAPI.json')
99

10+
const basicDocumentation = require('../models/BasicDocumentation.json')
11+
const basicValidFunction = require('../models/BasicValidFunction.json')
12+
1013
const OpenAPIGenerator = require('../../src/openAPIGenerator')
1114

1215
describe('OpenAPIGenerator', () => {
1316
let sls, logOutput
1417
beforeEach(function() {
1518
sls = {
19+
service: {
20+
service: 'test-service',
21+
getAllFunctions: () => {},
22+
getFunction: () => {}
23+
},
1624
version: '3.0.0',
1725
variables: {
1826
service: {
@@ -32,7 +40,7 @@ describe('OpenAPIGenerator', () => {
3240
options: {
3341
postmanCollection: 'postman.json'
3442
}
35-
}
43+
},
3644
}
3745

3846
logOutput = {
@@ -43,6 +51,119 @@ describe('OpenAPIGenerator', () => {
4351
}
4452
}
4553
});
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+
46167
describe('createPostman', () => {
47168
it('should generate a postman collection when a valid openAPI file is generated', function() {
48169
const fsStub = sinon.stub(fs, 'writeFileSync').returns(true)

0 commit comments

Comments
 (0)