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

Add file ref support #7

Merged
merged 11 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,261 changes: 5,432 additions & 2,829 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@conqa/serverless-openapi-documentation",
"version": "1.0.4",
"version": "1.1.0-alpha",
"description": "Serverless 1.0 plugin to generate OpenAPI V3 documentation from serverless configuration",
"main": "index.js",
"engines": {
Expand Down Expand Up @@ -42,14 +42,17 @@
"@types/chalk": "^0.4.31",
"@types/fs-extra": "^4.0.0",
"@types/jest": "^20.0.2",
"@types/js-yaml": "^3.5.31",
"@types/js-yaml": "^3.12.1",
"@types/json-schema": "^7.0.3",
"@types/node": "^8.0.7",
"@types/uuid": "^3.0.0",
"@types/lodash": "^4.14.123",
"@types/node": "^8.10.48",
"@types/serverless": "^1.18.2",
"@types/uuid": "^3.4.4",
"changelog-verify": "^1.0.4",
"jest": "^24.8.0",
"serverless": "^1.16.1",
"serverless": "^1.41.1",
"ts-jest": "^24.0.2",
"openapi-types": "^1.3.4",
"ts-node": "^3.1.0",
"tslint": "^5.4.3",
"tslint-config-temando": "^1.1.4",
Expand All @@ -61,7 +64,8 @@
"chalk": "^2.0.1",
"fs-extra": "^4.0.1",
"js-yaml": "^3.8.4",
"lutils": "^2.4.0",
"json-schema-ref-parser": "^6.1.0",
"lodash": "^4.17.11",
"swagger2openapi": "^2.5.0",
"uuid": "^3.1.0"
}
Expand Down
91 changes: 19 additions & 72 deletions src/DefinitionGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { JSONSchema7 } from 'json-schema';
import _ = require('lodash');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this syntax looks weird. how does it work? I've never seen it before

also, we normally use ramda 🙈 But I can see how much it makes sense to just replace the existing utils with lodash here 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an open source project, I would prefer to keep it easy for people to contribute - lodash is more aligned with this idea than ramda

// tslint:disable-next-line no-submodule-imports
import { validateSync as openApiValidatorSync } from 'swagger2openapi/validate';
import * as uuid from 'uuid';

import { parseModels } from './parse';
import { IDefinition, IDefinitionConfig, IOperation, IParameterConfig, IServerlessFunctionConfig } from './types';
import { clone, isIterable, merge, omit } from './utils';
import { cleanSchema } from './utils';

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

public config: IDefinitionConfig;

private root: string;

/**
* Constructor
* @param serviceDescriptor IServiceDescription
*/
constructor (config: IDefinitionConfig) {
this.config = clone(config);
constructor (config: IDefinitionConfig, root: string) {
this.config = _.cloneDeep(config);
this.root = root;
}

public parse () {
public async parse () {
const {
title = '',
description = '',
version = uuid.v4(),
models,
} = this.config;

merge(this.definition, {
_.merge(this.definition, {
openapi: this.version,
info: { title, description, version },
paths: {},
Expand All @@ -44,24 +47,7 @@ export class DefinitionGenerator {
},
});

if (isIterable(models)) {
for (const model of models) {
if (!model.schema) {
continue;
}

for (const definitionName of Object.keys(model.schema.definitions || {})) {
const definition = model.schema.definitions[definitionName];
if (typeof definition !== 'boolean') {
this.definition.components.schemas[definitionName] = this.cleanSchema(this.updateReferences(definition));
}
}

const schemaWithoutDefinitions = omit(model.schema, ['definitions']);

this.definition.components.schemas[model.name] = this.cleanSchema(this.updateReferences(schemaWithoutDefinitions));
}
}
this.definition.components.schemas = await parseModels(models, this.root);

return this;
}
Expand Down Expand Up @@ -101,49 +87,10 @@ export class DefinitionGenerator {
};

// merge path configuration into main configuration
merge(this.definition.paths, pathConfig);
}
}
}
}

/**
* Cleans schema objects to make them OpenAPI compatible
* @param schema JSON Schema Object
*/
private cleanSchema (schema) {
// Clone the schema for manipulation
const cleanedSchema = clone(schema);

// Strip $schema from schemas
if (cleanedSchema.$schema) {
delete cleanedSchema.$schema;
}

// Return the cleaned schema
return cleanedSchema;
}

/**
* Walks through the schema object recursively and updates references to point to openapi's components
* @param schema JSON Schema Object
*/
private updateReferences (schema: JSONSchema7): JSONSchema7 {
const cloned = clone(schema);

if (cloned.$ref) {
cloned.$ref = cloned.$ref.replace('#/definitions', '#/components/schemas');
} else {
for (const key of Object.getOwnPropertyNames(cloned)) {
const value = cloned[key];

if (typeof value === 'object') {
cloned[key] = this.updateReferences(value);
_.merge(this.definition.paths, pathConfig);
}
}
}

return cloned;
}

