Skip to content
This repository was archived by the owner on Dec 10, 2021. It is now read-only.

Commit efb0b85

Browse files
committed
Support for referencing external json schemas
1 parent cc41213 commit efb0b85

File tree

9 files changed

+287
-197
lines changed

9 files changed

+287
-197
lines changed

src/DefinitionGenerator.ts

Lines changed: 19 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { JSONSchema7 } from 'json-schema';
21
// tslint:disable-next-line no-submodule-imports
32
import { validateSync as openApiValidatorSync } from 'swagger2openapi/validate';
43
import * as uuid from 'uuid';
54

65
import { IDefinition, IDefinitionConfig, IOperation, IParameterConfig, IServerlessFunctionConfig } from './types';
7-
import { clone, isIterable, merge, omit } from './utils';
6+
import { cleanSchema } from './utils';
7+
import { parseModels } from './SchemaBuilder';
8+
import _ = require('lodash');
89

910
export class DefinitionGenerator {
1011
// The OpenAPI version we currently validate against
@@ -18,23 +19,25 @@ export class DefinitionGenerator {
1819

1920
public config: IDefinitionConfig;
2021

22+
private root: string;
23+
2124
/**
2225
* Constructor
23-
* @param serviceDescriptor IServiceDescription
2426
*/
25-
constructor (config: IDefinitionConfig) {
26-
this.config = clone(config);
27+
constructor (config: IDefinitionConfig, root: string) {
28+
this.config = _.cloneDeep(config);
29+
this.root = root;
2730
}
2831

29-
public parse () {
32+
public async parse () {
3033
const {
3134
title = '',
3235
description = '',
3336
version = uuid.v4(),
3437
models,
3538
} = this.config;
3639

37-
merge(this.definition, {
40+
_.merge(this.definition, {
3841
openapi: this.version,
3942
info: { title, description, version },
4043
paths: {},
@@ -44,24 +47,7 @@ export class DefinitionGenerator {
4447
},
4548
});
4649

47-
if (isIterable(models)) {
48-
for (const model of models) {
49-
if (!model.schema) {
50-
continue;
51-
}
52-
53-
for (const definitionName of Object.keys(model.schema.definitions || {})) {
54-
const definition = model.schema.definitions[definitionName];
55-
if (typeof definition !== 'boolean') {
56-
this.definition.components.schemas[definitionName] = this.cleanSchema(this.updateReferences(definition));
57-
}
58-
}
59-
60-
const schemaWithoutDefinitions = omit(model.schema, ['definitions']);
61-
62-
this.definition.components.schemas[model.name] = this.cleanSchema(this.updateReferences(schemaWithoutDefinitions));
63-
}
64-
}
50+
this.definition.components.schemas = await parseModels(models, this.root);
6551

6652
return this;
6753
}
@@ -101,49 +87,10 @@ export class DefinitionGenerator {
10187
};
10288

10389
// merge path configuration into main configuration
104-
merge(this.definition.paths, pathConfig);
105-
}
106-
}
107-
}
108-
}
109-
110-
/**
111-
* Cleans schema objects to make them OpenAPI compatible
112-
* @param schema JSON Schema Object
113-
*/
114-
private cleanSchema (schema) {
115-
// Clone the schema for manipulation
116-
const cleanedSchema = clone(schema);
117-
118-
// Strip $schema from schemas
119-
if (cleanedSchema.$schema) {
120-
delete cleanedSchema.$schema;
121-
}
122-
123-
// Return the cleaned schema
124-
return cleanedSchema;
125-
}
126-
127-
/**
128-
* Walks through the schema object recursively and updates references to point to openapi's components
129-
* @param schema JSON Schema Object
130-
*/
131-
private updateReferences (schema: JSONSchema7): JSONSchema7 {
132-
const cloned = clone(schema);
133-
134-
if (cloned.$ref) {
135-
cloned.$ref = cloned.$ref.replace('#/definitions', '#/components/schemas');
136-
} else {
137-
for (const key of Object.getOwnPropertyNames(cloned)) {
138-
const value = cloned[key];
139-
140-
if (typeof value === 'object') {
141-
cloned[key] = this.updateReferences(value);
90+
_.merge(this.definition.paths, pathConfig);
14291
}
14392
}
14493
}
145-
146-
return cloned;
14794
}
14895

14996
/**
@@ -240,7 +187,7 @@ export class DefinitionGenerator {
240187
}
241188

242189
if (parameter.schema) {
243-
parameterConfig.schema = this.cleanSchema(parameter.schema);
190+
parameterConfig.schema = cleanSchema(parameter.schema);
244191
}
245192

246193
if (parameter.example) {
@@ -299,7 +246,7 @@ export class DefinitionGenerator {
299246
reqBodyConfig.description = documentationConfig.requestBody.description;
300247
}
301248

302-
merge(requestBodies, reqBodyConfig);
249+
_.merge(requestBodies, reqBodyConfig);
303250
}
304251
}
305252
}
@@ -309,9 +256,9 @@ export class DefinitionGenerator {
309256

310257
private attachExamples (target, config) {
311258
if (target.examples && Array.isArray(target.examples)) {
312-
merge(config, { examples: clone(target.examples) });
259+
_.merge(config, { examples: _.cloneDeep(target.examples) });
313260
} else if (target.example) {
314-
merge(config, { example: clone(target.example) });
261+
_.merge(config, { example: _.cloneDeep(target.example) });
315262
}
316263
}
317264

@@ -339,12 +286,12 @@ export class DefinitionGenerator {
339286
description: header.description || `${header.name} header`,
340287
};
341288
if (header.schema) {
342-
methodResponseConfig.headers[header.name].schema = this.cleanSchema(header.schema);
289+
methodResponseConfig.headers[header.name].schema = cleanSchema(header.schema);
343290
}
344291
}
345292
}
346293

347-
merge(responses, {
294+
_.merge(responses, {
348295
[response.statusCode]: methodResponseConfig,
349296
});
350297
}
@@ -370,7 +317,7 @@ export class DefinitionGenerator {
370317

371318
this.attachExamples(responseModel, resModelConfig);
372319

373-
merge(content, { [responseKey] : resModelConfig });
320+
_.merge(content, { [responseKey] : resModelConfig });
374321
}
375322
}
376323

src/ServerlessOpenApiDocumentation.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import chalk from 'chalk';
1+
import * as chalk from 'chalk';
22
import * as fs from 'fs';
33
import * as YAML from 'js-yaml';
44
import { inspect } from 'util';
55
import { DefinitionGenerator } from './DefinitionGenerator';
66
import { IDefinitionType, ILog, Format, IDefinitionConfig } from './types';
7-
import { merge } from './utils';
87
import * as Serverless from 'serverless';
8+
import _ = require('lodash');
99

1010
interface Options {
1111
indent: number,
@@ -127,17 +127,17 @@ export class ServerlessOpenApiDocumentation {
127127
/**
128128
* Generates OpenAPI Documentation based on serverless configuration and functions
129129
*/
130-
private generate () {
130+
public async generate () {
131131
this.log(chalk.bold.underline('OpenAPI v3 Documentation Generator\n\n'));
132132
// Instantiate DocumentGenerator
133-
const generator = new DefinitionGenerator(this.customVars.documentation);
133+
const generator = new DefinitionGenerator(this.customVars.documentation, this.serverless.config.servicePath);
134134

135-
generator.parse();
135+
await generator.parse();
136136

137137
// Map function configurations
138138
const funcConfigs = this.serverless.service.getAllFunctions().map((functionName) => {
139139
const func = this.serverless.service.getFunction(functionName);
140-
return merge({ _functionName: functionName }, func);
140+
return _.merge({ _functionName: functionName }, func);
141141
});
142142

143143
// Add Paths to OpenAPI Output from Function Configuration

src/parse.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as path from 'path';
2+
import * as _ from 'lodash';
3+
import { JSONSchema7 } from 'json-schema';
4+
5+
import * as $RefParser from 'json-schema-ref-parser';
6+
7+
import { IModel } from './types';
8+
import { cleanSchema } from './utils';
9+
10+
export async function parseModels(models: IModel[], root: string): Promise<{}> {
11+
const schemas = {};
12+
13+
if (!_.isArrayLike(models)) {
14+
throw new Error('Empty models');
15+
}
16+
17+
for (const model of models) {
18+
if (!model.schema) {
19+
continue;
20+
}
21+
22+
if (typeof model.schema === 'string') {
23+
const fullPath = path.resolve(root, model.schema);
24+
25+
const schema = await $RefParser.bundle(fullPath) as JSONSchema7;
26+
27+
_.assign(schemas, updateReferences(schema.definitions));
28+
29+
schemas[model.name] = updateReferences(cleanSchema(schema));
30+
}
31+
32+
if(typeof model.schema === 'object') {
33+
schemas[model.name] = updateReferences(cleanSchema(model.schema));
34+
}
35+
}
36+
37+
return schemas;
38+
}
39+
40+
function updateReferences (schema: JSONSchema7): JSONSchema7 {
41+
if(!schema) {
42+
return schema;
43+
}
44+
45+
const cloned = _.cloneDeep(schema) as JSONSchema7;
46+
47+
if (cloned.$ref) {
48+
return {
49+
...cloned,
50+
$ref: cloned.$ref.replace('#/definitions', '#/components/schemas')
51+
};
52+
}
53+
54+
for (const key of Object.getOwnPropertyNames(cloned)) {
55+
const value = cloned[key];
56+
57+
if (typeof value === 'object') {
58+
cloned[key] = updateReferences(value);
59+
}
60+
}
61+
62+
return cloned;
63+
}

src/types.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { JSONSchema7 } from 'json-schema';
2-
3-
export interface IModels {
1+
export interface IModel {
42
name: string;
53
description: string;
64
contentType: string;
7-
schema: JSONSchema7;
5+
schema: string;
86
examples: any[];
97
example: object;
108
}
@@ -13,7 +11,7 @@ export interface IDefinitionConfig {
1311
title: string;
1412
description: string;
1513
version?: string;
16-
models: IModels[];
14+
models: IModel[];
1715
}
1816

1917
export enum Format {

src/utils.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,4 @@
1-
import { Clone, IMerge, Merge } from 'lutils';
21

3-
export const merge: IMerge = new Merge({ depth: 100 }).merge;
4-
export const clone = new Clone({ depth: 100 }).clone;
2+
import _ = require('lodash');
53

6-
export function isIterable (obj) {
7-
if (obj === null || obj === undefined) {
8-
return false;
9-
}
10-
11-
return typeof obj[Symbol.iterator] === 'function';
12-
}
13-
14-
export function omit<T extends object> (obj: T, keys: string[]): T {
15-
const cloned = clone(obj);
16-
17-
for (const key of keys) {
18-
delete cloned[key];
19-
}
20-
21-
return cloned;
22-
}
4+
export const cleanSchema = schema => _.omit(schema, '$schema', 'definitions')

0 commit comments

Comments
 (0)