UNPKG

@imqueue/rpc

Version:

RPC-like client-service implementation over messaging queue

293 lines 9.85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.expose = expose; /*! * IMQ-RPC Decorators: expose * * I'm Queue Software Project * Copyright (C) 2025 imqueue.com <support@imqueue.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * If you want to use this code in a closed source (commercial) project, you can * purchase a proprietary commercial license. Please contact us at * <support@imqueue.com> to get commercial licensing options. */ require("reflect-metadata"); const acorn = require("acorn"); const __1 = require(".."); const TS_TYPES = [ 'object', 'string', 'number', 'boolean', 'null', 'undefined', ]; const descriptions = {}; const RX_ARG_NAMES = /^(\S+)([=\s]+.*?)?$/; const RX_COMMA_SPLIT = /\s*,\s*/; const RX_MULTILINE_CLEANUP = /\*?\n +\* ?/g; const RX_DESCRIPTION = /^([\s\S]*?)@/; const RX_TAG = /(@[^@]+)/g; // noinspection RegExpRedundantEscape const RX_TYPE = /\{([\s\S]+)\}/; const RX_LI_CLEANUP = /^\s*-\s*/; const RX_SPACE_SPLIT = / /; // noinspection RegExpRedundantEscape const RX_OPTIONAL = /^\[(.*?)\]$/; /** * Lookup and returns a list of argument names for a given function * * @param {(...args: any[]) => any} fn * @return {string[]} */ function argumentNames(fn) { let src = fn.toString(); return src.slice(src.indexOf('(') + 1, src.indexOf(')')).split(RX_COMMA_SPLIT).map(arg => arg.trim().replace(RX_ARG_NAMES, '$1')).filter(arg => arg); } /** * Parses given multi-line comment block * * @param {string} src - class source code * @return {CommentMetadata} */ function parseComment(src) { let cleanSrc = src.replace(RX_MULTILINE_CLEANUP, '\n').trim(); let data = { description: '', params: [], returns: { description: '', type: '', tsType: '', } }; let match, tags = []; // istanbul ignore next data.description = String((cleanSrc.match(RX_DESCRIPTION) || [])[1] || '').trim(); while ((match = RX_TAG.exec(cleanSrc))) { tags.push(match[1].trim()); } for (let tag of tags) { let parts = tag.split(RX_SPACE_SPLIT); let tagName = parts.shift(); let tagDef = parts.join(' '); let typeMatch = tagDef.match(RX_TYPE); let tsType = '', name = '', description = '', type = ''; let isOptional = false; if (typeMatch) { tsType = typeMatch[1]; tagDef = tagDef.replace(RX_TYPE, '').trim(); parts = tagDef.split(/ /); } name = (parts.shift() || '').replace(RX_LI_CLEANUP, ''); description = parts.join(' ').replace(RX_LI_CLEANUP, ''); switch (tagName) { case '@param': case '@params': { let opMatch = name.match(RX_OPTIONAL); if (opMatch) { name = opMatch[1]; isOptional = true; } data.params.push({ description, name, tsType, type, isOptional, }); break; } case '@return': case '@returns': { description = [name, description].join(' ').trim(); data.returns = { description, type, tsType }; break; } } } return data; } // codebeat:disable[LOC,ABC] /** * Finds and parses methods and their comment blocks for a given class * * @param {string} name - class name * @param {string} src - class source code */ function parseDescriptions(name, src) { const comments = []; const options = { ecmaVersion: 8, locations: true, ranges: true, allowReserved: true, onComment: comments, }; const nodes = acorn.parse(src, options).body; descriptions[name] = { inherits: 'Function' }; for (let node of nodes) { // istanbul ignore if if (!(node && node.type === 'ClassDeclaration' && node.id && node.id.name === name && node.body.type === 'ClassBody')) { continue; } if (node.superClass) { // istanbul ignore next if (node.superClass.type === 'MemberExpression') { descriptions[name].inherits = node.superClass.property.name; } else if (node.superClass.type === 'Identifier') { descriptions[name].inherits = node.superClass.name; } } const methods = node.body.body.filter((f) => f.type === 'MethodDefinition'); for (let method of node.body.body) { // istanbul ignore if if (method.type !== 'MethodDefinition') { continue; } const methodName = method.key.name; const blocks = comments.filter(comment => comment.type === 'Block'); if (!blocks.length) { continue; } const methodStart = method.start; let lastDif = methodStart - blocks[0].end; let foundBlock = blocks[0]; for (let comment of blocks) { const dif = methodStart - comment.end; if (dif < 0) { break; } if (dif >= lastDif) { continue; } lastDif = dif; foundBlock = comment; } if (!method.range || foundBlock.start > method.range[1]) { continue; } const index = methods.indexOf(method); const prev = index && methods[index - 1]; const prevBeforeComment = !prev || (prev && prev.range && prev.range[1] <= foundBlock.start); if (prevBeforeComment) { // it's a method comment block!!!! descriptions[name][methodName] = { comment: parseComment(foundBlock.value), }; } } } } // codebeat:enable[LOC,ABC] /** * Helper function to make easy descriptions parts extractions * * @param {string} prop - property name to extract * @param {string} className - class name to lookup * @param {string} method - method name to lookup * @param {any} defaults - a default value to use if nothing found * @return {T} - found value */ function get(prop, className, method, defaults) { if (descriptions[className] && descriptions[className][method]) { let comment = descriptions[className][method].comment; // istanbul ignore else if (comment[prop]) { return comment[prop]; } } return defaults; } /** * Converts JavaScript type to most close possible TypeScript type * * @param type * @returns {string} */ function cast(type) { let tsType = String(type).toLowerCase(); if (tsType === 'undefined') { tsType = 'void'; } else if (tsType === 'array') { tsType = 'any[]'; } else if (!~TS_TYPES.indexOf(tsType)) { tsType = type; } return tsType; } /** * Expose decorator factory * * @return {( * target: object, * methodName: (string), * descriptor: TypedPropertyDescriptor<(...args: any[]) => any> * ) => void} - decorator function */ function expose() { return function exposeDecorator(target, methodName, descriptor) { let className = target.constructor.name; let argNames = argumentNames(descriptor.value); let retType = Reflect.getMetadata('design:returntype', target, methodName); let retTypeName = retType ? retType.name : String(retType); if (!descriptions[className]) { parseDescriptions(className, target.constructor.toString()); } let args = get('params', className, methodName, []); let ret = get('returns', className, methodName, { description: '', type: retTypeName, tsType: cast(retTypeName), }); ret.type = retTypeName; if (!args || !args.length) { for (let i = 0, s = argNames.length; i < s; i++) { args[i] = { description: '', name: argNames[i], type: '', tsType: '', isOptional: false, }; } } Reflect.getMetadata('design:paramtypes', target, methodName).forEach((typeConstructor, i) => { args[i].type = args[i].type || typeConstructor.name; args[i].tsType = args[i].tsType || cast(args[i].type); }); __1.IMQRPCDescription.serviceDescription[className] = __1.IMQRPCDescription.serviceDescription[className] || { inherits: descriptions[className].inherits, methods: {}, }; __1.IMQRPCDescription.serviceDescription[className].methods[methodName] = { description: get('description', className, methodName, ''), arguments: args, returns: ret, }; }; } //# sourceMappingURL=expose.js.map