UNPKG

jii

Version:

Jii - Full-Stack JavaScript Framework

482 lines (422 loc) 17.4 kB
/** * Jii — Full-Stack JavaScript Framework based on PHP Yii 2 Framework architecture. * * @author Vladimir Kozhin <affka@affka.ru> * @license MIT */ 'use strict'; const neatness = require('neatness'); const _isString = require('lodash/isString'); const _isFunction = require('lodash/isFunction'); const _isUndefined = require('lodash/isUndefined'); const _has = require('lodash/has'); const _each = require('lodash/each'); const _indexOf = require('lodash/indexOf'); const _isObject = require('lodash/isObject'); const _clone = require('lodash/clone'); const _drop = require('lodash/drop'); const _isEmpty = require('lodash/isEmpty'); const _isArray = require('lodash/isArray'); const _trimEnd = require('lodash/trimEnd'); const _upperFirst = require('lodash/upperFirst'); const _extend = require('lodash/extend'); const BaseJii = require('./BaseJii'); const WebApplication = require('./application/WebApplication'); const InvalidParamException = require('./exceptions/InvalidParamException'); const ApplicationException = require('./exceptions/ApplicationException'); const InvalidConfigException = require('./exceptions/InvalidConfigException'); const BaseObject = require('./base/BaseObject'); const Context = require('./base/Context'); const Component = require('./base/Component'); const Util = require('./helpers/Util'); /** * @class Jii * @extends BaseJii */ var Jii = _extend(BaseJii, /** @lends Jii */{ /** * @type {Application} */ app: null, /** * @type {object} */ aliases: null, _contextConfig: null, /** * Create web application, which available by Jii.app * @param {object} config * @returns {Application} */ createWebApplication(config) { return this.createApplication(WebApplication, config); }, /** * Main method which create application by class name and config. * @param {string} className * @param {object} config * @returns {Application} */ createApplication(className, config) { config = config || {}; config.application = config.application || {}; // Save context config this._contextConfig = config.context || {}; var ApplicationClass = this.namespace(className); if (!_isFunction(ApplicationClass)) { throw new InvalidParamException('Not found application class: ' + className); } // Init aliases this.aliases = {}; if (this.isNode) { this.aliases['@jii'] = __dirname; } // Create application instance return new ApplicationClass(config.application); }, /** * Main method which create application by class name and config. * @param {object} config * @returns {Context} */ createContext(config) { config = config || {}; // Merge with default context config config = this.mergeConfigs(this._contextConfig || {}, config); if (config.className) { var ContextClass = this.namespace(config.className); if (!_isFunction(ContextClass)) { throw new InvalidParamException('Not found context class: ' + className); } return new ContextClass(config); } return new Context(config); }, /** * Translates a path alias into an actual path. * * The translation is done according to the following procedure: * * 1. If the given alias does not start with '@', it is returned back without change; * 2. Otherwise, look for the longest registered alias that matches the beginning part * of the given alias. If it exists, replace the matching part of the given alias with * the corresponding registered path. * 3. Throw an exception or return false, depending on the `$throwException` parameter. * * For example, by default '@jii' is registered as the alias to the Jii framework directory, * say '/path/to/jii'. The alias '@jii/web' would then be translated into '/path/to/jii/web'. * * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. * This is because the longest alias takes precedence. * * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced * instead of '@foo/bar', because '/' serves as the boundary character. * * Note, this method does not check if the returned path exists or not. * * @param {string} alias the alias to be translated. * @param {boolean} [throwException] whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. * @return {string|boolean} the path corresponding to the alias, false if the root alias is not previously registered. * @throws {InvalidParamException} if the alias is invalid while throwException is true. * @see setAlias() */ getAlias(alias, throwException) { if (_isUndefined(throwException)) { throwException = true; } if (alias.indexOf('@') !== 0) { return alias; } var index = alias.indexOf('/'); var root = index === -1 ? alias : alias.substr(0, index); if (_has(this.aliases, root)) { if (_isString(this.aliases[root])) { return this.aliases[root] + (index !== -1 ? alias.substr(index) : ''); } var finedPath = null; _each(this.aliases[root], (path, name) => { var testAlias = alias + '/'; if (testAlias.indexOf(name + '/') === 0) { finedPath = path + alias.substr(name.length); return false; } }); if (finedPath !== null) { return finedPath; } } if (throwException) { throw new InvalidParamException('Invalid path alias: ' + alias); } return false; }, /** * Returns the root alias part of a given alias. * A root alias is an alias that has been registered via [[setAlias()]] previously. * If a given alias matches multiple root aliases, the longest one will be returned. * @param {string} alias the alias * @return {string|boolean} the root alias, or false if no root alias is found */ getRootAlias(alias) { var index = alias.indexOf('/'); var root = index === -1 ? alias : alias.substr(0, index); if (_has(this.aliases, root)) { if (_isString(this.aliases[root])) { return root; } var finedPath = null; _each(this.aliases[root], (path, name) => { if (_indexOf(alias + '/', name + '/') === 0) { finedPath = name; return false; } }); if (finedPath !== null) { return finedPath; } } return false; }, /** * Registers a path alias. * * A path alias is a short name representing a long path (a file path, a URL, etc.) * For example, we use '@jii' as the alias of the path to the Jii framework directory. * * A path alias must start with the character '@' so that it can be easily differentiated * from non-alias paths. * * Note that this method does not check if the given path exists or not. All it does is * to associate the alias with the path. * * Any trailing '/' and '\' characters in the given path will be trimmed. * * @param {string} alias the alias name (e.g. "@jii"). It must start with a '@' character. * It may contain the forward slash '/' which serves as boundary character when performing * alias translation by [[getAlias()]]. * @param {string} path the path corresponding to the alias. Trailing '/' and '\' characters * will be trimmed. This can be * * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a URL (e.g. `http://example.com`) * - a path alias (e.g. `@jii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias()]]. * * @throws {InvalidParamException} if $path is an invalid alias. * @see getAlias() */ setAlias(alias, path) { if (alias.indexOf('@') !== 0) { alias = '@' + alias; } var index = alias.indexOf('/'); var root = index === -1 ? alias : alias.substr(0, index); if (path !== null) { path = alias.indexOf('@') !== 0 ? _trimEnd(path, '/') : this.getAlias(path); if (!_has(this.aliases, root)) { if (index === -1) { this.aliases[root] = path; } else { this.aliases[root] = {}; this.aliases[root][alias] = path; } } else if (_isString(this.aliases[root])) { if (index === -1) { this.aliases[root] = path; } else { var oldPath = this.aliases[root]; this.aliases[root] = {}; this.aliases[root][alias] = path; this.aliases[root][root] = oldPath; } } else { this.aliases[root][alias] = path; //krsort(static::$aliases[$root]); } } else if (_has(this.aliases, root)) { if (_isObject(this.aliases[root])) { delete this.aliases[root][alias]; } else if (index === -1) { delete this.aliases[root]; } } }, /** * Creates a new instance using the given configuration. * @param {*} config Class name or object with param `className` * @returns {object} */ createObject(config) { var className = null; var objectClass = null; // Normalize config if (_isFunction(config)) { objectClass = config; config = {}; } else { if (_isString(config)) { className = config; config = {}; } else if (_has(config, 'className')) { className = config.className; config = _clone(config); delete config.className; } else { throw new ApplicationException('Wrong configuration for create object.'); } // Get class objectClass = this.namespace(className); if (!_isFunction(objectClass)) { throw new ApplicationException('Not found class `' + className + '` for create instance.'); } } // Arguments for constructor of class var args = [objectClass]; args = args.concat(_drop(arguments)); if (!_isEmpty(config)) { args.push(config); } return new (objectClass.bind.apply(objectClass, args))(); }, /** * Set configuration to object. This method find public param in object or setter method. * You can not use setter and public param at the same time for safety reason * @param {Object} object Class instance * @param {object} config Configuration object {key: value, ..} */ configure(object, config) { for (var key in config) { if (!config.hasOwnProperty(key)) { continue; } if (object instanceof Component) { object.set(key, config[key]); continue; } // Generate setter name var setter = 'set' + _upperFirst(key); if (!_isFunction(object[setter])) { if (_isFunction(object[key])) { throw new InvalidConfigException('You can not replace from config function `' + key + '` in object `' + object.className() + '`.'); } if (_isUndefined(object[key])) { throw new InvalidConfigException('Config param `' + key + '` is undefined in object `' + object.className() + '`.'); } } if (!_isUndefined(object[key]) && !_isFunction(object[key]) && _isFunction(object[setter])) { throw new InvalidConfigException('You have two setters (function and public param) for config param `' + key + '` in object `' + object.className() + '`. Please change param access (to `_' + key + '`) or remove setter method.'); } if (!_isUndefined(object[key]) && !_isFunction(object[key])) { if (Util.isStrictObject(object[key]) && Util.isStrictObject(config[key])) { object[key] = Jii.mergeConfigs(object[key], config[key]); } else { object[key] = config[key]; } } else if (_isFunction(object[setter])) { object[setter].call(object, config[key]); } } }, /** * Logs a trace message. * Trace messages are logged mainly for development purpose to see * the execution work flow of some code. * @param {string} message the message to be logged. * @param {string} [category] the category of the message. */ trace(message, category) { category = category || ''; if (this.debug) { console.log(String(new Date()) + ' TRACE ' + message); } //if (YII_DEBUG) { // @todo static.getLogger().log(message, Logger.LEVEL_TRACE, category); //} }, /** * Logs an error message. * An error message is typically logged when an unrecoverable error occurs * during the execution of an application. * @param {string} message the message to be logged. * @param {string} [category] the category of the message. */ error(message, category) { category = category || ''; console.error(String(new Date()) + ' ERROR ' + message); // @todo static.getLogger().log(message, Logger.LEVEL_ERROR, category); }, /** * Logs a warning message. * A warning message is typically logged when an error occurs while the execution * can still continue. * @param {string} message the message to be logged. * @param {string} [category] the category of the message. */ warning(message, category) { category = category || ''; console.warn(String(new Date()) + ' WARNING ' + message); // @todo static.getLogger().log(message, Logger.LEVEL_WARNING, category); }, /** * Logs an informative message. * An informative message is typically logged by an application to keep record of * something important (e.g. an administrator logs in). * @param {string} message the message to be logged. * @param {string} [category] the category of the message. */ info(message, category) { category = category || ''; console.info(String(new Date()) + ' INFO ' + message); // @todo static.getLogger().log(message, Logger.LEVEL_INFO, category); }, /** * Short alias for translate texts * @param {string} category * @param {string} [message] * @param {object} [params] * @param {string|null} [language] * @returns {*} */ t(category, message, params = {}, language = null) { if (this.app !== null && this.app.hasComponent('i18n')) { return this.app.getComponent('i18n').translate(category, message, params, language || this.app.language); } _each(params, (value, key) => { message = message.replace(new RegExp('\{' + key + '\}', 'gi'), value); }); return message; }, /** * @param {object...} [obj] * @returns {object} */ mergeConfigs(obj) { var dst = {}; for (var i = 0, l = arguments.length; i < l; ++i) { obj = arguments[i]; if (!_isObject(obj)) { continue; } // Convert class name to object for (var key in obj) { if (obj.hasOwnProperty(key)) { if (_isObject(obj[key]) && !_isArray(obj[key]) && !_isFunction(obj[key]) && !(obj instanceof BaseObject)) { dst[key] = this.mergeConfigs(dst[key], obj[key]); } else { dst[key] = obj[key]; } } } } return dst; }, catchHandler(e) { console.error(e.stack || e) } }); /** * @module Jii */ module.exports = Jii;