Is it possible for me to get the descriptor for the actual constructor method of a JS class? I am writing my own little wrapper around Express to try and procedurally generate routes and stuff (and I am sure there are plenty of other libraries to do this, but it’s a learning experience).
If I iterate through the function descriptors for a class prototype, I can convert the ‘value’ property to a string that represents a single method. If I call getString() on the descriptor for ‘constructor’ I get the entire class. I ONLY want the body of the constructor. I could add more parsing logic, but there has to be a way to do this.
/**
* Register all controllers and their routes
* @param {import('./application')} application
*/
static async registerControllersAsync(application) {
const
config = application.config,
app = application.express;
let controllerDir = path.resolve(config.rootDirectory, config.getValue('server.controllerDirectory', 'controllers')),
controllerFiles = (await readdir(controllerDir))
.filter(f => f.indexOf('Controller') > -1)
.map(f => path.join(controllerDir, f)),
parseMethodName = /(?<verb>(get|post|head|delete|put|connect|trace|patch))(?<path>.*)/;
for(const controllerFile of controllerFiles) {
const
controllerType = require(controllerFile),
router = express.Router(),
pathPrefix = controllerType.pathPrefix || false,
controllerName = controllerType.name.slice(0, controllerType.name.indexOf('Controller')).toLowerCase();
let
viewSearchPath = [ path.resolve(`${config.getValue('server.paths.viewPathRoot', 'views')}`, controllerName) ]
.concat(config.getValue('server.paths.sharedViews', [])
.map(p => path.resolve(config.rootDirectory, p)));
const
controllerSettings = {
application,
config,
constructorParameters: [],
controllerName,
controllerType,
viewLookupCache: BaseController.getControllerViewCache(controllerName),
viewSearchPath
};
if (typeof controllerType.registerRoutes === 'function')
controllerType.registerRoutes(app, router);
else {
/**
* Autowire "by convention" steps:
* (1) find methods beginning with valid HTTP verbs (e.g. get, post),
* (2) use remainder of method name as path,
* (3) extract any parameter names from handler and append to path as placeholders,
* (4) create the actual callback handle wrapper for incoming requests in the controller router,
*/
let descriptors = Object.getOwnPropertyDescriptors(controllerType.prototype);
/** @type {{ parameters: string[], verb: string, name: string, urlPath: string, ranking: number, defaultView: string }[]} */
let sortedRoutes = Object.keys(descriptors).map(name => {
/**
* Extract parameter names from a function
* @param {string} methodDef The raw method text to parse
* @returns
*/
function getParameterNames(methodDef) {
const parameterListStart = methodDef.indexOf('(') + 1,
parameterListEnd = methodDef.indexOf(')'),
parameterList = methodDef.slice(parameterListStart, parameterListEnd),
parameters = parameterList.split(',')
.filter(p => p.length > 0)
.map(p => {
let n = p.indexOf('=');
if (n > -1) {
p = p.slice(0, n);
return p.trim();
}
return p.trim();
});
return parameters;
}
let desc = descriptors[name];
if (typeof desc.value === 'function') {
let m = parseMethodName.exec(name),
// Routes with fewer parameters rank higher than those with more
ranking = 0;
if (m) {
/** @type {[string, string]} */
let { verb, path } = m.groups,
defaultView = path || 'index',
urlPath = controllerType.prototype[name].urlPath,
/** @type {string[]} */
parameters = getParameterNames(desc.value.toString());
if (typeof urlPath !== 'string') {
ranking += parameters.length;
if (parameters.length > 0) {
const pathSpecifier = parameters
.map(p => `:${p}`)
.join('/');
urlPath = `/${path}/${pathSpecifier}`;
}
else {
urlPath = '/' + path;
}
}
return { parameters, verb, name, urlPath, ranking, defaultView };
}
else if (name === 'constructor') {
// Default controller constructor only takes settings;
// Other parameters are assumed to be DI container references
const parameters = getParameterNames(desc.value.toString()).slice(1);
controllerSettings.constructorParameters.push(...parameters);
}
}
return false;
})
.filter(r => r !== false)
.sort((a, b) => {
if (a.ranking < b.ranking)
return -1;
else if (a.ranking > b.ranking)
return 1;
else
return a.name.localeCompare(b);
});
sortedRoutes.forEach(route => {
controllerType.prototype[route.name].defaultView = route.defaultView;
router[route.verb].call(router, route.urlPath,
/**
* @param {express.Request} request The incomming request message
* @param {express.Response} response The response to send back to the client.
*/
async (request, response) => {
try {
const
/** Create new controller for request, pass settings, and DI requirements @type {BaseController} */
controller = BaseController.createController(application, controllerName, request, response),
/** Fill the parameters with their respective values @type {string[]} */
parameterList = route.parameters.map(p => {
if (p in request.params)
return request.params[p];
else if (p in request.body)
return request.body[p];
});
await controller[route.name].apply(controller, parameterList);
}
catch(err) {
application.handleError(request, response, err);
}
});
});
}
BaseController.addControllerType(controllerSettings);
if (pathPrefix)
app.use(pathPrefix, router);
else
app.use(router);
}
app.get('/', (req, res) => {
res.sendStatus(404);
});
}
And here is my simple controller:
const
BaseController = require('../src/baseController');
/**
* Handle requests for the home page
*/
class HomeController extends BaseController {
/**
* @param {import('../src/baseController').ControllerSettings} settings Controller settings
* @param {import('../src/vault/vaultClient')} vault Injected vault client
*/
constructor(settings, vault) {
super(settings);
this.vault = vault;
}
/**
* Serve the landing page
* @returns
*/
async get() {
await this.renderAsync('index', { pageTitle: 'Pechanga Demo' });
}
}
module.exports = HomeController;
Here is what currently comes back for ‘get’:
Object.getOwnPropertyDescriptor(controllerType.prototype, 'get').value.toString()
"async get() {
await this.renderAsync('index', { pageTitle: 'Pechanga Demo' });
}"
And, of course, this is what comes back for ‘constructor’:
"async get() {
await this.renderAsync('index', { pageTitle: 'Pechanga Demo' });
}"
Object.getOwnPropertyDescriptor(controllerType.prototype, 'constructor').value.toString()
"class HomeController extends BaseController {
/**
* @param {import('../src/baseController').ControllerSettings} settings Controller settings
* @param {import('../src/vault/vaultClient')} vault Injected vault client
*/
constructor(settings, vault) {
super(settings);
this.vault = vault;
}
/**
* Serve the landing page
* @returns
*/
async get() {
await this.renderAsync('index', { pageTitle: 'Pechanga Demo' });
}
}"
But what I really want is just:
constructor(settings, vault) {
super(settings);
this.vault = vault;
}
Is this possible without parsing the entire class body?