UNPKG

jii

Version:

Jii - Full-Stack JavaScript Framework

511 lines (458 loc) 17.4 kB
/** * @author Ihor Skliar <skliar.ihor@gmail.com> * @license MIT */ 'use strict'; const Jii = require('../index'); const Console = require('../helpers/Console'); const Exception = require('./Exception'); const _trim = require('lodash/trim'); const _isEmpty = require('lodash/isEmpty'); const _indexOf = require('lodash/indexOf'); const _isArray = require('lodash/isArray'); const _toArray = require('lodash/toArray'); const _isFunction = require('lodash/isFunction'); const _isUndefined = require('lodash/isUndefined'); const _each = require('lodash/each'); const _values = require('lodash/values'); const _compact = require('lodash/compact'); const BaseController = require('../base/Controller'); const fs = require('fs'); const extract = require('extract-comments'); class Controller extends BaseController { preInit() { /** * @type {string[]} the options passed during execution. */ this._passedOptions = []; /** * @type {{main: {help: string, description: string}, properties: object, actions: object}} */ this._comments = null; /** * If not set, ANSI color will only be enabled for terminals that support it. * @type {boolean} whether to enable ANSI color in the output. */ this.color = true; /** * @type {boolean} whether to run the command interactively. */ this.interactive = true; super.preInit(...arguments); } /** * Returns a value indicating whether ANSI color is enabled. * * ANSI color is enabled only if [[color]] is set true or is not set * and the terminal supports ANSI color. * * @returns {boolean} Whether to enable ANSI style in output. */ isColorEnabled() { return this.color; } /** * Runs an action with the specified action ID and parameters. * If the action ID is empty, the method will use [[defaultAction]]. * @param {string} id the ID of the action to be executed. * @param {Context} context * @returns {Promise} the status of the action execution. 0 means normal, other values mean abnormal. */ runAction(id, context) { var params = context.request.getParams(); if (!_isEmpty(params)) { // populate options here so that they are available in beforeAction(). var options = this.options(id === '' ? this.defaultAction : id); _each(params, (value, name) => { if (_indexOf(options, name) !== -1) { var defaultAction = this.name; this.name = _isArray(defaultAction) ? value.split(/\s*,\s*/) : value; delete params[name]; } else if (Number(name) != name) { throw new Exception(Jii.t('jii', 'Unknown option: --{name}', { name: name })); } this._passedOptions.push(name); }); } return super.runAction(id, context); } /** * Formats a string with ANSI codes * * You may pass additional parameters using the constants defined in [[Console]]. * * Example: * * ``` * echo this.ansiFormat('This will be red and underlined.', FG_RED, UNDERLINE); * ``` * * @param {string} string the string to be formatted * @returns {string} */ ansiFormat(string) { if (this.isColorEnabled()) { string = Console.ansiFormat(string, _toArray(arguments).slice(1)); } return string; } /** * Prints a string to STDOUT * * You may optionally format the string with ANSI codes by * passing additional parameters using the constants defined in [[Console]]. * * Example: * * ``` * this.stdout('This will be red and underlined.', FG_RED, UNDERLINE); * ``` * * @param {string} string the string to print * @returns {int|boolean} Number of bytes printed or false on error */ stdout(string) { if (this.isColorEnabled()) { string = Console.ansiFormat(string, _toArray(arguments).slice(1)); } Console.stdout(string); } /** * Prints a string to STDERR * * You may optionally format the string with ANSI codes by * passing additional parameters using the constants defined in [[Console]]. * * Example: * * ``` * this.stderr('This will be red and underlined.', FG_RED, UNDERLINE); * ``` * * @param {string} string the string to print * @returns {int|boolean} Number of bytes printed or false on error */ stderr(string) { if (this.isColorEnabled()) { string = Console.ansiFormat(string, _toArray(arguments).slice(1)); } Console.stderr(string); } /** * Prompts the user for input and validates it * * @param {string} text prompt string * @param {[]} options the options to validate the input: * * - required: whether it is required or not * - default: default value if no input is inserted by the user * - pattern: regular expression pattern to validate user input * - validator: a callable function to validate input. The function must accept two parameters: * - input: the user input to validate * - error: the error value passed by reference if validation failed. * @returns {Promise} the user input */ prompt(text, options) { options = options || []; if (this.interactive) { return Console.prompt(text, options); } else { return Promise.resolve(options['default'] || ''); } } /** * Asks user to confirm by typing y or n. * * @param {string} message to echo out before waiting for user input * @param {boolean} [defaultValue] this value is returned if no selection is made. * @returns {Promise} */ confirm(message, defaultValue) { defaultValue = defaultValue || false; if (this.interactive) { return Console.confirm(message, defaultValue); } else { return Promise.resolve(true); } } /** * Gives the user an option to choose from. Giving '?' as an input will show * a list of options to choose from and their explanations. * * @param {string} prompt the prompt message * @param {[]} options Key-value array of options to choose from * * @returns {Promise} An option character the user chose */ select(prompt, options) { options = options || []; return Console.select(prompt, options); } /** * Returns the names of valid options for the action (id) * An option requires the existence of a public member variable whose * name is the option name. * Child classes may override this method to specify possible options. * * Note that the values setting via options are not available * until [[beforeAction()]] is being called. * * @param {string} actionID the action id of the current request * @returns {[]} the names of the options valid for the action */ options(actionID) { // actionId might be used in subclasses to provide options specific to action id return [ 'color', 'interactive' ]; } /** * Returns properties corresponding to the options for the action id * Child classes may override this method to specify possible properties. * * @param {string} actionID the action id of the current request * @returns {object} properties corresponding to the options for the action */ getOptionValues(actionID) { // actionId might be used in subclasses to provide properties specific to action id var properties = {}; _each(this.options(this.action.id), property => { properties[property] = this.get(property); }); return properties; } /** * Returns the names of valid options passed during execution. * * @returns {[]} the names of the options passed during execution */ getPassedOptions() { return this._passedOptions; } /** * Returns the properties corresponding to the passed options * * @returns {object} the properties corresponding to the passed options */ getPassedOptionValues() { var properties = {}; _each(this._passedOptions, property => { properties[property] = this.get(property); }); return properties; } /** * Returns one-line short summary describing this controller. * * You may override this method to return customized summary. * The default implementation returns first line from the PHPDoc comment. * * @returns {string} */ getHelpSummary() { return this._parseDocCommentSummary(); } /** * Returns help information for this controller. * * You may override this method to return customized help. * The default implementation returns help information retrieved from the PHPDoc comment. * @returns {string} */ getHelp() { return this._parseDocCommentDetail(); } /** * Returns a one-line short summary describing the specified action. * @param {Action} action action to get summary for * @returns {string} a one-line short summary describing the specified action. */ getActionHelpSummary(action) { return this._parseDocCommentSummaryForAction(action.id); } /** * Returns the detailed help information for the specified action. * @param {Action} action action to get help for * @returns {string} the detailed help information for the specified action. */ getActionHelp(action) { return this._parseDocCommentDetailForAction(action.id); } /** * Returns the help information for the anonymous arguments for the action. * The returned value should be an array. The keys are the argument names, and the values are * the corresponding help information. Each value must be an array of the following structure: * * - required: boolean, whether this argument is required. * - type: string, the js type of this argument. * - default: string, the default value of this argument * - comment: string, the comment of this argument * * The default implementation will return the help information extracted from the doc-comment of * the parameters corresponding to the action method. * * @param {Action} action * @returns {[]} the help information of the action arguments */ getActionArgsHelp(action) { return this.getActionHelp(action).params; } /** * Returns the help information for the options for the action. * The returned value should be an array. The keys are the option names, and the values are * the corresponding help information. Each value must be an array of the following structure: * * - type: string, the js type of this argument. * - default: string, the default value of this argument * - comment: string, the comment of this argument * * The default implementation will return the help information extracted from the doc-comment of * the properties corresponding to the action options. * * @param {Action} action * @returns {object} the help information of the action options */ getActionOptionsHelp(action) { var optionNames = this.options(action.id); if (_isEmpty(optionNames)) { return []; } var options = {}; _each(this, (property, name) => { if (_isFunction(this[name]) || _indexOf(optionNames, name) === -1) { return; } var defaultValue = this[name]; var tags = this._parseDocCommentTags(name); if (tags && (tags.var !== undefined || tags.property !== undefined)) { options[name] = !_isUndefined(tags.var) ? tags.var : tags.property; options[name].default = defaultValue; } else { options[name] = { type: null, default: defaultValue, comment: '' }; } }); return options; } /** * Returns the first line of docblock. * * @returns {string} */ _parseDocCommentSummary() { this._loadComments(); return this._comments.main && this._comments.main.help || ''; } /** * Returns the first line of docblock for some action. * * @param {string} id of action * @returns {string} */ _parseDocCommentSummaryForAction(id) { this._loadComments(); return this._comments.actions[id] && this._comments.actions[id].help || ''; } /** * Returns full description from the main docblock. * * @returns {string} */ _parseDocCommentDetail() { this._loadComments(); return this._comments.main && this._comments.main.description || ''; } /** * Returns full description from the action docblock. * * @param {string} id of action * @returns {string} */ _parseDocCommentDetailForAction(id) { this._loadComments(); return this._comments.actions[id] && this._comments.actions[id].description || ''; } /** * Returns object of information about some property from the docblock. * * @param {string} property name * @returns {object} */ _parseDocCommentTags(property) { this._loadComments(); return this._comments.properties[property] || null; } /** * * @private */ _loadComments() { if (this._comments) { return; } var comments = _values(extract(/*fs.readFileSync(classPath, 'utf-8')*/'')); // TODO var exp = /^action(.*)(:|\()(.*)/i; var exp2 = /\n@/gi; var exp3 = /^@/i; var propertyDataExp = /^@(var|property) (\w+) ((.|[\s\S])*)/; var propertyExp = /^@(var|property)(.*)/i; var paramExp = /^@param(.*)/i; var paramDataExp = /^@param \{(\w+)\} (\w+) ((.|[\s\S])*)/i; this._comments = { main: { help: '', description: '' }, properties: {}, actions: {} }; _each(comments, (comment, i) => { var content = comment.content.split('\n'); var fakeContent = comment.content.replace(exp2, '\n@@'); var fakeArr = fakeContent.split(exp2); var description = _compact(fakeArr.map(line => exp3.test(line) ? '' : line)).join('\n'); description = _trim(description, '\n'); if (comment.code && exp.test(comment.code)) { var actionId = comment.code.replace(exp, '$1').toLowerCase(); // @todo lowercase is wrong. need convert AaBb to aa-bb. if (this._comments.actions[actionId] === undefined) { this._comments.actions[actionId] = { help: '', params: {} }; } this._comments.actions[actionId].help = content !== undefined && content[0] !== '' ? content[0] : ''; var params = _compact(fakeArr.map(line => exp3.test(line) ? line : '')); this._comments.actions[actionId].description = description; _each(params, line => { if (paramExp.test(line)) { var arr = line.replace(paramDataExp, '$1{SEP}$2{SEP}$3').split('{SEP}'); this._comments.actions[actionId].params[arr[1]] = { type: arr[0], comment: arr[2] }; } }); } else if (propertyExp.test(comment.content)) { var pName = comment.code.split(':')[0]; var arr = comment.content.replace(propertyDataExp, '$1{SEP}$2{SEP}$3').split('{SEP}'); if (this._comments.properties[pName] === undefined) { this._comments.properties[pName] = {}; } this._comments.properties[pName][arr[0]] = { type: arr[1], comment: arr[2] }; } }); } } Controller.EXIT_CODE_ERROR = 1; Controller.EXIT_CODE_NORMAL = 0; module.exports = Controller;