Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: theodo/serverless-openapi
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: includable/serverless-openapi
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 15 commits
  • 40 files changed
  • 1 contributor

Commits on Dec 18, 2022

  1. Copy the full SHA
    8044c4b View commit details

Commits on Mar 16, 2024

  1. Copy the full SHA
    fa1332b View commit details
  2. 1.3.0

    tschoffelen committed Mar 16, 2024
    Copy the full SHA
    026fd22 View commit details
  3. chore: fix definition

    tschoffelen committed Mar 16, 2024
    Copy the full SHA
    d390926 View commit details
  4. 1.3.1

    tschoffelen committed Mar 16, 2024
    Copy the full SHA
    309a37f View commit details
  5. chore: fix schema

    tschoffelen committed Mar 16, 2024
    Copy the full SHA
    87ab966 View commit details
  6. 1.3.2

    tschoffelen committed Mar 16, 2024
    Copy the full SHA
    5a74a27 View commit details

Commits on Apr 14, 2024

  1. Copy the full SHA
    fe83b28 View commit details
  2. 1.3.3

    tschoffelen committed Apr 14, 2024
    Copy the full SHA
    a7e056c View commit details
  3. Copy the full SHA
    cea0a3d View commit details
  4. 1.3.4

    tschoffelen committed Apr 14, 2024
    Copy the full SHA
    c644c34 View commit details

Commits on Apr 30, 2024

  1. feat: support security

    tschoffelen committed Apr 30, 2024
    Copy the full SHA
    97f74bc View commit details
  2. 1.3.5

    tschoffelen committed Apr 30, 2024
    Copy the full SHA
    68d3b3d View commit details
  3. Copy the full SHA
    b6ea747 View commit details
  4. 1.3.6

    tschoffelen committed Apr 30, 2024
    Copy the full SHA
    f40b4d8 View commit details
8 changes: 0 additions & 8 deletions .editorconfig

This file was deleted.

3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

21 changes: 0 additions & 21 deletions .eslintrc.yml

This file was deleted.

1 change: 0 additions & 1 deletion .node-version

This file was deleted.

