Skip to content

Commit

Permalink
spec generation and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Rivera committed Feb 28, 2021
1 parent 085fd15 commit 3fc1b25
Show file tree
Hide file tree
Showing 11 changed files with 1,460 additions and 69 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
module.exports = {
env: {
browser: true,
node: true,
es2021: true
},
extends: [
'standard'
'eslint:recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
/build
/node_modules

package-lock.json
yarn-error.log
yarn.lock
4 changes: 3 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
{
"printWidth": 105
}
1,226 changes: 1,225 additions & 1 deletion example/petstore.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
"dist",
"templates"
],
"bin": {
"openapi2md": "dist/cli.js"
},
"scripts": {
"example": "tsc && node dist/cli.js --spec example/petstore.json --o build/",
"build": "tsc",
"pretty": "yarn prettier --write src/*.ts",
"prepare": "yarn pretty && yarn lint && yarn build",
Expand Down Expand Up @@ -66,6 +68,7 @@
"add": "^2.0.6",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"xml-js": "^1.6.11",
"yargs": "^16.2.0",
"yarn": "^1.22.10"
}
Expand Down
10 changes: 5 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { hideBin } from "yargs/helpers";
import convert from "./index";

const red = (text) => `\x1b[31m${text}\x1b[0m`;
const magenta = (text) => `\x1b[35m${text}\x1b[0m`;
const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
const green = (text) => `\x1b[32m${text}\x1b[0m`;

Expand All @@ -25,18 +24,19 @@ const argv = yargs(hideBin(process.argv))
type: "string",
default: "./build",
},
template: {
templates: {
alias: "t",
describe: "templates paths",
describe: "custom templates path",
type: "string",
default: "./templates",
default: "../templates",
},
})
.help().argv;

