import { readFile } from "fs/promises"; import * as handlebars from 'handlebars'; import { join, relative } from "path"; import { Logger } from 'winston'; import { createFile, createPath } from "../../helpers/fileMethods"; import { deepClone } from "../../helpers/objectMethods"; import { replaceAll } from "../../helpers/stringMethods"; import { IJsonSchema } from '../../types/IJSONSchema'; import { INopeDescriptorFunctionParameter } from "../../types/nope/nopeDescriptor.interface"; import { IFunctionOptions, IParsableDescription } from "../../types/nope/nopeModule.interface"; /** * Helper function to merge the Parameters into 1 description. * @param elements */ function _unifySchema(elements: INopeDescriptorFunctionParameter[], options: { sharedDefinitions: boolean, logger?: Logger, }){ const ret: IJsonSchema = { type: 'object', properties: {}, required: [] } for (const item of elements){ if (options.sharedDefinitions){ item.schema.definitions = undefined; item.schema.$schema = undefined; } // Assign the Schema ret.properties[item.name] = item.schema as any as IJsonSchema; // If the Element is Optional, if (!item.optional){ ret.required.push(item.name) } } return ret; } /** * Function, to parse a description to an Open-API Element. * @param description * @param options */ export async function parseModuleToOpenAPI(description: IParsableDescription, options: { outputDir: string, sharedDefinitions: boolean, logger?: Logger, }){ const _description = deepClone(description); // load the Template. const template = await readFile(join(process.cwd(), 'lib' , 'parsers', 'open-api' ,'templates', 'method.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. 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'; // 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'; // Open API uses "required" instead of optional => invert param.optional = !param.optional; 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; }); // 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).tag = description.name; // Determine the Filename. const fileDir = join(options.outputDir, _description.name, '{instance}' ,'methods'); const fileName = join(fileDir, method.id+'.ts'); // 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') } } } /** * Function, to parse a description to an Open-API Element. * @param description * @param options */ export async function parseFunctionToOpenAPI(description: IFunctionOptions, options: { outputDir: string, sharedDefinitions: boolean, logger?: Logger, }){ const _description = deepClone(description); // load the Template. const template = await readFile(join(process.cwd(), 'lib' , 'parsers', 'open-api' ,'templates', 'function.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'; // Open API uses "required" instead of optional => invert param.optional = !param.optional; 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; }); // Make shure the Return type isnt an array _description.schema.outputs = Array.isArray(_description.schema.outputs) ? _unifySchema(_description.schema.outputs, options) : _description.schema.outputs; if (options.sharedDefinitions) { _description.schema.outputs.definitions = undefined; _description.schema.outputs.$schema = undefined; } // 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'; // Determine the Filename. const fileDir = join(options.outputDir, 'generic-services'); const fileName = join(fileDir, _description.id+'.ts'); // 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), '\\', '/'); } // Write down the Schema: await createFile( // Generate the Path. fileName, renderAPI(_description) ); if (options.logger) { options.logger.info('Generated -> ' + fileName); } } else if (options.logger){ options.logger.warn('parser can not convert: "' + _description.id + '". The Function contains functions as parameters') } }