nope/lib/parsers/open-api/OpenApiParser.ts

341 lines
9.7 KiB
TypeScript
Raw Normal View History

2021-11-16 20:41:08 +00:00
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2021-11-11 11:36:37
* @modify date 2021-11-11 11:36:37
* @desc [description]
*/
2020-11-09 14:28:09 +00:00
import { readFile } from "fs/promises";
2021-11-16 20:41:08 +00:00
import * as handlebars from "handlebars";
import { ILogger } from "js-logger";
2020-11-09 06:42:24 +00:00
import { join, relative } from "path";
import { createFile, createPath } from "../../helpers/fileMethods";
import { deepClone } from "../../helpers/objectMethods";
2020-11-09 14:28:09 +00:00
import { replaceAll } from "../../helpers/stringMethods";
2021-11-16 20:41:08 +00:00
import { IJsonSchema } from "../../types/IJSONSchema";
2020-11-09 14:28:09 +00:00
import { INopeDescriptorFunctionParameter } from "../../types/nope/nopeDescriptor.interface";
2021-12-04 07:25:26 +00:00
import {
IFunctionOptions,
IParsableDescription,
} from "../../types/nope/nopeModule.interface";
2020-11-09 06:42:24 +00:00
/**
* Helper function to merge the Parameters into 1 description.
2021-12-04 07:25:26 +00:00
* @param elements
2020-11-09 06:42:24 +00:00
*/
2021-12-04 07:25:26 +00:00
function _unifySchema(
elements: INopeDescriptorFunctionParameter[],
options: {
sharedDefinitions: boolean;
logger?: ILogger;
}
) {
const ret: IJsonSchema = {
type: "object",
properties: {},
required: [],
};
for (const item of elements) {
if (options.sharedDefinitions) {
item.schema.definitions = undefined;
item.schema.$schema = undefined;
}
2021-12-04 07:25:26 +00:00
// Assign the Schema
ret.properties[item.name] = item.schema as any as IJsonSchema;
2020-11-09 06:42:24 +00:00
2021-12-04 07:25:26 +00:00
// If the Element is Optional,
if (!item.optional) {
ret.required.push(item.name);
2020-11-09 06:42:24 +00:00
}
2021-12-04 07:25:26 +00:00
}
2020-11-09 06:42:24 +00:00
2021-12-04 07:25:26 +00:00
return ret;
2020-11-09 06:42:24 +00:00
}
/**
* Function, to parse a description to an Open-API Element.
2021-12-04 07:25:26 +00:00
* @param description
* @param options
2020-11-09 06:42:24 +00:00
*/
2021-12-04 07:25:26 +00:00
export async function parseModuleToOpenAPI(
description: IParsableDescription,
options: {
outputDir: string;
sharedDefinitions: boolean;
mode: "js" | "ts";
logger?: Logger;
}
): Promise<void> {
const _description = deepClone(description);
// load the Template.
const template = await readFile(
join(
process.cwd(),
"lib",
"parsers",
"open-api",
"templates",
"method." + options.mode + ".handlebars"
),
{
encoding: "utf-8",
}
);
// Renderfuncting
const renderAPI = handlebars.compile(template);
2020-11-15 19:11:25 +00:00
2021-12-04 07:25:26 +00:00
await createPath(join(options.outputDir));
// Now iterate over the Methods of the Module and find parsable Methods.
for (const [idx, method] of _description.methods.entries()) {
// Test if the Method contains some functions in the input / Output:
const parsedInputs = JSON.stringify(method.schema.inputs);
const parsedOutput = JSON.stringify(method.schema.outputs);
if (
!parsedInputs.includes('"function"') &&
!parsedOutput.includes('"function"')
) {
// The Method should be parseable.
// 1. Specify the Mode (No Params = GET, else POST)
(method as any).mode = method.schema.inputs.length > 0 ? "POST" : "GET";
(method as any).required = method.schema.inputs
.filter((param) => !param.optional)
.map((param) => param.name);
(method as any).hasInput = method.schema.inputs.length > 0;
// Now adapt the Schema of the Method.
// Iterate over the Inputs, add a Description if not provided
// And determine whehter the Parameters are required or not.
method.schema.inputs = method.schema.inputs.map((param) => {
// Provide a Description. (If not provided)
param.description = param.description || "Not provided";
if (options.sharedDefinitions) {
param.schema.definitions = undefined;
param.schema.$schema = undefined;
2020-11-09 06:42:24 +00:00
}
2021-12-04 07:25:26 +00:00
// Parse the Schema in here.
(param as any).parsedSchema = JSON.stringify(param.schema);
return param;
});
// Make shure the Return type isnt an array
method.schema.outputs = Array.isArray(method.schema.outputs)
? _unifySchema(method.schema.outputs, options)
: method.schema.outputs;
if (options.sharedDefinitions) {
method.schema.outputs.definitions = undefined;
method.schema.outputs.$schema = undefined;
}
// Add an Description to the result.
(method as any).resultDescription =
method.schema.outputs.description || "Not Provided";
// And add a Schema for the Return type.
(method as any).parsedOutput = JSON.stringify(method.schema.outputs);
(method as any).parsedInput = JSON.stringify(
_unifySchema(method.schema.inputs, options)
);
(method as any).hasInput = method.schema.inputs.length > 0;
(method as any).tag = description.name;
// Determine the Filename.
const fileDir = join(
options.outputDir,
_description.name,
"{instance}",
"methods"
);
const fileName = join(fileDir, method.id + "." + options.mode);
// Determine the Import Pathes.
const imports = [
{
dir: join(process.cwd(), "lib", "types", "nope"),
fileName: "nopeDispatcher.interface",
name: "pathOfDispatcher",
},
{
dir: join(process.cwd(), "lib", "helpers"),
fileName: "dispatcherPathes",
name: "pathOfHelper",
},
];
for (const imp of imports) {
const relativDir = relative(fileDir, imp.dir);
(method as any)[imp.name] = replaceAll(
join(relativDir, imp.fileName),
"\\",
"/"
);
}
// Write down the Schema:
await createFile(
// Generate the Path.
fileName,
renderAPI(method)
);
if (options.logger) {
options.logger.info("Generated -> " + fileName);
}
} else if (options.logger) {
options.logger.warn(
'parser can not convert: "' +
description.name +
"." +
method.id +
'". The Function contains functions as parameters'
);
2020-11-09 06:42:24 +00:00
}
2021-12-04 07:25:26 +00:00
}
}
/**
* Function, to parse a description to an Open-API Element.
2021-12-04 07:25:26 +00:00
* @param description
* @param options
*/
2021-12-04 07:25:26 +00:00
export async function parseFunctionToOpenAPI(
description: IFunctionOptions,
options: {
outputDir: string;
sharedDefinitions: boolean;
mode: "js" | "ts";
logger?: Logger;
}
): Promise<void> {
const _description = deepClone(description);
// load the Template.
const template = await readFile(
join(
process.cwd(),
"lib",
"parsers",
"open-api",
"templates",
"function." + options.mode + ".handlebars"
),
{
encoding: "utf-8",
}
);
// Renderfuncting
const renderAPI = handlebars.compile(template);
await createPath(join(options.outputDir));
// Now iterate over the Methods of the Module and find parsable Methods.
// Test if the Method contains some functions in the input / Output:
const parsedInputs = JSON.stringify(_description.schema.inputs);
const parsedOutput = JSON.stringify(_description.schema.outputs);
if (
!parsedInputs.includes('"function"') &&
!parsedOutput.includes('"function"')
) {
// The Method should be parseable.
// 1. Specify the Mode (No Params = GET, else POST)
(_description as any).mode =
_description.schema.inputs.length > 0 ? "POST" : "GET";
// Now adapt the Schema of the Method.
// Iterate over the Inputs, add a Description if not provided
// And determine whehter the Parameters are required or not.
_description.schema.inputs = _description.schema.inputs.map((param) => {
// Provide a Description. (If not provided)
param.description = param.description || "Not provided";
if (options.sharedDefinitions) {
param.schema.definitions = undefined;
param.schema.$schema = undefined;
}
// Parse the Schema in here.
(param as any).parsedSchema = JSON.stringify(param.schema);
return param;
2021-11-16 20:41:08 +00:00
});
2020-11-15 19:11:25 +00:00
2021-12-04 07:25:26 +00:00
// Make shure the Return type isnt an array
_description.schema.outputs = Array.isArray(_description.schema.outputs)
? _unifySchema(_description.schema.outputs, options)
: _description.schema.outputs;
2021-12-04 07:25:26 +00:00
if (options.sharedDefinitions) {
_description.schema.outputs.definitions = undefined;
_description.schema.outputs.$schema = undefined;
}
2020-11-15 19:11:25 +00:00
2021-12-04 07:25:26 +00:00
// Add an Description to the result.
(_description as any).resultDescription =
_description.schema.outputs.description || "Not Provided";
// And add a Schema for the Return type.
(_description as any).parsedOutput = JSON.stringify(
_description.schema.outputs
);
(_description as any).tag = "generic-services";
(_description as any).parsedInput = JSON.stringify(
_unifySchema(_description.schema.inputs, options)
);
(_description as any).hasInput = _description.schema.inputs.length > 0;
(_description as any).required = _description.schema.inputs
.filter((param) => !param.optional)
.map((param) => param.name);
(_description as any).name = _description.id;
// Determine the Filename.
const fileDir = join(options.outputDir, "generic-services");
const fileName = join(fileDir, _description.id + "." + options.mode);
// Determine the Import Pathes.
const imports = [
{
dir: join(process.cwd(), "lib", "types", "nope"),
fileName: "nopeDispatcher.interface",
name: "pathOfDispatcher",
},
];
for (const imp of imports) {
const relativDir = relative(fileDir, imp.dir);
(_description as any)[imp.name] = replaceAll(
join(relativDir, imp.fileName),
"\\",
"/"
);
}
2020-11-15 19:11:25 +00:00
2021-12-04 07:25:26 +00:00
// Write down the Schema:
await createFile(
// Generate the Path.
fileName,
renderAPI(_description)
);
2020-11-15 19:11:25 +00:00
2021-12-04 07:25:26 +00:00
if (options.logger) {
options.logger.info("Generated -> " + fileName);
2020-11-15 19:11:25 +00:00
}
2021-12-04 07:25:26 +00:00
} else if (options.logger) {
options.logger.warn(
'parser can not convert: "' +
_description.id +
'". The Function contains functions as parameters'
);
}
}