convert(
path.resolve(process.cwd(), argv.spec),
path.resolve(process.cwd(), argv.target)
path.resolve(process.cwd(), argv.target),
path.resolve(__dirname, argv.templates)
)
.then(() => {
console.log(green("Done! ✨"));
Expand Down
205 changes: 194 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,237 @@ import * as fs from "fs";
import * as path from "path";
import * as Handlebars from "handlebars";
import * as _ from "lodash";
import * as xml from "xml-js";

const convert = (specFile: string, outPath: string): Promise<void> => {
/**
* Helper function to serialize an object to JSON
* @param {any} data the object to serialize
* @returns {string} a JSON serialized representation of the object
*/
const buildJson = (data: any): string => {
return JSON.stringify(data, null, 2);
};

/**
* Helper function to serialize an object to XML
* @param {string} name XML name for this node
* @param {any} data the object to serialize
* @returns {string} a XML serialized representation of the object
*/
const buildXml = (name: string, data: any): string => {
let options = { compact: true, spaces: 2 };
let obj = {
_declaration: {
_attributes: {
version: "1.0",
encoding: "utf-8",
},
},
[name]: data,
};

return xml.json2xml(JSON.stringify(obj), options);
};

/**
* Convert openapi spec to markdown
* @param {string} specFile specification file
* @param {string} outPath path to write documents
* @param {string} templatePath path to markdown templates
* @returns {Promise<void>}
*/
const convert = (
specFile: string,
outPath: string,
templatePath: string = "../templates/"
): Promise<void> => {
return new Promise((resolve, reject) => {
try {
// load the spec from a json into an object
const spec = require(specFile);
let schemas: any[] = [];
let schemasObject: any;

if (fs.existsSync(outPath)) {
// ToDo: delete existing path
}

console.log(spec.components.schemas);
const propertiesComponent = (schema) => {
let arr: any[] = [];

if (Object.prototype.hasOwnProperty.call(schema, "type")) {
if (schema.type.includes("object")) {
Object.keys(schema.properties).forEach((pathKey: string) => {
const property = (schema.properties as any)[pathKey];
let example;

// if property has properties is an object, recursively call this function
if (Object.prototype.hasOwnProperty.call(property, "properties")) {
example = Object.fromEntries(propertiesComponent(property.properties));
}
// if property has a reference, try to build from it
else if (Object.prototype.hasOwnProperty.call(property, "$ref")) {
example = Object.fromEntries(
propertiesComponent((spec.components.schemas as any)[property.$ref.split("/").pop()])
);
}
// if an example is defined use it
else if (Object.prototype.hasOwnProperty.call(property, "example")) {
example = property.example;
}
// try to create an example
else {
switch (property.type) {
case "array": {
if (Object.prototype.hasOwnProperty.call(property, "items")) {
if (Object.prototype.hasOwnProperty.call(property.items, "$ref")) {
example = [
Object.fromEntries(
propertiesComponent(
(spec.components.schemas as any)[property.items.$ref.split("/").pop()]
)
),
];
} else if (Object.prototype.hasOwnProperty.call(property.items, "type")) {
// propertyName = property.items.xml.name;
example = [
{
[property.items.xml.name]: property.items.type,
},
];
}
}

break;
}
case "string": {
if (Object.prototype.hasOwnProperty.call(property, "format")) {
if (property.format == "date-time") {
let date = new Date();
example = date.toISOString();
} else if (property.format == "date") {
let date = new Date();
example = date.toISOString().substring(0, 10);
}
}

if (
Object.prototype.hasOwnProperty.call(property, "enum") &&
property.enum.length > 0
) {
example = property.enum[0];
}

break;
}
case "uid": {
example = Math.random()
.toString(36)
.replace(/[^a-z0-9]+/g, "")
.substr(0, 8);
break;
}
case "number": {
example = Math.random() * 10000;

break;
}
case "integer": {
example = Math.floor(Math.random() * 10000);

break;
}
case "boolean": {
example = true;

break;
}
}
}

// unable to make an example, just use the property name
if (example === undefined) {
example = pathKey;
}

arr.push([pathKey, example]);
// propertyName = key;
});
}
}

return arr;
};

Handlebars.registerHelper("schemaRef", (key, context, schemas) => {
if (Object.prototype.hasOwnProperty.call(context, "$ref")) {
if (key.toLowerCase().includes("json")) {
return buildJson(schemas[context.$ref]);
} else if (key.toLowerCase().includes("xml")) {
return buildXml(context.$ref.split("/").pop().toLowerCase(), schemas[context.$ref]);
}
} else if (Object.prototype.hasOwnProperty.call(context, "type")) {
if (context.type.includes("array")) {
if (Object.prototype.hasOwnProperty.call(context.items, "$ref")) {
if (key.toLowerCase().includes("json")) {
return buildJson([schemas[context.items.$ref]]);
} else if (key.toLowerCase().includes("xml")) {
return buildXml(context.items.$ref.split("/").pop().toLowerCase(), [
schemas[context.items.$ref],
]);
}
}
}
} else {
// ToDo: implement logic for schemes without $ref
}

return "{}";
});

// iterate schemas to build a sample js object
Object.keys(spec.components.schemas).forEach((key: string) => {
const schema = (spec.components.schemas as any)[key];
const schemaRef = (spec.components.schemas as any)[key];

console.log(`${key}\n\n`, schema);
// store the schema into an array
schemas.push([
`#/components/schemas/${key}`,
Object.fromEntries(propertiesComponent(schemaRef)),
]);
});

// transform the schemas array into an object
schemasObject = Object.fromEntries(schemas);

const pathTemplate = Handlebars.compile(
fs.readFileSync(
path.resolve(__dirname, "../templates/path.hdb"),
"utf8"
)
fs.readFileSync(path.resolve(__dirname, templatePath, "path.hdb"), "utf8")
);

// iterate paths
Object.keys(spec.paths).forEach((schemaKey: string) => {
const apiPath = (spec.paths as any)[schemaKey];

// try to create output paths
fs.mkdirSync(`${outPath}${schemaKey}`, { recursive: true });
console.log(`${schemaKey}`);

Object.keys(apiPath).forEach((apiPathKey: string) => {
const method = (apiPath as any)[apiPathKey];

// render the path using Handlebars and save it
fs.writeFileSync(
`${outPath}${schemaKey}/${apiPathKey}.md`,
pathTemplate({
slug: _.kebabCase(`${schemaKey}-${apiPathKey}`),
path: schemaKey,
httpMethod: _.toUpper(apiPathKey),
method: method,
schemas: schemasObject,
})
);

console.log(`\t${apiPathKey}`);
});
});

// all done
resolve();
} catch (error) {
reject(error);
Expand Down
51 changes: 7 additions & 44 deletions templates/path.hdb
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,10 @@ Update an existent pet in the store

| Options | |
| ------------ | ---------------- |
| required | true |
| required | true |
| content-type | application-json |

**Example Value**

```json
{
"id": 10,
"name": "doggie",
"category": {
"id": 1,
"name": "Dogs"
},
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": "available"
}
```

## Responses

Expand All @@ -66,30 +45,14 @@ Update an existent pet in the store
| ------------ | ---------------- |
| content-type | {{@key}} |

**Example Value**
**Example response**

```json
{
"id": 10,
"name": "doggie",
"category": {
"id": 1,
"name": "Dogs"
},
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": "available"
}
{{{schemaRef @key schema ../../../schemas}}}
```

{{/each}}
{{/each}}

{{/with}}
{{/each}} {{! content }}
{{/each}} {{! responses }}

{{/with}} {{! method }}
Loading

0 comments on commit 3fc1b25

Please sign in to comment.