/**
Expand Down Expand Up @@ -240,7 +187,7 @@ export class DefinitionGenerator {
}

if (parameter.schema) {
parameterConfig.schema = this.cleanSchema(parameter.schema);
parameterConfig.schema = cleanSchema(parameter.schema);
}

if (parameter.example) {
Expand Down Expand Up @@ -299,7 +246,7 @@ export class DefinitionGenerator {
reqBodyConfig.description = documentationConfig.requestBody.description;
}

merge(requestBodies, reqBodyConfig);
_.merge(requestBodies, reqBodyConfig);
}
}
}
Expand All @@ -309,9 +256,9 @@ export class DefinitionGenerator {

private attachExamples (target, config) {
if (target.examples && Array.isArray(target.examples)) {
merge(config, { examples: clone(target.examples) });
_.merge(config, { examples: _.cloneDeep(target.examples) });
} else if (target.example) {
merge(config, { example: clone(target.example) });
_.merge(config, { example: _.cloneDeep(target.example) });
}
}

Expand Down Expand Up @@ -339,12 +286,12 @@ export class DefinitionGenerator {
description: header.description || `${header.name} header`,
};
if (header.schema) {
methodResponseConfig.headers[header.name].schema = this.cleanSchema(header.schema);
methodResponseConfig.headers[header.name].schema = cleanSchema(header.schema);
}
}
}

merge(responses, {
_.merge(responses, {
[response.statusCode]: methodResponseConfig,
});
}
Expand All @@ -370,7 +317,7 @@ export class DefinitionGenerator {

this.attachExamples(responseModel, resModelConfig);

merge(content, { [responseKey] : resModelConfig });
_.merge(content, { [responseKey] : resModelConfig });
}
}

Expand Down
115 changes: 71 additions & 44 deletions src/ServerlessOpenApiDocumentation.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
import chalk from 'chalk';
import * as fs from 'fs';
import * as YAML from 'js-yaml';
import _ = require('lodash');
import * as Serverless from 'serverless';
import { inspect } from 'util';

import { DefinitionGenerator } from './DefinitionGenerator';
import { IDefinitionType, ILog } from './types';
import { merge } from './utils';
import { Format, IDefinitionConfig, IDefinitionType, ILog } from './types';

interface IOptions {
indent: number;
format: Format;
output: string;
}

interface IProcessedInput {
options: IOptions;
}

interface ICustomVars {
documentation: IDefinitionConfig;
}

interface IService {
custom: ICustomVars;
}

interface IVariables {
service: IService;
}

interface IFullServerless extends Serverless {
variables: IVariables;
processedInput: IProcessedInput;
}

export class ServerlessOpenApiDocumentation {
public hooks;
public commands;
/** Serverless Instance */
private serverless;
/** CLI options */
// private options;
private serverless: IFullServerless;

/** Serverless Service Custom vars */
private customVars;
private customVars: ICustomVars;

/**
* Constructor
* @param serverless
* @param options
*/
constructor (serverless, options) {
constructor (serverless: IFullServerless, options) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come options doesn't need a type?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project is not configured(yet?) to use strict typescript transpilation.


// pull the serverless instance into our class vars
this.serverless = serverless;
// pull the CLI options into our class vars
// this.options = options;
// Serverless service custom variables
this.customVars = this.serverless.variables.service.custom;

Expand Down Expand Up @@ -67,51 +94,20 @@ export class ServerlessOpenApiDocumentation {
process.stdout.write(str.join(' '));
}

/**
* Processes CLI input by reading the input from serverless
* @returns config IConfigType
*/
private processCliInput (): IDefinitionType {
const config: IDefinitionType = {
format: 'yaml',
file: 'openapi.yml',
indent: 2,
};

config.indent = this.serverless.processedInput.options.indent || 2;
config.format = this.serverless.processedInput.options.format || 'yaml';

if (['yaml', 'json'].indexOf(config.format.toLowerCase()) < 0) {
throw new Error('Invalid Output Format Specified - must be one of "yaml" or "json"');
}

config.file = this.serverless.processedInput.options.output ||
((config.format === 'yaml') ? 'openapi.yml' : 'openapi.json');

this.log(
`${chalk.bold.green('[OPTIONS]')}`,
`format: "${chalk.bold.red(config.format)}",`,
`output file: "${chalk.bold.red(config.file)}",`,
`indentation: "${chalk.bold.red(String(config.indent))}"\n\n`,
);

return config;
}

/**
* Generates OpenAPI Documentation based on serverless configuration and functions
*/
private generate () {
public async generate () {
this.log(chalk.bold.underline('OpenAPI v3 Documentation Generator\n\n'));
// Instantiate DocumentGenerator
const generator = new DefinitionGenerator(this.customVars.documentation);
const generator = new DefinitionGenerator(this.customVars.documentation, this.serverless.config.servicePath);

generator.parse();
await generator.parse();

// Map function configurations
const funcConfigs = this.serverless.service.getAllFunctions().map((functionName) => {
const func = this.serverless.service.getFunction(functionName);
return merge({ _functionName: functionName }, func);
return _.merge({ _functionName: functionName }, func);
});

// Add Paths to OpenAPI Output from Function Configuration
Expand Down Expand Up @@ -162,4 +158,35 @@ export class ServerlessOpenApiDocumentation {

this.log(`${chalk.bold.green('[OUTPUT]')} To "${chalk.bold.red(config.file)}"\n`);
}

/**
* Processes CLI input by reading the input from serverless
* @returns config IConfigType
*/
private processCliInput (): IDefinitionType {
const config: IDefinitionType = {
format: Format.yaml,
file: 'openapi.yml',
indent: 2,
};

config.indent = this.serverless.processedInput.options.indent || 2;
config.format = this.serverless.processedInput.options.format || Format.yaml;

if ([Format.yaml, Format.json].indexOf(config.format) < 0) {
throw new Error('Invalid Output Format Specified - must be one of "yaml" or "json"');
}

config.file = this.serverless.processedInput.options.output ||
((config.format === 'yaml') ? 'openapi.yml' : 'openapi.json');

this.log(
`${chalk.bold.green('[OPTIONS]')}`,
`format: "${chalk.bold.red(config.format)}",`,
`output file: "${chalk.bold.red(config.file)}",`,
`indentation: "${chalk.bold.red(String(config.indent))}"\n\n`,
);

return config;
}
}
Loading