1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -4,3 +4,4 @@ tslint.json
tsconfig.json
jest.config.js
yarn.lock
src/**
1 change: 0 additions & 1 deletion .npmrc

This file was deleted.

1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
20 changes: 0 additions & 20 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -3,6 +3,6 @@
"editor.formatOnSave": true,
"prettier.eslintIntegration": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}
18 changes: 14 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
{
"version": "0.1.0",
"version": "2.0.0",
"command": "tsc",
"isShellCommand": true,
"args": [],
"showOutput": "silent",
"problemMatcher": "$tsc"
"problemMatcher": "$tsc",
"tasks": [
{
"label": "tsc",
"type": "shell",
"command": "tsc",
"problemMatcher": "$tsc",
"group": {
"_id": "build",
"isDefault": false
}
}
]
}
47 changes: 0 additions & 47 deletions CHANGELOG.md

This file was deleted.

21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

350 changes: 0 additions & 350 deletions README.md

This file was deleted.

16 changes: 0 additions & 16 deletions jest.config.js

This file was deleted.

9,951 changes: 0 additions & 9,951 deletions package-lock.json

This file was deleted.

55 changes: 8 additions & 47 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,46 +1,15 @@
{
"name": "serverless-openapi",
"version": "1.1.2",
"name": "@includable/serverless-openapi",
"version": "1.3.6",
"description": "Serverless plugin to generate OpenAPI V3 documentation from serverless configuration",
"main": "index.js",
"engines": {
"node": ">=6.0.0",
"npm": ">=3.8.6"
},
"repository": {
"type": "git",
"url": "https://github.com/theodo/serverless-openapi.git"
},
"bugs": "https://github.com/theodo/serverless-openapi/issues",
"author": "Abilio Henrique <abilio.henrique@temando.com>",
"contributors": [
"Abilio Henrique <abilio.henrique@temando.com>",
"nfour <novus.nfour@gmail.com>",
"Alexandre Pernin <alex.e.pernin@gmail.com>"
],
"keywords": [
"OpenAPI",
"swagger",
"serverless"
],
"main": "build/index.js",
"author": "Includable <info@includable.com>",
"license": "MIT",
"scripts": {
"test": "jest -c ./jest.config.js",
"test:build": "jest -c '{ \"testRegex\": \".spec.js$\"}' build",
"test:coverage": "jest -c ./jest.config.js --coverage",
"lint": "eslint --ext .ts,.tsx .",
"lint:fix": "npm run lint -- --fix",
"preversion": "npm run lint && npm run build && npm run test:build && changelog-verify CHANGELOG.md",
"version": "version-changelog CHANGELOG.md && changelog-verify CHANGELOG.md && git add CHANGELOG.md",
"release": "cd build && npm publish",
"test:project": "cd test/project && ./node_modules/.bin/sls openapi generate",
"test:prepare": "scripts/prepareTests.bash",
"build:link": "npm run build && cd build && npm link",
"build:watch": "npm run build && tsc --watch",
"build": "scripts/build.bash"
"build": "tsc",
"prepare": "npm run build"
},
"devDependencies": {
"@types/bluebird": "^3.5.8",
"@types/chalk": "^0.4.31",
"@types/fs-extra": "^4.0.0",
"@types/jest": "^20.0.2",
@@ -52,21 +21,13 @@
"@types/uuid": "^3.4.4",
"@typescript-eslint/eslint-plugin": "^1.7.0",
"@typescript-eslint/parser": "^1.7.0",
"changelog-verify": "^1.0.4",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.2.0",
"eslint-plugin-prettier": "^3.0.1",
"jest": "^26.6.3",
"openapi-types": "^1.3.4",
"prettier": "^1.17.0",
"prettier": "^3.2.5",
"serverless": "^1.41.1",
"ts-jest": "^24.0.2",
"ts-node": "^3.1.0",
"typescript": "^3.4.5",
"version-changelog": "^3.1.1"
"typescript": "^3.4.5"
},
"dependencies": {
"bluebird": "^3.5.0",
"chalk": "^2.0.1",
"fs-extra": "^4.0.1",
"js-yaml": "^3.8.4",
14 changes: 0 additions & 14 deletions scripts/build.bash

This file was deleted.

7 changes: 0 additions & 7 deletions scripts/prepareTests.bash

This file was deleted.

132 changes: 79 additions & 53 deletions src/DefinitionGenerator.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import {
DefinitionConfig,
Operation,
ParameterConfig,
ServerlessFunctionConfig
ServerlessFunctionConfig,
} from "./types";
import { cleanSchema } from "./utils";

@@ -20,7 +20,7 @@ export class DefinitionGenerator {
// Base configuration object
public definition: Definition = {
openapi: this.version,
components: {}
components: {},
};

public config: DefinitionConfig;
@@ -43,16 +43,16 @@ export class DefinitionGenerator {
models,
security,
securitySchemes,
servers
servers,
} = this.config;

_.merge(this.definition, {
openapi: this.version,
info: { title, description, version },
paths: {},
components: {
schemas: {}
}
schemas: {},
},
});

if (security) {
@@ -106,11 +106,12 @@ export class DefinitionGenerator {
// Build OpenAPI path configuration structure for each method
const pathConfig = {
[`/${normalizedPath}`]: {
[httpEventConfig.method.toLowerCase()]: this.getOperationFromConfig(
funcConfig._functionName,
httpEventConfig.documentation
)
}
[httpEventConfig.method.toLowerCase()]:
this.getOperationFromConfig(
funcConfig._functionName,
httpEventConfig.documentation,
),
},
};

// merge path configuration into main configuration
@@ -129,10 +130,10 @@ export class DefinitionGenerator {
*/
private getOperationFromConfig(
funcName: string,
documentationConfig
documentationConfig,
): Operation {
const operationObj: Operation = {
operationId: funcName
operationId: funcName,
};

if (documentationConfig.summary) {
@@ -147,13 +148,26 @@ export class DefinitionGenerator {
operationObj.tags = documentationConfig.tags;
}

if (documentationConfig.security) {
operationObj.security = documentationConfig.security;
}

if (documentationConfig.deprecated) {
operationObj.deprecated = true;
}

const camelCaseFunctionName = funcName
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/[\s_-]+/g, "");

if (documentationConfig.requestBody) {
operationObj.requestBody = documentationConfig.requestBody;
} else if (documentationConfig.request) {
operationObj.requestBody = this.getRequestBodiesFromConfig(
documentationConfig
documentationConfig,
camelCaseFunctionName,
);
}

@@ -192,7 +206,7 @@ export class DefinitionGenerator {
name: parameter.name,
in: type,
description: parameter.description || "",
required: parameter.required || false // Note: all path parameters must be required
required: parameter.required || false, // Note: all path parameters must be required
};

// if type is path, then required must be true (@see OpenAPI 3.0-RC1)
@@ -242,49 +256,61 @@ export class DefinitionGenerator {
/**
* Derives request body schemas from event documentation configuration
* @param documentationConfig
* @param camelCaseFunctionName
*/
private getRequestBodiesFromConfig(documentationConfig) {
private getRequestBodiesFromConfig(
documentationConfig,
camelCaseFunctionName,
) {
const requestBodies = {};

if (
documentationConfig.request?.schema &&
!documentationConfig.requestModels
) {
const requestSchemaName = `${camelCaseFunctionName}Request`;
documentationConfig.requestModels = {
"application/json": {
name: requestSchemaName,
schema: documentationConfig.request.schema,
},
};
}

if (!documentationConfig.requestModels) {
throw new Error(
`Required requestModels in: ${JSON.stringify(
documentationConfig,
null,
2
)}`
);
return requestBodies;
}

// Does this event have a request model?
if (documentationConfig.requestModels) {
// For each request model type (Sorted by "Content-Type")
for (const requestModelType of Object.keys(
documentationConfig.requestModels
documentationConfig.requestModels,
)) {
// get schema reference information
const requestModel = this.config.models
.filter(
model =>
model.name === documentationConfig.requestModels[requestModelType]
)
.pop();

if (requestModel) {
const reqModelConfig = {
schema: {
$ref: `#/components/schemas/${
documentationConfig.requestModels[requestModelType]
}`
}
};

this.attachExamples(requestModel, reqModelConfig);

const value = documentationConfig.requestModels[requestModelType];
let reqModelConfig;
if (typeof value === "string") {
// get schema reference information
const requestModel = this.config.models
.filter((model) => model.name === value)
.pop();

if (requestModel) {
reqModelConfig = {
schema: {
$ref: `#/components/schemas/${value}`,
},
};
this.attachExamples(requestModel, reqModelConfig);
}
} else if (value.schema) {
reqModelConfig = value;
}
if (reqModelConfig) {
const reqBodyConfig: { content: object; description?: string } = {
content: {
[requestModelType]: reqModelConfig
}
[requestModelType]: reqModelConfig,
},
};

if (
@@ -328,25 +354,25 @@ export class DefinitionGenerator {
response.responseBody && "description" in response.responseBody
? response.responseBody.description
: `Status ${response.statusCode} Response`,
content: this.getResponseContent(response.responseModels)
content: this.getResponseContent(response.responseModels),
};

if (response.responseHeaders) {
methodResponseConfig.headers = {};
for (const header of response.responseHeaders) {
methodResponseConfig.headers[header.name] = {
description: header.description || `${header.name} header`
description: header.description || `${header.name} header`,
};
if (header.schema) {
methodResponseConfig.headers[header.name].schema = cleanSchema(
header.schema
header.schema,
);
}
}
}

_.merge(responses, {
[response.statusCode]: methodResponseConfig
[response.statusCode]: methodResponseConfig,
});
}
}
@@ -359,14 +385,14 @@ export class DefinitionGenerator {

for (const responseKey of Object.keys(response)) {
const responseModel = this.config.models.find(
model => model.name === response[responseKey]
(model) => model.name === response[responseKey],
);

if (responseModel) {
const resModelConfig = {
schema: {
$ref: `#/components/schemas/${response[responseKey]}`
}
$ref: `#/components/schemas/${response[responseKey]}`,
},
};

this.attachExamples(responseModel, resModelConfig);
@@ -379,8 +405,8 @@ export class DefinitionGenerator {
}

private getHttpEvents(funcConfig) {
return funcConfig.filter(event =>
event.http || event.httpApi ? true : false
return funcConfig.filter((event) =>
event.http || event.httpApi ? true : false,
);
}
}
61 changes: 38 additions & 23 deletions src/ServerlessOpenApiDocumentation.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { inspect } from "util";

import { DefinitionGenerator } from "./DefinitionGenerator";
import { Format, DefinitionConfig, DefinitionType, ILog } from "./types";
import functionPropertiesSchema from "./functionPropertiesSchema";

interface Options {
indent: number;
@@ -30,6 +31,7 @@ interface Variables {
service: Service;
}

// @ts-ignore
interface FullServerless extends Serverless {
variables: Variables;
processedInput: ProcessedInput;
@@ -65,26 +67,38 @@ export class ServerlessOpenApiDocumentation {
options: {
output: {
usage: "Output file location [default: openapi.yml|json]",
shortcut: "o"
shortcut: "o",
},
format: {
usage: "OpenAPI file format (yml|json) [default: yml]",
shortcut: "f"
shortcut: "f",
},
indent: {
usage: "File indentation in spaces [default: 2]",
shortcut: "i"
}
}
}
}
}
shortcut: "i",
},
},
},
},
},
};

// Declare the hooks our plugin is interested in
this.hooks = {
"openapi:generate:serverless": this.generate.bind(this)
"openapi:generate:serverless": this.generate.bind(this),
};

// Extend the configuration schema for the serverless.yml file
serverless.configSchemaHandler.defineFunctionEventProperties(
"aws",
"http",
functionPropertiesSchema,
);
serverless.configSchemaHandler.defineFunctionEventProperties(
"aws",
"httpApi",
functionPropertiesSchema,
);
}

private log: ILog = (...str: Array<string>) => {
@@ -99,51 +113,52 @@ export class ServerlessOpenApiDocumentation {
// Instantiate DocumentGenerator
const generator = new DefinitionGenerator(
this.customVars.documentation,
this.serverless.config.servicePath
this.serverless.config.servicePath,
);

await generator.parse();

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

// Add Paths to OpenAPI Output from Function Configuration
// @ts-ignore
generator.readFunctions(funcConfigs);

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

this.log(
`${chalk.bold.yellow(
"[VALIDATION]"
)} Validating OpenAPI generated output\n`
"[VALIDATION]",
)} Validating OpenAPI generated output\n`,
);

const validation = generator.validate();

if (validation.valid) {
this.log(
`${chalk.bold.green("[VALIDATION]")} OpenAPI valid: ${chalk.bold.green(
"true"
)}\n\n`
"true",
)}\n\n`,
);
} else {
this.log(
`${chalk.bold.red(
"[VALIDATION]"
)} Failed to validate OpenAPI document: \n\n`
"[VALIDATION]",
)} Failed to validate OpenAPI document: \n\n`,
);
this.log(
`${chalk.bold.green("Context:")} ${JSON.stringify(
validation.context,
null,
2
)}\n`
2,
)}\n`,
);

