Skip to content

Commit 86c236f

Browse files
authored
Merge pull request #2 from temando/tests
Tests
2 parents d8a6bb8 + 5542777 commit 86c236f

18 files changed

+2393
-173
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
"test:coverage": "jest --coverage",
1616
"lint": "tslint 'src/**/*.ts'",
1717
"release": "cd build && npm publish",
18+
"test:project": "cd test/project && yarn sls openapi generate",
19+
"test:prepare": "scripts/prepareTests.bash",
20+
"build:link": "yarn build && cd build && yarn link",
21+
"build:watch": "yarn build && tsc --watch",
1822
"build": "scripts/build.bash"
1923
},
2024
"devDependencies": {

scripts/prepareTests.bash

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
4+
yarn build:link \
5+
&& cd test/project \
6+
&& yarn \
7+
&& yarn link serverless-openapi-documentation

src/DocumentGenerator.ts renamed to src/DefinitionGenerator.ts

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,76 @@
11
import { dereference } from '@jdw/jst';
2-
import * as c from 'chalk';
3-
import * as openApiValidator from 'swagger2openapi/validate.js';
2+
import * as openApiValidator from 'swagger2openapi/validate';
43

54
import * as uuid from 'uuid';
6-
import { IParameterConfig, IServerlessFunctionConfig, IServiceDescription } from './types';
5+
import { IDefinition, IDefinitionConfig, IParameterConfig, IServerlessFunctionConfig } from './types';
76
import { clone, merge } from './utils';
87

9-
export class DocumentGenerator {
8+
export class DefinitionGenerator {
109
// The OpenAPI version we currently validate against
11-
private openapiVersion = '3.0.0-RC1';
10+
public version = '3.0.0-RC1';
1211

1312
// Base configuration object
14-
private config = {
15-
openapi: this.openapiVersion,
16-
description: '',
17-
version: '0.0.0',
18-
title: '',
19-
paths: {},
20-
components: {
21-
schemas: {},
22-
},
13+
public definition = <IDefinition> {
14+
openapi: this.version,
15+
components: {},
2316
};
2417

25-
private serviceDescriptor: IServiceDescription;
18+
public config: IDefinitionConfig;
2619

2720
/**
2821
* Constructor
2922
* @param serviceDescriptor IServiceDescription
3023
*/
31-
constructor (serviceDescriptor: IServiceDescription) {
32-
this.serviceDescriptor = clone(serviceDescriptor);
33-
34-
merge(this.config, {
35-
openapi: this.openapiVersion,
36-
servers: [],
37-
info: {
38-
title: serviceDescriptor.summary || '',
39-
description: serviceDescriptor.description || '',
40-
version: serviceDescriptor.version || uuid.v4(),
41-
},
24+
constructor (config: IDefinitionConfig) {
25+
this.config = clone(config);
26+
}
27+
28+
public parse () {
29+
const {
30+
title = '',
31+
description = '',
32+
version = uuid.v4(),
33+
models,
34+
} = this.config;
35+
36+
merge(this.definition, {
37+
openapi: this.version,
38+
info: { title, description, version },
4239
paths: {},
4340
components: {
4441
schemas: {},
4542
securitySchemes: {},
4643
},
4744
});
4845

49-
for (const model of serviceDescriptor.models) {
50-
this.config.components.schemas[model.name] = this.cleanSchema(dereference(model.schema));
46+
if (models) {
47+
for (const model of models) {
48+
this.definition.components.schemas[model.name] = this.cleanSchema(
49+
dereference(model.schema),
50+
);
51+
}
5152
}
53+
54+
return this;
5255
}
5356

54-
public generate () {
55-
const result: any = {};
56-
process.stdout.write(`${ c.bold.yellow('[VALIDATION]') } Validating OpenAPI generated output\n`);
57+
public validate (): { valid: boolean, context: string[], warnings: any[], error?: any[] } {
58+
const payload: any = {};
59+
5760
try {
58-
openApiValidator.validateSync(this.config, result);
59-
process.stdout.write(`${ c.bold.green('[VALIDATION]') } OpenAPI valid: ${c.bold.green('true')}\n\n`);
60-
return this.config;
61-
} catch (e) {
62-
process.stdout.write(
63-
`${c.bold.red('[VALIDATION]')} Failed to validate OpenAPI document: \n\n${c.yellow(e.message)}\n\n` +
64-
`${c.bold.green('Path:')} ${result.context.pop()}\n`,
65-
);
66-
throw new Error('Failed to validate OpenAPI document');
61+
openApiValidator.validateSync(this.definition, payload);
62+
} catch (error) {
63+
payload.error = JSON.parse(error.message.replace(/^Failed OpenAPI3 schema validation: /, ''));
6764
}
65+
66+
return payload;
6867
}
6968

7069
/**
7170
* Add Paths to OpenAPI Configuration from Serverless function documentation
7271
* @param config Add
7372
*/
74-
public addPathsFromFunctionConfig (config: IServerlessFunctionConfig[]): void {
73+
public readFunctions (config: IServerlessFunctionConfig[]): void {
7574
// loop through function configurations
7675
for (const funcConfig of config) {
7776
// loop through http events
@@ -93,7 +92,7 @@ export class DocumentGenerator {
9392
},
9493
};
9594
// merge path configuration into main configuration
96-
merge(this.config.paths, pathConfig);
95+
merge(this.definition.paths, pathConfig);
9796
}
9897
}
9998
}
@@ -170,7 +169,6 @@ export class DocumentGenerator {
170169
: parameter.style === 'form';
171170
}
172171

173-
// console.log(parameter);
174172
if (parameter.schema) {
175173
parameterConfig.schema = this.cleanSchema(parameter.schema);
176174
}
@@ -181,7 +179,6 @@ export class DocumentGenerator {
181179
parameterConfig.examples = parameter.examples;
182180
}
183181

184-
// Add parameter config to parameters array
185182
parameters.push(parameterConfig);
186183
}
187184
}
@@ -201,7 +198,7 @@ export class DocumentGenerator {
201198
// For each request model type (Sorted by "Content-Type")
202199
for (const requestModelType of Object.keys(documentationConfig.requestModels)) {
203200
// get schema reference information
204-
const requestModel = this.serviceDescriptor.models.filter(
201+
const requestModel = this.config.models.filter(
205202
(model) => model.name === documentationConfig.requestModels[requestModelType],
206203
).pop();
207204

@@ -278,7 +275,7 @@ export class DocumentGenerator {
278275
private getResponseContent (response) {
279276
const content = {};
280277
for (const responseKey of Object.keys(response)) {
281-
const responseModel = this.serviceDescriptor.models.filter(
278+
const responseModel = this.config.models.filter(
282279
(model) => model.name === response[responseKey],
283280
).pop();
284281
if (responseModel) {
@@ -295,7 +292,7 @@ export class DocumentGenerator {
295292
merge(content, { [responseKey] : resModelConfig });
296293
}
297294
}
298-
// console.log(content);
295+
299296
return content;
300297
}
301298

src/ServerlessOpenApiDocumentation.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as c from 'chalk';
22
import * as fs from 'fs';
33
import * as YAML from 'js-yaml';
4-
import { DocumentGenerator } from './DocumentGenerator';
5-
import { IConfigType } from './types';
4+
import { inspect } from 'util';
5+
import { DefinitionGenerator } from './DefinitionGenerator';
6+
import { IDefinitionType, ILog } from './types';
67
import { merge } from './utils';
78

89
export class ServerlessOpenApiDocumentation {
@@ -62,12 +63,16 @@ export class ServerlessOpenApiDocumentation {
6263
};
6364
}
6465

66+
log: ILog = (...str: string[]) => {
67+
process.stdout.write(str.join(' '));
68+
}
69+
6570
/**
6671
* Processes CLI input by reading the input from serverless
6772
* @returns config IConfigType
6873
*/
69-
private processCliInput (): IConfigType {
70-
const config: IConfigType = {
74+
private processCliInput (): IDefinitionType {
75+
const config: IDefinitionType = {
7176
format: 'yaml',
7277
file: 'openapi.yml',
7378
indent: 2,
@@ -83,22 +88,25 @@ export class ServerlessOpenApiDocumentation {
8388
config.file = this.serverless.processedInput.options.output ||
8489
((config.format === 'yaml') ? 'openapi.yml' : 'openapi.json');
8590

86-
process.stdout.write(
87-
`${c.bold.green('[OPTIONS]')} ` +
88-
`format: "${c.bold.red(config.format)}", ` +
89-
`output file: "${c.bold.red(config.file)}", ` +
91+
this.log(
92+
`${c.bold.green('[OPTIONS]')}`,
93+
`format: "${c.bold.red(config.format)}",`,
94+
`output file: "${c.bold.red(config.file)}",`,
9095
`indentation: "${c.bold.red(String(config.indent))}"\n\n`,
91-
);
96+
);
97+
9298
return config;
9399
}
94100

95101
/**
96102
* Generates OpenAPI Documentation based on serverless configuration and functions
97103
*/
98104
private generate () {
99-
process.stdout.write(c.bold.underline('OpenAPI v3 Documentation Generator\n\n'));
105+
this.log(c.bold.underline('OpenAPI v3 Documentation Generator\n\n'));
100106
// Instantiate DocumentGenerator
101-
const dg = new DocumentGenerator(this.customVars.documentation);
107+
const generator = new DefinitionGenerator(this.customVars.documentation);
108+
109+
generator.parse();
102110

103111
// Map function configurations
104112
const funcConfigs = this.serverless.service.getAllFunctions().map((functionName) => {
@@ -107,28 +115,46 @@ export class ServerlessOpenApiDocumentation {
107115
});
108116

109117
// Add Paths to OpenAPI Output from Function Configuration
110-
dg.addPathsFromFunctionConfig(funcConfigs);
118+
generator.readFunctions(funcConfigs);
111119

112120
// Process CLI Input options
113121
const config = this.processCliInput();
114122

115-
// Generate the resulting OpenAPI Object
116-
const outputObject = dg.generate();
123+
this.log(`${ c.bold.yellow('[VALIDATION]') } Validating OpenAPI generated output\n`);
124+
125+
const validation = generator.validate();
126+
127+
if (validation.valid) {
128+
this.log(`${ c.bold.green('[VALIDATION]') } OpenAPI valid: ${c.bold.green('true')}\n\n`);
129+
} else {
130+
this.log(`${c.bold.red('[VALIDATION]')} Failed to validate OpenAPI document: \n\n`);
131+
this.log(`${c.bold.green('Path:')} ${JSON.stringify(validation.context, null, 2)}\n`);
132+
133+
for (const info of validation.error) {
134+
this.log(c.grey('\n\n--------\n\n'));
135+
this.log(' ', info.schemaPath, c.bold.yellow(info.message));
136+
this.log(c.grey('\n\n--------\n\n'));
137+
this.log(`${inspect(info, { colors: true, depth: 2 })}\n\n`);
138+
}
139+
}
140+
141+
const { definition } = generator;
117142

118143
// Output the OpenAPI document to the correct format
119-
let outputContent = '';
144+
145+
let output;
120146
switch (config.format.toLowerCase()) {
121147
case 'json':
122-
outputContent = JSON.stringify(outputObject, null, config.indent);
148+
output = JSON.stringify(definition, null, config.indent);
123149
break;
124150
case 'yaml':
125151
default:
126-
outputContent = YAML.safeDump(outputObject, { indent: config.indent });
152+
output = YAML.safeDump(definition, { indent: config.indent });
127153
break;
128154
}
129155

130-
// Write to disk
131-
fs.writeFileSync(config.file, outputContent);
132-
process.stdout.write(`${ c.bold.green('[SUCCESS]') } Output file to "${c.bold.red(config.file)}"\n`);
156+
fs.writeFileSync(config.file, output);
157+
158+
this.log(`${ c.bold.green('[OUTPUT]') } To "${c.bold.red(config.file)}"\n`);
133159
}
134160
}

src/__tests__/DocumentGenerator.spec.ts renamed to src/__tests__/DefinitionGenerator.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path';
22
import * as Serverless from 'serverless';
3-
import { DocumentGenerator } from '../DocumentGenerator';
3+
import { DefinitionGenerator } from '../DefinitionGenerator';
44

55
class ServerlessInterface extends Serverless {
66
public service: any = {};
@@ -12,7 +12,7 @@ class ServerlessInterface extends Serverless {
1212

1313
describe('OpenAPI Documentation Generator', () => {
1414
it('Generates OpenAPI document', async () => {
15-
const servicePath = path.join(__dirname, './fixtures');
15+
const servicePath = path.join(__dirname, '../../test/project');
1616
const serverlessYamlPath = path.join(servicePath, './serverless.yml');
1717
const sls: ServerlessInterface = new Serverless();
1818

@@ -27,7 +27,7 @@ describe('OpenAPI Documentation Generator', () => {
2727
await sls.variables.populateService();
2828

2929
if ('documentation' in sls.service.custom) {
30-
const docGen = new DocumentGenerator(sls.service.custom.documentation);
30+
const docGen = new DefinitionGenerator(sls.service.custom.documentation);
3131

3232
expect(docGen).not.toBeNull();
3333
} else {

src/__tests__/fixtures/package.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)