if (typeof validation.error === "string") {
@@ -177,7 +192,7 @@ export class ServerlessOpenApiDocumentation {
fs.writeFileSync(config.file, output);

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

@@ -189,7 +204,7 @@ export class ServerlessOpenApiDocumentation {
const config: DefinitionType = {
format: Format.yaml,
file: "openapi.yml",
indent: 2
indent: 2,
};

config.indent = this.serverless.processedInput.options.indent || 2;
@@ -198,7 +213,7 @@ export class ServerlessOpenApiDocumentation {

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

@@ -210,7 +225,7 @@ export class ServerlessOpenApiDocumentation {
`${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`
`indentation: "${chalk.bold.red(String(config.indent))}"\n\n`,
);

return config;
102 changes: 0 additions & 102 deletions src/__tests__/DefinitionGenerator.spec.ts

This file was deleted.

181 changes: 181 additions & 0 deletions src/functionPropertiesSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
export default {
properties: {
documentation: {
type: "object",
properties: {
tags: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
summary: {
type: "string",
description: "A brief summary of the operation.",
},
description: {
type: "string",
description:
"A longer description of the operation, GitHub Flavored Markdown is allowed.",
},
externalDocs: {
type: "object",
additionalProperties: false,
description: "information about external documentation",
required: ["url"],
properties: {
description: {
type: "string",
},
url: {
type: "string",
},
},
},
operationId: {
type: "string",
description: "A unique identifier of the operation.",
},
produces: {
description: "A list of MIME types the API can produce.",
allOf: [
{
type: "array",
items: {
type: "string",
description: "The MIME type of the HTTP message.",
},
uniqueItems: true,
},
],
},
consumes: {
description: "A list of MIME types the API can consume.",
allOf: [
{
type: "array",
items: {
type: "string",
description: "The MIME type of the HTTP message.",
},
uniqueItems: true,
},
],
},
parameters: {
type: "array",
description: "The parameters needed to send a valid API call.",
items: {
oneOf: [
{
type: "object",
required: ["name", "in", "schema"],
properties: {
description: {
type: "string",
description:
"A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.",
},
name: {
type: "string",
description: "The name of the parameter.",
},
in: {
type: "string",
description: "Determines the location of the parameter.",
enum: ["body"],
},
required: {
type: "boolean",
description:
"Determines whether or not this parameter is required or optional.",
default: false,
},
schema: {
type: "object",
description:
"A deterministic version of a JSON Schema object.",
},
},
additionalProperties: false,
},
{
type: "object",
required: ["name", "in", "type"],
properties: {
required: {
type: "boolean",
description:
"Determines whether or not this parameter is required or optional.",
default: false,
},
in: {
type: "string",
description: "Determines the location of the parameter.",
enum: ["header", "path", "query"],
},
description: {
type: "string",
description:
"A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.",
},
name: {
type: "string",
description: "The name of the parameter.",
},
type: {
type: "string",
enum: ["string", "number", "boolean", "integer", "array"],
},
format: {
type: "string",
},
},
},
],
},
uniqueItems: true,
},
responses: {
type: "object",
description:
"Response objects names can either be any valid HTTP status code or 'default'.",
minProperties: 1,
additionalProperties: false,
patternProperties: {
"^([0-9]{3})$|^(default)$": {
type: "object",
required: ["description"],
properties: {
description: {
type: "string",
},
schema: {
type: "object",
},
headers: {
type: "object",
},
examples: {
type: "object",
},
},
additionalProperties: false,
},
},
},
schemes: {
type: "array",
},
deprecated: {
type: "boolean",
default: false,
},
security: {
type: "array",
},
},
},
},
};
19 changes: 9 additions & 10 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ function updateReferences(schema: JSONSchema7): JSONSchema7 {
if (cloned.$ref) {
return {
...cloned,
$ref: cloned.$ref.replace("#/definitions", "#/components/schemas")
$ref: cloned.$ref.replace("#/definitions", "#/components/schemas"),
};
}

@@ -31,27 +31,26 @@ function updateReferences(schema: JSONSchema7): JSONSchema7 {
return cloned;
}

export async function parseModels(
models: Array<Model>,
root: string
): Promise<{}> {
export async function parseModels(models: Array<Model>, root: string): Promise<{}> {
const schemas = {};

if (!_.isArrayLike(models)) {
throw new Error("Empty models");
models = [];
}

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

const schema = (typeof model.schema === "string"
? await $RefParser.bundle(path.resolve(root, model.schema))
: model.schema) as JSONSchema7;
const schema = (
typeof model.schema === "string"
? await $RefParser.bundle(path.resolve(root, model.schema))
: model.schema
) as JSONSchema7;

_.assign(schemas, updateReferences(schema.definitions), {
[model.name]: updateReferences(cleanSchema(schema))
[model.name]: updateReferences(cleanSchema(schema)),
});
}

2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ export interface DefinitionConfig {

export enum Format {
yaml = "yaml",
json = "json"
json = "json",
}

export interface DefinitionType {
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import _ = require("lodash");

export const cleanSchema = schema => _.omit(schema, "$schema", "definitions");
export const cleanSchema = (schema) => _.omit(schema, "$schema", "definitions");
19 changes: 0 additions & 19 deletions test/project/models/Common.json

This file was deleted.

118 changes: 0 additions & 118 deletions test/project/models/ErrorResponse.json

This file was deleted.

15 changes: 0 additions & 15 deletions test/project/models/ExternalOne.json

This file was deleted.

15 changes: 0 additions & 15 deletions test/project/models/ExternalTwo.json

This file was deleted.

5 changes: 0 additions & 5 deletions test/project/models/PutDocumentResponse.json

This file was deleted.

31 changes: 0 additions & 31 deletions test/project/models/RecursiveResponse.json

This file was deleted.

31 changes: 0 additions & 31 deletions test/project/models/RecursiveResponseTwo.json

This file was deleted.

268 changes: 0 additions & 268 deletions test/project/openapi.yml

This file was deleted.

3,155 changes: 0 additions & 3,155 deletions test/project/package-lock.json

This file was deleted.

9 changes: 0 additions & 9 deletions test/project/package.json

This file was deleted.

75 changes: 0 additions & 75 deletions test/project/serverless.doc.yml

This file was deleted.

58 changes: 0 additions & 58 deletions test/project/serverless.yml

This file was deleted.

8 changes: 0 additions & 8 deletions tslint.json

This file was deleted.

5,377 changes: 5,377 additions & 0 deletions yarn.lock

Large diffs are not rendered by default.