diff --git a/.babelrc b/.babelrc index 2aec121..9aeec14 100644 --- a/.babelrc +++ b/.babelrc @@ -2,8 +2,21 @@ "presets": ["es2015", "stage-2"], "plugins": [ ["module-resolver", { - "root": ["./src"], - "alias": { "commands": "./commands" } + "root": ["./lib"], + "alias": { + "bin": "bin", + "bundle": "bundle", + "clean": "clean", + "help": "help", + "util": "util", + "visitors": "visitors", + "visualize": "visualize" + } }] - ] + ], + "env": { + "test": { + "plugins": ["istanbul"] + } + } } diff --git a/.gitignore b/.gitignore index 6ad53d1..a0d231d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Project -/bin -lib +.nyc_output coverage node_modules diff --git a/.istanbul.yml b/.istanbul.yml index 10e292d..02b328d 100644 --- a/.istanbul.yml +++ b/.istanbul.yml @@ -1,5 +1,6 @@ instrumentation: root: src + include-all-sources: true extensions: - .es6 - .js diff --git a/CHANGELOG.md b/CHANGELOG.md index ac35c19..efe4ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1 @@ ## Rinoa v1.0.0 - ROADMAP - -##### Features: -* [ ] Esprima ES6 AST Parsing - * [ ] Imports / Exports - * [ ] Annotations -* [ ] Output formats - * [ ] UMD (Global, AMD, CommonJS) - * [ ] AMD - * [ ] CommonJS - * [ ] ES -* [ ] Multiple Bundles diff --git a/README.md b/README.md index 377b058..f758e2c 100644 --- a/README.md +++ b/README.md @@ -3,31 +3,58 @@ [![Build Status](https://travis-ci.org/nahuelio/squarebox.svg?branch=master)](https://travis-ci.org/nahuelio/squarebox) [![Coverage Status](https://coveralls.io/repos/github/nahuelio/squarebox/badge.svg)](https://coveralls.io/github/nahuelio/squarebox) +[![Version](https://img.shields.io/badge/Version-1.0.0-blue.svg?style=flat)]() +[![Version](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://www.opensource.org/licenses/mit-license.php) +[![GitHub stars](https://img.shields.io/github/stars/nahuelio/squarebox.svg?style=social&label=Stars)]() + +#### Introduction + +Experimental ES6/CommonJS/AMD Module Bundler + +#### Installation + +```npm install [-g] squarebox``` + +#### Usage + +CLI Commands ``` -Badges +global option: -c (sqbox.js|.sqboxrc|URL) | Default: ./.sqboxrc +contextual help - sqbox [command] help + +* sqbox help - global help (list of commands and general usage) +* sbox bundle --options +* sbox clean +* sbox visualize ``` -#### Introduction +Programmatic API ``` -TODO +const sqbox = require('squarebox'); +sqbox.clean([opts]) + .bundle([config]) + .visualize([opts]); ``` -#### Installation +#### Official Documentation ``` TODO ``` -#### Usage +#### Configuration + ``` TODO ``` #### Contribute + ``` TODO ``` -Built with :heart: by [NahuelIO](http://nahuel.io) - [MIT License](http://www.opensource.org/licenses/mit-license.php) +[![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://nahuel.io) +by [Nahuel IO](http://nahuel.io) diff --git a/bin/sqbox b/bin/sqbox new file mode 100644 index 0000000..4a3f017 --- /dev/null +++ b/bin/sqbox @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +require('babel-register')({ + "presets": ["es2015", "stage-2"], + "plugins": [ + ["module-resolver", { + "root": ["../"], + "cwd": __dirname, + "alias": { + "bin": "bin", + "bundle": "bundle", + "clean": "clean", + "help": "help", + "util": "util", + "visitors": "visitors", + "visualize": "visualize" + } + }] + ] +}); +require('babel-polyfill'); +require('commands/bin/sqbox').run(__dirname); diff --git a/lib/bin/commands.json b/lib/bin/commands.json new file mode 100644 index 0000000..e4301ca --- /dev/null +++ b/lib/bin/commands.json @@ -0,0 +1,49 @@ +[{ + "name": "help", + "aliases": ["help"], + "description": "Help", + "path": "help/help", + "options": { + "clean": {}, + "bundle": {}, + "graph": {} + } +}, { + "name": "clean", + "aliases": ["clean"], + "description": "Clean output directory", + "path": "clean/clean", + "options": { + "override": { "default": false }, + "config": { "default": ".sqboxrc" }, + "url": {}, + "target": { "default": "./dist" } + } +}, { + "name": "bundle", + "aliases": ["bundle"], + "description": "Module Bundler", + "path": "bundle/bundle", + "options": { + "override": { "default": false }, + "config": { "default": ".sqboxrc" }, + "url": { "alias": "u" }, + "scan": { "alias": "s", "default": "." }, + "exclude": { "alias": "x" }, + "extensions": { "alias": "e", "default": ".js,.jsx,.es6,.es" }, + "alias": { "alias": "a", "default": {} }, + "target": { "alias": "t", "default": "dist>umd:./dist" }, + "logLevel": { "alias": "lv", "default": "output" } + } +}, { + "name": "graph", + "aliases": ["graph"], + "description": "Generates a graphical representation of your bundles", + "path": "visualize/graph", + "options": { + "override": { "default": false }, + "config": { "default": ".sqboxrc" }, + "url": {}, + "target": { "default": "./dist" } + } +}] diff --git a/lib/bin/sqbox.es6 b/lib/bin/sqbox.es6 new file mode 100644 index 0000000..8624bf0 --- /dev/null +++ b/lib/bin/sqbox.es6 @@ -0,0 +1,152 @@ +/** +* @module bin +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import 'util/mixins'; +import extend from 'extend'; +import Collection from 'util/adt/collection'; +import Factory from 'util/factory/factory'; +import Command from 'command'; +import CommandsList from './commands.json'; +import logger from 'util/logger/logger'; + +let enforcer = Symbol('SquareBox'); + +/** +* Class SquareBox +* @extends {Command} +* +* @uses {bin.visitor.Commander} +**/ +class SquareBox extends Command { + + /** + * Constructor + * @public + * @param {Object} [args = {}] - Constructor arguments + * @return {bin.SquareBox} + **/ + constructor(...args) { + super(...args); + return SquareBox.isPrivate(this.attachEvents()); + } + + /** + * Attaches Events + * @public + * @return {bin.SquareBox} + **/ + attachEvents() { + this.once(SquareBox.events.done, this.after); + return this; + } + + /** + * Before Run + * @public + * @override + * @return {bin.SquareBox} + **/ + before() { + super.before(); + this.commander.read(); + return this; + } + + /** + * Run + * @public + * @override + * @return {bin.SquareBox} + **/ + run() { + this.before(); + this.configuration.parse().then(_.bind(this.onConfiguration, this)); + return this; + } + + /** + * Configuration Loaded and Parsed Handler + * @public + * @param {visitors.Configuration} configuration - configuration visitor reference + * @return {bin.SquareBox} + **/ + onConfiguration() { + const { path } = this.options; + //console.log(_.pick(this, Command.options)); + // TODO: Factory.get(path, this.params()).run(); + return this; + } + + /** + * Constructor Validation + * @public + * @throws {Error} Private violation + * @param {Symbol} pte - constructor enforcer + * @return {bin.SquareBox} + **/ + isPrivate(pte) { + if(!_.isEqual(pte, enforcer)) throw new Error('Private Violation'); + return this; + } + + /** + * Register Command Factories + * @public + * @override + * @return {bin.SquareBox} + **/ + register() { + super.register(); + Factory.registerAll(this.constructor.commands); + return this; + } + + /** + * Available Commands + * @static + * @type {util.adt.Collection} + **/ + static commands = Collection.new(CommandsList); + + /** + * SquareBox visitors + * @static + * @override + * @type {Array} + **/ + static visitors = [ + 'visitors/commander', + 'visitors/configuration' + ].concat(Command.visitors); + + /** + * Static enforcer validation + * @static + * @param {bin.SquareBox} instance - squarebox instance reference + * @return {bin.SquareBox} + **/ + static isPrivate(instance) { + return instance.isPrivate(enforcer); + } + + /** + * Command options + * @static + * @type {Array} + **/ + static options = Command.options.concat(['options']); + + /** + * Static Run + * @static + * @param {String} [cwd = process.cwd()] - base path + * @return {bin.SquareBox} + **/ + static run(cwd = process.cwd()) { + return this.new({ cwd }).run(); + } + +} + +export default SquareBox; diff --git a/lib/bundle/bundle.es6 b/lib/bundle/bundle.es6 new file mode 100644 index 0000000..5f228bc --- /dev/null +++ b/lib/bundle/bundle.es6 @@ -0,0 +1,29 @@ +/** +* @module bundle +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Command from 'command'; + +/** +* Class Bundle +* @extends {Command} +**/ +class Bundle extends Command { + + /** + * Run + * @public + * @override + * @return {bundle.Bundle} + **/ + run() { + // TODO + console.log('Bundle.run()...'); + return super.run(); + } + +} + +export default Bundle; diff --git a/lib/clean/clean.es6 b/lib/clean/clean.es6 new file mode 100644 index 0000000..0f9eaea --- /dev/null +++ b/lib/clean/clean.es6 @@ -0,0 +1,29 @@ +/** +* @module clean +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Command from 'command'; + +/** +* Class Clean +* @extends {Command} +**/ +class Clean extends Command { + + /** + * Run + * @public + * @override + * @return {clean.Clean} + **/ + run() { + // TODO + console.log('Clean.run()...'); + return super.run(); + } + +} + +export default Clean; diff --git a/lib/command.es6 b/lib/command.es6 new file mode 100644 index 0000000..4dedf69 --- /dev/null +++ b/lib/command.es6 @@ -0,0 +1,245 @@ +/** +* @module +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { EventEmitter } from 'events'; +import _ from 'util/mixins'; +import extend from 'extend'; +import Factory from 'util/factory/factory'; +import Visited from 'util/visitor/visited'; + +/** +* Class Command +* @extends {util.visitor.Visited} +* +* @uses {visitors.formatter.Json} +* @uses {visitors.async.Asynchronous} +**/ +class Command extends Visited { + + /** + * Constructor + * @public + * @param {Object} [args = {}] - constructor arguments + * @return {Command} + **/ + constructor(args = {}) { + super(); + return this.settings(args).register().acceptAll(); + } + + /** + * Set settings + * @see {visitors.Configuration} + * @public + * @param {Object} [options = {}] - command options + * @return {Command} + **/ + settings(options) { + return extend(true, this, this.constructor.defaults, _.pick(options, this.constructor.options)); + } + + /** + * Registers Visitors + * @public + * @return {Command} + **/ + register() { + Factory.basePath(this.dirname).registerAll(this.constructor.visitors); + return this; + } + + /** + * Accepts All Visitors + * @public + * @return {command.Command} + **/ + acceptAll() { + return _.reduce(this.constructor.visitors, (memo, v) => memo.accept(Factory.get(v, this)), this); + } + + /** + * Proxified asynchronous next strategy + * FIXME: Implement when the command gets rejected. + * @public + * @override + * @param adt {util.proxy.Asynchronous} adt used for asynchronous operations + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @return {Promise} + **/ + next(adt, resolve, reject) { + this.once(Command.events.done, resolve); + return this.run(); + } + + /** + * Default hook before run + * @public + * @return {Command} + **/ + before() { + return this.pending(); + } + + /** + * Default Run + * @public + * @return {Command} + **/ + run() { + return this.before().after(); + } + + /** + * Default hook after run + * @public + * @return {Command} + **/ + after() { + return this.done(); + } + + /** + * Command Pending exeuction state + * @public + * @return {command.Command} + **/ + pending() { + this.emit(Command.events.pending, this); + return this; + } + + /** + * Command Done exeuction state + * @public + * @return {command.Command} + **/ + done() { + this.emit(Command.events.done, this); + return this; + } + + /** + * Retrieves source + * @public + * @return {Object} + **/ + source() { + return this.source; + } + + /** + * Retrieves and resolves scan directory (glob) + * @public + * @return {String} + **/ + scan() { + return this.source().scan; + } + + /** + * Retrieves and resolves excluded folders + * @public + * @return {Array} + **/ + exclude() { + return this.source().exclude; + } + + /** + * Retrieves list of extensions to scan + * @public + * @return {Array} + **/ + extensions() { + return this.source().extensions; + } + + /** + * Retrieves aliases for modules + * @public + * @return {Object} + **/ + alias() { + return this.source().alias; + } + + /** + * Retrieves target + * @public + * @return {Object} + **/ + target() { + return this.target; + } + + /** + * Retrieve targets by using a given predicate passed by parameter + * @public + * @param {Function} predicate - predicate to walk over the targets + * @return {Array} + **/ + targets(predicate) { + return _.defined(predicate) && _.isFunction(predicate) ? _.map(this.target, predicate, this) : []; + } + + /** + * Retrieves command options + * @public + * @return {Object} + **/ + getOptions() { + return this.options; + } + + /** + * Command Visitors + * @static + * @type {Array} + **/ + static visitors = [ + 'visitors/formatter/json', + 'visitors/async/async' + ]; + + /** + * Command Defaults + * @static + * @type {Object} + **/ + static defaults = { + env: 'development', + dirname: __dirname, + cwd: process.cwd() + }; + + /** + * Command options + * @static + * @type {Array} + **/ + static options = [ + 'env', + 'dirname', + 'cwd', + 'scan', + 'exclude', + 'extensions', + 'alias', + 'target' + ]; + + /** + * Command Events + * @static + * @type {Object} + **/ + static events = { + pending: 'commands:command:pending', + done: 'commands:command:done' + }; + +} + +export default Command; diff --git a/lib/help/help.es6 b/lib/help/help.es6 new file mode 100644 index 0000000..51b7639 --- /dev/null +++ b/lib/help/help.es6 @@ -0,0 +1,29 @@ +/** +* @module help +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Command from 'command'; + +/** +* Class Clean +* @extends {Command} +**/ +class Clean extends Command { + + /** + * Run + * @public + * @override + * @return {help.Clean} + **/ + run() { + // TODO + console.log('Help.run()...'); + return super.run(); + } + +} + +export default Clean; diff --git a/src/commands/util/adt/collection.es6 b/lib/util/adt/collection.es6 similarity index 63% rename from src/commands/util/adt/collection.es6 rename to lib/util/adt/collection.es6 index 992b8e5..1beed2c 100644 --- a/src/commands/util/adt/collection.es6 +++ b/lib/util/adt/collection.es6 @@ -1,42 +1,28 @@ /** -* @module commands.util.adt +* @module util.adt * @author Patricio Ferreira <3dimentionar@gmail.com> **/ import { EventEmitter } from 'events'; import _ from 'underscore'; import extend from 'extend'; -import Iterator from './iterator'; +import Iterator from 'util/adt/iterator'; /** * Class Collection * @extends events.EventEmitter **/ -export default class Collection extends EventEmitter { - - /** - * Internal Array - * @private - * @type {Array} - **/ - _collection = []; - - /** - * Interface for objects - * @private - * @type {Function} - **/ - _interface = null; +class Collection extends EventEmitter { /** * Constructor * @public * @param [initial = []] {Array} Initial Array * @param [opts = {}] {Object} collection options - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ constructor(initial = [], opts = {}) { super(); - Collection._aggregate(extend(true, this, { _interface: opts.interface })) + extend(true, this, _.omit(opts, 'silent', 'interface'), { _collection: [], _interface: opts.interface }); return (initial.length > 0) ? this.set(initial) : this; } @@ -57,10 +43,11 @@ export default class Collection extends EventEmitter { * @param name {String} event name * @param [opts = {}] {Object} additional options * @param [...args] {Any} additional arguments to pass to the emit - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ - _fire(name, opts = {}, ...args) { - return !opts.silent ? this.emit(name, this, ...args) : this; + _fire(name, opts, ...args) { + if(!opts.silent) this.emit(name, this, ...args); + return this; } /** @@ -76,16 +63,28 @@ export default class Collection extends EventEmitter { return (this.hasInterface() && !_.instanceOf(e, this._interface)) ? w() : wo(); } + /** + * Returns a json representation of a given element only if this collection has defined an interface + * and the given element implements {util.proxy.Json} interface + * @private + * @param element {Any} element to get json representation + * @return {Any} + **/ + _toJSON(element) { + return (this.hasInterface() && element.toJSON) ? element.toJSON() : element; + } + /** * Default instanciation strategy for new elements added in this collection * @private * @param e {Any} element to instanciate - * @param [opts = {}] {Object} additional options + * @param opts {Object} additional options * @return {Any} **/ - _new(e, opts = {}) { - if(!this._valid(e)) return null; - return this._when(e, () => _.defined(opts.new) ? opts.new(e) : new this._interface(e), () => e); + _new(e, opts) { + return this._when(e, + () => _.defined(opts.new && !this.hasInterface()) ? opts.new(e) : new this._interface(e), + () => e); } /** @@ -94,7 +93,7 @@ export default class Collection extends EventEmitter { * @fires {Collection.events.set} * @param col {Array} collection of new elements * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ set(col, opts = {}) { if(!this._valid(col) || !_.isArray(col)) return this; @@ -112,9 +111,11 @@ export default class Collection extends EventEmitter { * @return {Any} **/ add(element, opts = {}) { - this._collection.push(this._new(element, opts)); - this._fire(Collection.events.add, opts, element); - return element; + if(!this._valid(element)) return null; + let added = this._new(element, opts); + this._collection.push(added); + this._fire(Collection.events.add, opts, added); + return added; } /** @@ -123,11 +124,12 @@ export default class Collection extends EventEmitter { * @fires {Collection.events.addall} * @param [col = []] {Array} collection of new elements to add * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ addAll(col = [], opts = {}) { - _.each(col, (e) => this.add(e, extend(true, opts, { silent: true }))); - return this._fire(Collection.events.addall, opts); + if(!_.isArray(col) || col.length === 0) return this; + let added = _.map(col, (e) => this.add(e, extend(true, {}, opts, { silent: true }))); + return this._fire(Collection.events.addall, opts, added); } /** @@ -148,7 +150,7 @@ export default class Collection extends EventEmitter { **/ contains(element) { if(!this._valid(element)) return false; - return this.some((e) => _.isEqual(e, element)); + return this.some((e) => _.isEqual(this._toJSON(e), element)); } /** @@ -158,7 +160,7 @@ export default class Collection extends EventEmitter { * @return {Boolean} **/ containsAll(elements = []) { - if(!this._valid(elements)) return false; + if(!this._valid(elements) || elements.length === 0) return false; return _.every(_.map(elements, this.contains, this)); } @@ -174,18 +176,35 @@ export default class Collection extends EventEmitter { } /** - * Removes an element at the given index. If no index is passed, this method will remove the last element in + * Removes an element at the given index. If no index is passed, this method will remove the first element in * this collection. * @public * @fires {Collection.events.remove} - * @param [ix = (this._collection.size() - 1)] {Number} index used to remove + * @param [ix = 0] {Number} index used to remove * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Collection} + * @return {Number} **/ - remove(ix = (this._collection.size() - 1), opts = {}) { - if(!this._valid(ix) || !_.isNumber(ix) || ix > (this.size() - 1)) return this; + removeAt(ix = 0, opts = {}) { + if(!this._valid(ix) || !_.isNumber(ix) || ix > (this.size() - 1)) return null; this._collection.splice(ix, 1); - return this._fire(Collection.events.remove, opts); + this._fire(Collection.events.remove, opts, ix); + return ix; + } + + /** + * Remove a given element + * @public + * @fires {Collection.events.remove} + * @param element {Any} element to remove + * @param [opts = {}] {Object} additional options + * @return {Number} + **/ + remove(element, opts = {}) { + if(!this._valid(element)) return null; + let ix = this.findIndex((e) => _.isEqual(this._toJSON(e), this._toJSON(element))); + if(ix === -1) return null; + this._fire(Collection.events.remove, opts, this._collection.splice(ix, 1)); + return element; } /** @@ -194,21 +213,29 @@ export default class Collection extends EventEmitter { * @fires {Collection.events.removeall} * @param [col = []] {Array} collection of elements to remove * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ removeAll(col = [], opts = {}) { - - return this._fire(Collection.events.removeall, opts); + if(!_.isArray(col) || col.length === 0) return this; + let removed = _.map(col, (e) => this.remove(e, extend(true, {}, opts, { silent: true }))); + return this._fire(Collection.events.removeall, opts, _.compact(removed)); } /** * Removes elements by a given predicate * @public + * @fires {Collection.events.remove} * @param predicate {Function} predicate used to evaluate - * @return {commands.util.adt.Collection} + * @param [opts = {}] {Object} additional options + * @return {util.adt.Collection} **/ - removeBy() { - // TODO + removeBy(predicate, opts = {}) { + if(!this._valid(predicate) || !_.isFunction(predicate)) return this; + for(var i = 0, len = this.size(); i < len; i++) { + if(predicate(this._collection[i], i, this._collection)) { + this.removeAt(i, opts); i--; len--; + } + } return this; } @@ -219,17 +246,19 @@ export default class Collection extends EventEmitter { * @fires {Collection.events.sort} * @param [opts = {}] {Object} additional options * @param comparator {Function} comparator reference - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ sort(comparator, opts = {}) { - // TODO + (!_.defined(comparator) || !_.isFunction(comparator)) ? + this._collection.sort() : + this._collection.sort(comparator); return this._fire(Collection.events.sort, opts); } /** * Returns a new iterator instance of this collection * @public - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ iterator() { return Iterator.new(this._collection); @@ -240,10 +269,10 @@ export default class Collection extends EventEmitter { * @public * @fires {Collection.events.reset} * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ reset(opts = {}) { - // TODO + this._collection = []; return this._fire(Collection.events.reset, opts); } @@ -292,45 +321,45 @@ export default class Collection extends EventEmitter { /** * @event set **/ - set: 'commands:util:adt:collection:set', + set: 'util:adt:collection:set', /** * @event add **/ - add: 'commands:util:adt:collection:add', + add: 'util:adt:collection:add', /** * @event addall **/ - addall: 'commands:util:adt:collection:addall', + addall: 'util:adt:collection:addall', /** * @event remove **/ - remove: 'commands:util:adt:collection:remove', + remove: 'util:adt:collection:remove', /** * @event removeall **/ - removeall: 'commands:util:adt:collection:removeall', + removeall: 'util:adt:collection:removeall', /** * @event reset **/ - reset: 'commands:util:adt:collection:reset', + reset: 'util:adt:collection:reset', /** * @event sort **/ - sort: 'commands:util:adt:collection:sort' + sort: 'util:adt:collection:sort' }; /** * Underscore interface methods for aggregation * @static - * @type Array + * @return {Array} **/ - static UNDERSCORE = [ + static UNDERSCORE = () => [ 'each', 'map', 'findWhere', @@ -369,27 +398,29 @@ export default class Collection extends EventEmitter { /** * Underscore aggregation * @static - * @param instance {commands.util.adt.Collection} - * @return {commands.util.adt.Collection} + * @param instance {util.adt.Collection} + * @return {util.adt.Collection} **/ - static _aggregate(instance) { - _.each(this.UNDERSCORE, function(method) { - if(_[method] && !_.defined(instance[method])) { - instance[method] = _.bind(function() { - return _[method].apply(this, [this._collection].concat(_.toArray(arguments))); - }, instance); - } + static _aggregate() { + _.each(this.UNDERSCORE(), function(method) { + if(!_[method] || _.defined(this.prototype[method])) return; + this.prototype[method] = function() { + return _[method].apply(this, [this._collection].concat(_.toArray(arguments))); + }; }, this); + return this; } /** * Static constructor * @static * @param [...args] {Any} Constructor arguments - * @return {commands.util.adt.Collection} + * @return {util.adt.Collection} **/ static new(...args) { return new this(...args); } } + +export default Collection._aggregate(); diff --git a/src/commands/util/adt/iterator.es6 b/lib/util/adt/iterator.es6 similarity index 86% rename from src/commands/util/adt/iterator.es6 rename to lib/util/adt/iterator.es6 index 8855ccf..1c73528 100644 --- a/src/commands/util/adt/iterator.es6 +++ b/lib/util/adt/iterator.es6 @@ -1,5 +1,5 @@ /** -* @module commands.util.adt +* @module util.adt * @author Patricio Ferreira <3dimentionar@gmail.com> **/ import { EventEmitter } from 'events'; @@ -9,7 +9,7 @@ import _ from 'underscore'; * Class Iterator * @extends events.EventEmitter **/ -export default class Iterator extends EventEmitter { +class Iterator extends EventEmitter { /** * Internal collection @@ -29,7 +29,7 @@ export default class Iterator extends EventEmitter { * Constructor * @public * @param [initial = []] {Array} initial elements in the collection - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ constructor(initial = []) { super(); @@ -53,7 +53,7 @@ export default class Iterator extends EventEmitter { * @public * @param [col = []] {Array} elements to set to this iterator * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ set(col = [], opts = {}) { if(!this._valid(col) || col.length === 0) return this; @@ -82,7 +82,7 @@ export default class Iterator extends EventEmitter { /** * Reset the cursor to the beginning to the index 0 * @public - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ rewind() { this._pointer = 0; @@ -94,7 +94,7 @@ export default class Iterator extends EventEmitter { * @public * @fires {Iterator.events.remove} * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ remove(opts = {}) { if(this._collection.length === 0) return this; @@ -111,22 +111,24 @@ export default class Iterator extends EventEmitter { /** * @event set **/ - set: 'commands:util:adt:iterator:set', + set: 'util:adt:iterator:set', /** * @event remove **/ - remove: 'commands:util:adt:iterator:remove' + remove: 'util:adt:iterator:remove' } /** * Static Constructor * @static * @param [...args] {Any} constructor arguments - * @return {commands.util.adt.Iterator} + * @return {util.adt.Iterator} **/ static new(...args) { return new this(...args); } } + +export default Iterator; diff --git a/lib/util/adt/queue-async.es6 b/lib/util/adt/queue-async.es6 new file mode 100644 index 0000000..f485e3c --- /dev/null +++ b/lib/util/adt/queue-async.es6 @@ -0,0 +1,135 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Queue from 'util/adt/queue'; +import Asynchronous from 'visitors/async/async'; + +/** +* Class QueueAsync +* Defines the interface of an asynchronous FIFO Queue (FirstIn-FirstOut). +* Interface for objects on this queue, **must** implement method `next` of +* {@link visitors.async.Asynchronous} for promise resolution. +* @example +*
Usage
+* +* let myqueue = QueueAsync.new([1,2,3], { capacity: 3, interface: [Class] }) +* .on(QueueAsync.events.next, (element) => console.log('Next: ', element)) +* .on(QueueAsync.events.end, (results) => console.log('End: ', results)) +* const mypromise = p.poll(); // asynchronous +* @extends util.adt.Queue +**/ +class QueueAsync extends Queue { + + /** + * Constructor + * @public + * @override + * @param {Array} [initial = []] - initial elements + * @param {Object} [opts = {}] - collection options + * @return {util.adt.QueueAsync} + **/ + constructor(initial = [], opts = {}) { + return super(initial, extend(true, opts, { _visitor: Asynchronous.new(), _last: [] })); + } + + /** + * Default instanciation strategy for new elements added in this collection + * @private + * @override + * @param {Any} e - element to instanciate + * @param {Object} opts - additional options + * @return {Any} + **/ + _new(e, opts) { + let element = super._new(e, opts); + this._visitor.validate(element); + return element.accept(this._visitor); + } + + /** + * Resets Last Results + * @public + * @return {util.adt.QueueAsync} + **/ + _resetLast() { + this._last = []; + return this; + } + + /** + * Retrieves and removes the head of this queue, or returns null if this queue is empty + * @public + * @async + * @override + * @param {Object} [opts = {}] - additional options + * @param {Boolean} [next = false] - async queue already started + * @return {Promise} + **/ + async poll(opts = {}, next = false) { + if(!next) this._resetLast(); + const res = await this.next(opts); + return this.onNext(res, opts); + } + + /** + * Asynchronous Queue next + * @public + * @emits {QueueAsync.events.next} - when opts.silent is false or undefined + * @param {Object} [opts] - additional options + * @return {Promise} + **/ + next(opts) { + let element = super.poll(opts); + if(!opts.silent) this.emit(QueueAsync.events.next, element); + return element.execute(this); + } + + /** + * Retrieves and removes the head of this queue, or returns null if this queue is empty + * @public + * @param {Promise} res - current promise (resolved or rejected) + * @param {Object} [opts] - additional options + * @return {Any} + **/ + onNext(res, opts) { + this._last.push(res); + return this.isEmpty() ? this.end(opts) : this.poll(opts, true); + } + + /** + * Asynchronous Queue end will return last results. + * @public + * @emits {QueueAsync.events.end} - when opts.silent is false or undefined + * @param {Object} [opts = {}] - additional options + * @return {Array} + **/ + end(opts) { + if(!opts.silent) this.emit(QueueAsync.events.end, this._last); + return this._last; + } + + /** + * Queue Events + * @static + * @type {Object} + **/ + static events = extend(false, {}, Queue.events, { + + /** + * @event next + **/ + next: 'util:adt:queue-async:next', + + /** + * @event end + **/ + end: 'util:adt:queue-async:end' + + }); + +} + +export default QueueAsync; diff --git a/src/commands/util/adt/queue.es6 b/lib/util/adt/queue.es6 similarity index 51% rename from src/commands/util/adt/queue.es6 rename to lib/util/adt/queue.es6 index 89fccca..2a785f8 100644 --- a/src/commands/util/adt/queue.es6 +++ b/lib/util/adt/queue.es6 @@ -1,11 +1,11 @@ /** -* @module commands.util.adt +* @module util.adt * @author Patricio Ferreira <3dimentionar@gmail.com> **/ import _ from 'underscore'; import extend from 'extend'; -import Collection from 'commands/util/adt/collection'; -import QueueException from 'commands/util/exception/adt/queue'; +import Collection from 'util/adt/collection'; +import QueueException from 'util/exception/adt/queue'; /** * Class Queue @@ -15,67 +15,63 @@ import QueueException from 'commands/util/exception/adt/queue'; * * let myqueue = new Queue([1,2,3], { capacity: 4 }); // initial is set to capacity was set to 4 * myqueue.offer(4); // Adds one more element without violating capacity (3) -* myqueue.poll(); +* myqueue.peek(); * * let myqueue = new Queue([{ name: 'one' }, { name: 'two' }], { capacity: 3, interface: MyClass }); * myqueue.offer({ name: 3 }); // Adds one more element without violating capacity (3) * myqueue.poll(); -* @extends commands.util.adt.Collection +* @extends util.adt.Collection **/ -export default class Queue extends Collection { - - /** - * Queue capacity - * @public - * @type {Number} - **/ - capacity = 0 +class Queue extends Collection { /** * Constructor * @public * @override - * @param [intial = []] {Array} initial collection of elements - * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Queue} + * @param [initial = []] {Array} Initial Array + * @param [opts = {}] {Object} collection options + * @return {util.adt.Queue} **/ constructor(initial = [], opts = {}) { - super(initial, opts); - return extend(true, this, { capacity: opts.capacity }); + const { capacity } = opts; + return super(initial, extend(true, opts, _.defined(capacity) ? { capacity } : { capacity: 0 })); } /** - * Validates capacity of the queue to either decide, to add or not the element on this queue - * @TODO: Keep an eye on the order of validations while unit testing... + * Validates element or array of elements to decide either, to add or not the elements on this queue * @private * @override - * @throws {commands.util.exceptions.QueueException} + * @throws {util.exceptions.QueueException} * @param element {Any} element to validate - * @param opts {Object} additional options * @return {Boolean} **/ - _valid(element, opts) { - if(this.size() >= this.capacity) return false; - if(!_.defined(opts.capacity)) - throw QueueException.new({ type: 'capacityUndefined', level: QueueException.fatal }); - if(_.isArray(element) && element.length > this.capacity) - throw QueueException.new({ type: 'capacityViolation', level: QueueException.fatal }, - { capacity: this.capacity }); + _valid(element) { + const { capacity } = this; + if(_.isArray(element) && (element.length > capacity)) + throw QueueException.new('capacityViolation', { capacity }); return super._valid(element); } + /** + * Returns true if the current size has reached the capacity of the queye, false otherwise + * @private + * @return {Boolean} + **/ + _validCapacity() { + return (this.size() < this.capacity); + } + /** * Set initial collection of elements on this queue * @public * @override * @param [col = []] {Array} collection of elements * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Queue} + * @return {util.adt.Queue} **/ set(col = [], opts = {}) { - if(!this._valid(col, opts)) return this; - this.capacity = opts.capacity; - return super.set(col); + if(!this._validCapacity() || !this._valid(col) || !_.isArray(col) || col.length === 0) return this; + return super.set(col, opts); } /** @@ -88,9 +84,8 @@ export default class Queue extends Collection { * @return {Boolean} **/ offer(element, opts = {}) { - if(!this._valid(element, opts)) return false; - this.add(element, extend(true, opts, { silent true })) - ._fire(Queue.events.offer, this, element); + if(!this._validCapacity() || !this._valid(element)) return false; + this._fire(Queue.events.offer, opts, this.add(element, extend(true, {}, opts, { silent: true }))); return true; } @@ -106,10 +101,14 @@ export default class Queue extends Collection { /** * Retrieves and removes the head of this queue, or returns null if this queue is empty * @public + * @param [opts = {}] {Object} additional options * @return {Object} **/ - poll() { - return (this.size() > 0) ? this.remove(0, { silent: true })._fire(Queue.events.poll) : null; + poll(opts = {}) { + if(this.size() === 0) return null; + let polled = this.remove(this.get(0), { silent: true }); + this._fire(Queue.events.poll, opts, polled); + return polled; } /** @@ -117,26 +116,18 @@ export default class Queue extends Collection { * @static * @type {Object} **/ - static events = extend(false, Collection.events, { + static events = extend(false, {}, Collection.events, { /** * @event offer **/ - offer: 'commands:util:adt:queue:offer', + offer: 'util:adt:queue:offer', /** * @event poll **/ - poll: 'commands:util:adt:queue:poll' - }) - - /** - * Static constructor - * @static - * @param [...args] {Any} Constructor arguments - * @return {commands.util.adt.Queue} - **/ - static new(...args) { - return new this(...args); - } + poll: 'util:adt:queue:poll' + }); } + +export default Queue; diff --git a/lib/util/adt/stack-async.es6 b/lib/util/adt/stack-async.es6 new file mode 100644 index 0000000..bc69f46 --- /dev/null +++ b/lib/util/adt/stack-async.es6 @@ -0,0 +1,135 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Stack from 'util/adt/stack'; +import Asynchronous from 'visitors/async/async'; + +/** +* Class StackAsync +* Defines the interface of a Stack LIFO (LastIn-FirstOut) +* Interface for objects on this stack, must implement method `next` of {@link util.proxy.Asynchronous} +* for promise resolution. +* @example +*
Usage
+* +* let mystack = StackAsync.new([1,2,3], { interface: [Class] }) +* .on(StackAsync.events.next, (element) => console.log('Next: ', element)) +* .on(StackAsync.events.end, (results) => console.log('End: ', results)) +* const mypromise = mystack.pop(); // asynchronous +* @extends util.adt.Stack +**/ +class StackAsync extends Stack { + + /** + * Constructor + * @public + * @override + * @param [initial = []] {Array} Initial Array + * @param [opts = {}] {Object} collection options + * @return {util.adt.StackAsync} + **/ + constructor(initial = [], opts = {}) { + return super(initial, extend(true, opts, { _visitor: Asynchronous.new(), _last: [] })); + } + + /** + * Default instanciation strategy for new elements added in this collection + * @private + * @override + * @param e {Any} element to instanciate + * @param opts {Object} additional options + * @return {Any} + **/ + _new(e, opts) { + let element = super._new(e, opts); + this._visitor.validate(element); + return element.accept(this._visitor); + } + + /** + * Resets Last Results + * @public + * @return {util.adt.StackAsync} + **/ + _resetLast() { + this._last = []; + return this; + } + + /** + * Retrieves and removes the head of this stack, or returns null if this stack is empty + * @public + * @async + * @override + * @param [opts = {}] {Object} additional options + * @param {Boolean} [next = false] - async queue already started + * @return {Promise} + **/ + async pop(opts = {}, next = false) { + if(!next) this._resetLast(); + const res = await this.next(opts); + return this.onNext(res, opts); + } + + /** + * Asynchronous Queue next + * @public + * @emits {StackAsync.events.next} - when opts.silent = false or undefined + * @param [opts] {Object} additional options + * @return {Promise} + **/ + next(opts) { + let element = super.pop(opts); + if(!opts.silent) this.emit(StackAsync.events.next, element); + return element.execute(this); + } + + /** + * Retrieves and removes the head of this queue, or returns null if this queue is empty + * @public + * @param res {Promise} promise reference with resolution (resolved or rejected) + * @param [opts] {Object} additional options + * @return {Promise} + **/ + onNext(res, opts) { + this._last.push(res); + return this.isEmpty() ? this.end(opts) : this.pop(opts, true); + } + + /** + * Asynchronous Queue end + * @public + * @emits {StackAsync.events.end} - when opts.silent is false or undefined + * @param [opts = {}] {Object} additional options + * @return {util.adt.StackAsync} + **/ + end(opts) { + if(!opts.silent) this.emit(StackAsync.events.end, this._last); + return this._last; + } + + /** + * Queue Events + * @static + * @type {Object} + **/ + static events = extend(false, {}, Stack.events, { + + /** + * @event next + **/ + next: 'util:adt:stack-async:next', + + /** + * @event end + **/ + end: 'util:adt:stack-async:end' + + }); + +} + +export default StackAsync; diff --git a/src/commands/util/adt/stack.es6 b/lib/util/adt/stack.es6 similarity index 67% rename from src/commands/util/adt/stack.es6 rename to lib/util/adt/stack.es6 index 97f7484..6267d04 100644 --- a/src/commands/util/adt/stack.es6 +++ b/lib/util/adt/stack.es6 @@ -1,10 +1,12 @@ /** -* @module commands.util.adt.Stack +* @module util.adt.Stack * @author Patricio Ferreira <3dimentionar@gmail.com> **/ +'use strict'; + import _ from 'underscore'; import extend from 'extend'; -import Collection from 'commands/util/adt/collection'; +import Collection from 'util/adt/collection'; /** * Class Stack @@ -18,20 +20,21 @@ import Collection from 'commands/util/adt/collection'; * * let mystack = new Stack([{ name: 'one' }, { name: 'two' }], { interface: MyClass }); * mystack.push({ name: 3 }); // Adds one more element into the stack -* mystack.pop(); // Outputs { name: 'one' } of MyClass, removing the element -* @extends commands.util.adt.Collection +* mystack.pop(); // Outputs { name: 3 } of MyClass, removing the element +* @extends util.adt.Collection **/ -export default class Stack extends Collection { +class Stack extends Collection { /** * Constructor * @public * @param [initial = []] {Array} initial collection of elements on this stack * @param [opts = {}] {Object} additional options - * @return {commands.util.adt.Stack} + * @return {util.adt.Stack} **/ constructor(initial = [], opts = {}) { - return super(initial, opts); + super(initial, opts); + return this; } /** @@ -59,13 +62,14 @@ export default class Stack extends Collection { /** * Retrieves and removes the head of this stack, or returns null if this stack is empty * @public - * @return {Object} + * @param [opts = {}] {Object} additional options + * @return {Any} **/ - pop() { + pop(opts = {}) { if(this.size() <= 0) return null; - let removed = this.remove(0, { silent: true }); - this._fire(Stack.events.pop, {}, removed); - return removed; + let popped = this.remove(this.get(this.size() - 1), { silent: true }); + this._fire(Stack.events.pop, opts, popped); + return popped; } /** @@ -76,7 +80,7 @@ export default class Stack extends Collection { **/ search(element) { if(!this._valid(element)) return -1; - return this.findIndex((e) => _.isEqual(((this.hasInterface() && _.defined(e.toJSON)) ? e.toJSON() : e), element)); + return this.findIndex((e) => _.isEqual(this._toJSON(e), this._toJSON(element))); } /** @@ -84,26 +88,18 @@ export default class Stack extends Collection { * @static * @type {Object} **/ - static events = extend(false, Collection.events, { + static events = extend(false, {}, Collection.events, { /** * @event push **/ - push: 'commands:util:adt:stack:push', + push: 'util:adt:stack:push', /** * @event pop **/ - pop: 'commands:util:adt:stack:pop' + pop: 'util:adt:stack:pop' }); - /** - * Static constructor - * @static - * @param [...args] {Any} Constructor arguments - * @return {commands.util.adt.Stack} - **/ - static new(...args) { - return new this(...args); - } - } + +export default Stack; diff --git a/lib/util/exception/adt/queue.es6 b/lib/util/exception/adt/queue.es6 new file mode 100644 index 0000000..29e7364 --- /dev/null +++ b/lib/util/exception/adt/queue.es6 @@ -0,0 +1,39 @@ +/** +* @module util.exception.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Exception from 'util/exception/exception'; + +/** +* Class QueueException +* @extends {util.exception.Exception} +**/ +class QueueException extends Exception { + + /** + * Constructor + * @public + * @override + * @param [...args] {Any} constructor attribute + * @return {util.exception.adt.QueueException} + **/ + constructor(...args) { + super(...args); + this.name = 'QueueException'; + return this; + } + + /** + * Command Exception types + * @public + * @type {Object} + **/ + static type = extend(true, {}, Exception.type, { + capacityViolation: _.template(`Queue element's collection passed overflows the current capacity <%= capacity %>`) + }); + +} + +export default QueueException; diff --git a/lib/util/exception/exception.es6 b/lib/util/exception/exception.es6 new file mode 100644 index 0000000..6ce4a6d --- /dev/null +++ b/lib/util/exception/exception.es6 @@ -0,0 +1,52 @@ +/** +* @module util.exception +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Logger from 'util/logger/logger'; + +/** +* Class Exception +* @extends {Error} +**/ +class Exception extends Error { + + /** + * Constructor + * @public + * @override + * @param message {Function} message template function + * @param [...args] {Any} constructor attribute + * @return {util.exception.Exception} + **/ + constructor(message, ...args) { + super(message); + this.name = 'Exception'; + Error.captureStackTrace(this, this.constructor); + return this; + } + + /** + * Command Exception types + * @public + * @type {Object} + **/ + static type = { + unknown: _.template('Unknown Exception') + }; + + /** + * Static Constructor + * @static + * @param [type = 'unknown'] + * @param [...args] {Any} additional arguments + * @return {util.exception.Exception} + **/ + static new(type, ...args) { + return new this(this.type[type](...args), ...args); + } + +} + +export default Exception; diff --git a/lib/util/exception/proxy/interface.es6 b/lib/util/exception/proxy/interface.es6 new file mode 100644 index 0000000..e2cd882 --- /dev/null +++ b/lib/util/exception/proxy/interface.es6 @@ -0,0 +1,40 @@ +/** +* @module util.exception.proxy +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Exception from 'util/exception/exception'; + +/** +* Class InterfaceException +* @extends {util.exception.Exception} +**/ +class InterfaceException extends Exception { + + /** + * Constructor + * @public + * @override + * @param [...args] {Any} constructor attribute + * @return {util.exception.proxy.InterfaceException} + **/ + constructor(...args) { + super(...args); + this.name = 'InterfaceException'; + return this; + } + + /** + * Command Exception types + * @public + * @type {Object} + **/ + static type = extend(true, {}, Exception.type, { + proxy: _.template(`Proxies require a 'target' in which their interface operates on`), + interface: _.template(`Instance is required to implement [<%= name %>]`) + }); + +} + +export default InterfaceException; diff --git a/lib/util/factory/factory.es6 b/lib/util/factory/factory.es6 new file mode 100644 index 0000000..5660203 --- /dev/null +++ b/lib/util/factory/factory.es6 @@ -0,0 +1,199 @@ +/** +* @module util.factory +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'util/mixins'; +import fs from 'fs-extra'; +import path, { resolve } from 'path'; +import extend from 'extend'; +import { EventEmitter } from 'events'; +import logger from 'util/logger/logger'; + +/** +* Class Factory +* @extends {events.EventEmitter} +**/ +class Factory extends EventEmitter { + + /** + * Constructor + * @public + * @param {Object} [attrs = {}] - constructor attributes + * @return {util.factory.Factory} + **/ + constructor(attrs = {}) { + super(); + return extend(true, this, attrs, { _factories: new Map() }).basePath(); + } + + /** + * Resets factories + * @public + * @return {util.factoryFactory} + **/ + reset() { + this._factories.clear(); + return this; + } + + /** + * Sets a base path + * @public + * @param {String} [ph = Factory.basePath] - base path to resolve factories filepaths + * @return {util.factory.Factory} + **/ + basePath(ph = Factory.basePath) { + this.path = ph; + return this; + } + + /** + * Resolves path to factory class file + * @private + * @param {String} ph - path to resolve + * @return {String} + **/ + _resolve(ph) { + return resolve(this.path, ph); + } + + /** + * Validates factory path to file + * @private + * @param {String} ph - path to registered factory + * @return {Boolean} + **/ + _validate(ph) { + try { + return (_.defined(ph) && _.isString(ph) && ph.length > 0 && _.defined(require.resolve(this._resolve(ph)))); + } catch(ex) { + logger(`${this.toString()} > [ERROR-CODE] ${ex.code}`).out({}, logger.white); + logger(ex.message).warn(logger); + return false; + } + } + + /** + * Resolves which factory class constructor to use for instanciation + * If a static new method is detected on the factory, this will be used. + * If no static new method is detected and the constructor is a function, operator new will be used. + * Finally, if the factory is not a Function, this method will return it verbatim (no arguments will be passed). + * @private + * @param {Any} o - factory object + * @param {Any} [...args] - constructor factory arguments + * @return {Any} + **/ + _new(o, ...args) { + if(_.isFunction(o)) return _.defined(o.new) ? o.new(...args) : new o(...args); + return o; + } + + /** + * Perform a look up and retrieves the first factory matching the path. Returns null otherwise + * @public + * @param {String} [ph = ''] - path to registered factory + * @return {Any} + **/ + find(ph = '') { + for(let [k, v] of this._factories.entries()) + if(k === ph) return v; + return null; + } + + /** + * Returns true if the given path was registered previously, false otherwise + * @public + * @param {String} ph - path to registered factory + * @return {Boolean} + **/ + exists(ph) { + if(!_.defined(ph)) return false; + return this._factories.has(ph); + } + + /** + * Registers a single factory object with a given path, if it haven't been registered before. + * @public + * @param {String} ph - factory path location + * @return {util.factory.Factory} + **/ + register(ph) { + if(this._validate(ph) && !this.exists(ph)) this._factories.set(ph, this._resolve(ph)); + return this; + } + + /** + * Registers a list of factory objects with a given path list, if they haven't been registered before. + * @public + * @param {Array} [paths = []] - factory path locations + * @return {util.factory.Factory} + **/ + registerAll(paths = []) { + _.each(paths, this.register, this); + return this; + } + + /** + * Unregisters a single factory object with a given path, if it have been registered before. + * @public + * @param {String} ph - factory path location + * @return {util.factory.Factory} + **/ + unregister(ph) { + if(this.exists(ph)) this._factories.delete(ph); + return this; + } + + /** + * Unregisters a list of factory objects with a given path list, if they have been registered before. + * @public + * @param {Array} [paths = []] - factory path locations + * @return {util.factory.Factory} + **/ + unregisterAll(paths = []) { + _.each(paths, this.unregister, this); + return this; + } + + /** + * Obtain a new instance of the factory, by optionally passing arguments to the constructor. + * If the factory is not found, returns null. + * @public + * @param {String} ph - registred factory path + * @param {Any} [...args] - factory constructor arguments + * @return {Any} + **/ + get(ph, ...args) { + return this.exists(ph) ? this._new(require(this.find(ph)).default, ...args) : null; + } + + /** + * String representation of this class + * @public + * @override + * @return {String} + **/ + toString() { + return this.constructor.name; + } + + /** + * Factory Defaults + * @static + * @type {Object} + **/ + static basePath = process.cwd(); + + /** + * Static Constructor + * @static + * @param {Any} [...args] - constructor arguments + * @return {util.factory.Factory} + **/ + static new(...args) { + return new this(...args); + } + +} + +export default Factory.new(); diff --git a/lib/util/logger/logger.es6 b/lib/util/logger/logger.es6 new file mode 100644 index 0000000..f48af1e --- /dev/null +++ b/lib/util/logger/logger.es6 @@ -0,0 +1,326 @@ +/** +* @module util.logger +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { EventEmitter } from 'events'; +import _ from 'underscore'; +import _s from 'underscore.string'; +import extend from 'extend'; +import chalk from 'chalk'; + +/** +* Class Logger +* @extends events.EventEmitter +**/ +export class Logger extends EventEmitter { + + /** + * Constructor + * @public + * @return {util.logger.Logger} + **/ + constructor() { + super(); + this.level(); + return extend(true, this, { _buffer: [] }); + } + + /** + * Proxy's 'getPrototypeOf' trap override + * @public + * @param {Any} target - constructor reference + * @return {Object} + **/ + getPrototypeOf(target) { + return this.constructor.prototype; + } + + /** + * Proxy's 'get' trap override + * @public + * @param {util.logger.Logger} target - logger instance reference + * @param {String} property - property to resolve + * @return {util.logger.Logger} + **/ + get(target, property) { + if(_.defined(this[property])) return this[property]; + if(_.defined(chalk[property])) return chalk[property]; + return null; + } + + /** + * Proxy's 'set' trap override + * @public + * @param {util.logger.Logger} target - logger instance reference + * @param {String} property - property to resolve + * @param {Any} value - value to set + * @return {Boolean} + **/ + set(target, property, value) { + this[property] = value; + return true; + } + + /** + * Proxy's 'defineProperty' trap override + * @public + * @param {util.logger.Logger} target - logger instance reference + * @param {String} property - property to define + * @return {Boolean} + **/ + defineProperty(target, property) { + this[property] = null; + return true; + } + + /** + * Proxy's 'deleteProperty' trap override + * @public + * @param {util.logger.Logger} target - logger instance reference + * @param {String} property - property to delete + * @return {Boolean} + **/ + deleteProperty(target, property) { + delete this[property]; + return true; + } + + /** + * Proxy's 'apply' trap override + * @private + * @param {util.logger.Logger} target - logger instance reference + * @param {Any} thisArg - this context reference + * @param {Array} args - arguments list + * @return + **/ + apply(target, thisArg, args) { + this._add(...args); + return Logger.ref; + } + + /** + * Validates level of output provided + * Validation Rules: + * - If level equals `silent` -> will output only fatal logs + * - If level equals `output` -> will output warn, fatal and output (except debug). + * - If level equals `debug` -> will print all logs (warn, fatal, output and debug). + * @public + * @param {Object} type - log's type + * @return {Boolean} + **/ + _validate(type) { + const { fatal, debug } = Logger.type; + if(this._level === Logger.level.silent && _.isEqual(type.name, fatal.name)) return true; + if(this._level === Logger.level.output && !_.isEqual(type.name, debug.name)) return true; + if(this._level === Logger.level.debug) return true; + return false; + } + + /** + * Appends Message to the internal buffer + * @public + * @param {String} message - message to add + * @return {util.logger.Logger} + **/ + _add(message) { + this._buffer.push(message); + return this; + } + + /** + * Flushes message buffer + * @private + * @emits {Logger.events.flush} + * @return {util.logger.Logger} + **/ + _flush() { + this._buffer = []; + return this; + } + + /** + * Fires Event + * @public + * @param {String} name - event name + * @param opts {Object} - additional options + * @return {util.logger.Logger} + **/ + _fire(name, opts) { + if(!opts.silent) this.emit(name, this); + return this; + } + + /** + * Outputs message to the stdout + * @private + * @param {Function} style - chalk style override + * @param {Object} type - Logger type + * @return {util.logger.Logger} + **/ + _output(style, type) { + if(!this._validate(type)) return this._flush(); + let format = _.defined(style) ? style : type.style; + return this._stdout(format(this._buffer.join('\n')))._flush(); + } + + /** + * Standard Out (console.log wrapper) + * @private + * @param {String} content - content to output in stdout + * @return {util.logger.Logger} + **/ + _stdout(content) { + console.warn(content); + return this; + } + + /** + * Output message + * @public + * @emits {Logger.events.debug} + * @param {Object} [opts = {}] - additional options + * @param {Object} style - chalk optional style + * @return {util.logger.Logger} + **/ + out(opts = {}, style) { + return this._output(style, Logger.type.output)._fire(Logger.events.output, opts); + } + + /** + * Debug message + * @public + * @emits {Logger.events.debug} + * @param {Object} [opts = {}] - additional options + * @param {Object} style - chalk optional style + * @return {util.logger.Logger} + **/ + debug(opts = {}, style) { + return this._output(style, Logger.type.debug)._fire(Logger.events.debug, opts); + } + + /** + * Warning message + * @public + * @emits {Logger.events.warning} + * @param {Object} [opts = {}] - additional options + * @return {util.logger.Logger} + **/ + warn(opts = {}) { + return this._output(null, Logger.type.warning)._fire(Logger.events.warning, opts); + } + + /** + * Fatal message + * @public + * @emits {Logger.events.fatal} + * @param {Object} [opts = {}] - additional options + **/ + fatal(opts = {}) { + this._output(null, Logger.type.fatal)._fire(Logger.events.fatal, opts); + process.exit(1); + } + + /** + * Sets Logger Level + * @public + * @param {String} [level = Logger.level.output] - log's level + * @return {Function} + **/ + level(lvl = Logger.level.output) { + this._level = lvl; + return this; + } + + /** + * Logger Types + * @static + * @type {Object} + **/ + static type = { + debug: { + name: 'debug', + style: chalk.white + }, + output: { + name: 'output', + style: chalk.green + }, + warning: { + name: 'warning', + style: chalk.yellow + }, + fatal: { + name: 'fatal', + style: chalk.red + } + } + + /** + * Logger Levels + * @static + * @type {Object} + **/ + static level = { + output: 'logger:output', // Default + debug: 'logger:debug', + silent: 'logger:silent' + } + + /** + * Logger Events + * @static + * @type {Object} + **/ + static events = { + + /** + * @event flush + **/ + flush: 'util:logger:flush', + + /** + * @event debug + **/ + debug: 'util:logger:debug', + + /** + * @event output + **/ + output: 'util:logger:output', + + /** + * @event warning + **/ + warning: 'util:logger:warning', + + /** + * @event fatal + **/ + fatal:'util:logger:fatal' + + } + + /** + * Sets an static reference of an instance of this class + * @private + * @static + * @param {util.logger.Logger} logger - logger instance reference + * @return {Any} + **/ + static _instance(logger) { + return (this.ref = logger); + } + + /** + * Static Constructor + * @static + * @param {Any} [...args] - constructor arguments + * @return {util.logger.Logger} + **/ + static new(...args) { + return new this(...args); + } + +} + +export default Logger._instance(new Proxy(Logger, Logger.new())); diff --git a/src/commands/util/mixins.js b/lib/util/mixins.es6 similarity index 83% rename from src/commands/util/mixins.js rename to lib/util/mixins.es6 index c90996e..54864a0 100644 --- a/src/commands/util/mixins.js +++ b/lib/util/mixins.es6 @@ -1,5 +1,5 @@ /** -* Underscore Mixins +* @module util * @author Patricio Ferreira <3dimentionar@gmail.com> **/ import _ from 'underscore'; @@ -43,7 +43,7 @@ _.mixin({ if(_.isRealObject(v)) return _.parametrize(v, pf, k); if(_.isArray(v)) v = v.join(','); return [`${pf}${_s.ltrim(_s(dk + ' ' + k).dasherize().value(), '-')}`, v]; - }, this).flatten().compact().value(); + }, this).flatten().filter((v, k) => _.defined(v)).value(); }, /** @@ -54,7 +54,7 @@ _.mixin({ **/ isRealObject: function(o) { if(!_.defined(o) || !_.defined(o.constructor)) return false; - return ((o).constructor.toString().indexOf('Object') !== -1); + return ((o).constructor.toString().indexOf('function Object() {') !== -1); }, /** @@ -69,6 +69,18 @@ _.mixin({ return (_.isRealObject(o) || _.isArray(o)); }, + /** + * Returns an array of all method names of a given object. + * @public + * @param {String} o - object to be evaluated + * @return {Boolean} + **/ + methods: function(o) { + return _.filter(Object.getOwnPropertyNames(o), (name) => { + return (typeof(o[name]) === 'function'); + }); + }, + /** * Returns true if parameter source is an instance of parameter constructor, false otherwise. * @public @@ -92,3 +104,5 @@ _.mixin({ } }); + +export default _; diff --git a/lib/util/visitor/visited.es6 b/lib/util/visitor/visited.es6 new file mode 100644 index 0000000..ecd64ae --- /dev/null +++ b/lib/util/visitor/visited.es6 @@ -0,0 +1,60 @@ +/** +* @module util.visitor +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { EventEmitter } from 'events'; +import _ from 'underscore'; +import extend from 'extend'; +import Visitor from 'util/visitor/visitor'; +import InterfaceException from 'util/exception/proxy/interface'; + +/** +* Class Visited +* @extends {events.EventEmitter} +**/ +class Visited extends EventEmitter { + + /** + * Constructor + * @public + * @param {Any} [...args] - constructor arguments + * @return {util.visitor.Visited} + **/ + constructor(...args) { + super(); + return extend(true, this, ...args); + } + + /** + * Returns true if a given visitor is defined and an instance of util.visitor.Visitor, false otherwise + * @public + * @param {util.visitor.Visitor} visitor - visitor to validate + * @return {Boolean} + **/ + validate(visitor) { + return _.defined(visitor) && _.instanceOf(visitor, Visitor); + } + + /** + * Default strategy that accepts visitor by this visited instance + * @public + * @param {util.visitor.Visitor} vi - visitor to accept + * @return {util.visitor.Visited} + **/ + accept(visitor) { + return this.validate(visitor) ? visitor.visit(this) : this; + } + + /** + * Static Constructor + * @static + * @param {Any} [...args] - constructor arguments + * @return {util.visitor.Visited} + **/ + static new(...args) { + return new this(...args); + } + +} + +export default Visited; diff --git a/lib/util/visitor/visitor.es6 b/lib/util/visitor/visitor.es6 new file mode 100644 index 0000000..36f4323 --- /dev/null +++ b/lib/util/visitor/visitor.es6 @@ -0,0 +1,115 @@ +/** +* @module util.visitor +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { EventEmitter } from 'events'; +import _ from 'underscore'; +import _s from 'underscore.string'; +import extend from 'extend'; +import Visited from 'util/visitor/visited'; +import InterfaceException from 'util/exception/proxy/interface'; + +/** +* Class Visitor +* @extends {events.EventEmitter} +**/ +class Visitor extends EventEmitter { + + /** + * Constructor + * @public + * @param {Any} [...args] - constructor arguments + * @return {util.visitor.Visitor} + **/ + constructor(...args) { + super(); + return extend(true, this, ...args); + } + + /** + * Filter private methods declared on this visitor + * @public + * @return {Array} + **/ + _methods() { + const { prototype } = this.constructor; + return _.filter(_.methods(prototype).concat(_.methods(this)), (func) => { + return !_s.startsWith(func, '_') && !_.contains(Visitor._methods, func); + }); + } + + /** + * Decorates visited instance with methods declared on this visitor + * @private + * @param {util.visitor.Visited} vi - instance to be visited by this visitor + * @param {Any} [...args] - arguments passed to the visitor who visit the current visited instance + * @return {util.visitor.Visited} + **/ + _decorate(vi, ...args) { + return _.reduce(this._methods(), (memo, method) => { + if(!_.defined(memo[method])) memo[method] = _.bind(this[method], this, vi, ...args); + return memo; + }, vi); + } + + /** + * Returns true if the visited instance implements Visited interface. + * Otherwise, this method will raise an InterfaceException. + * @public + * @throws {util.exception.proxy.InterfaceException} + * @param {util.visitor.Visited} vi - visited instance + * @return {Boolean} + **/ + validate(vi) { + if(!_.defined(vi)) return false; + if(!_.defined(vi.accept)) + throw InterfaceException.new('interface', { name: 'util.visitor.Visited' }); + return true; + } + + /** + * Default Visit Strategy will return the visited object verbatim. + * This method is likely to be overriden by subsclasses of this visitor when needed. + * @public + * @param {util.visitor.Visited} vi - instance to be visited by this visitor + * @param {Any} [...args] - arguments passed to the visitor who visit the current visited instance + * @return {util.visitor.Visited} + **/ + visit(vi, ...args) { + return this.validate(vi) ? this._decorate(vi, ...args) : null; + } + + /** + * Visitor Name + * @public + * @type {String} + **/ + get name() { + return 'Visitor'; + } + + /** + * Visitor Methods to filter out of the proxifing visit strategy + * @static + * @type {Array} + **/ + static _methods = [ + 'visit', + 'validate', + 'name', + 'constructor' + ].concat(_.functions(EventEmitter.prototype)); + + /** + * Static Constructor + * @static + * @param {Any} [...args] - constructor arguments + * @return {util.visitor.Visitor} + **/ + static new(...args) { + return new this(...args); + } + +} + +export default Visitor; diff --git a/lib/visitors/async/async.es6 b/lib/visitors/async/async.es6 new file mode 100644 index 0000000..c9b7038 --- /dev/null +++ b/lib/visitors/async/async.es6 @@ -0,0 +1,63 @@ +/** +* @module visitors.async +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { EventEmitter } from 'events'; +import _ from 'underscore'; +import Visitor from 'util/visitor/visitor'; + +/** +* Class Asynchronous +* @extends {util.visitor.Visitor} +**/ +class Asynchronous extends Visitor { + + /** + * Constructor + * @public + * @override + * @param {Any} [...args] - constructor arguments + * @return {visitors.async.Asynchronous} + **/ + constructor(...args) { + return super(); + } + + /** + * Default Next Strategy to always resolve the promise. + * Note: This method was designed (and it's most likely) to be overriden by + * {@link util.visitor.Visited} subclasses that use this visitor. + * @public + * @param adt {util.proxy.Asynchronous} adt used for asynchronous operations + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @return {visitors.async.Asynchronous} + **/ + next(adt, resolve, reject) { + resolve(); + return this; + } + + /** + * Default strategy to perform an asynchronous operation + * @public + * @param {util.visitor.Visited} [ctx] - context reference + * @param {visitors.async.Asynchronous} adt - reference to adt using this interface on their elements + * @return {Promise} + **/ + execute(ctx, adt) { + return new Promise((resolve, reject) => ctx.next(adt, resolve, reject)); + } + + /** + * Visitor Name + * @public + * @type {String} + **/ + get name() { + return 'AsynchronousVisitor'; + } + +} + +export default Asynchronous; diff --git a/lib/visitors/commander.es6 b/lib/visitors/commander.es6 new file mode 100644 index 0000000..bff0ee6 --- /dev/null +++ b/lib/visitors/commander.es6 @@ -0,0 +1,195 @@ +/** +* @module visitors +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { resolve } from 'path'; +import _ from 'underscore'; +import extend from 'extend'; +import yargs from 'yargs'; +import chalk from 'chalk'; +import Factory from 'util/factory/factory'; +import Visitor from 'util/visitor/visitor'; +import logger from 'util/logger/logger'; + +/** +* Class Commander +* @extends {util.visitor.Visitor} +**/ +class Commander extends Visitor { + + /** + * Constructor + * @public + * @override + * @param {Command} command - command visited by this visitor + * @return {util.visitor.Visitor} + **/ + constructor(command) { + return super({ command }); + } + + /** + * Visit Strategy + * @public + * @override + * @param {util.visitor.Visited} vi - instance to be visited by this visitor + * @param {Any} [...args] - arguments passed to the visitor + * @return {util.visitor.Visited} + **/ + visit(vi, ...args) { + return this.validate(vi) ? extend(false, vi, { commander: this }) : vi; + } + + /** + * Reads CLI arguments and build yargs interface + * @public + * @return {yargs} + **/ + read() { + return _.reduce(Commander.decorators, (memo, decorator) => { + return Factory.get(decorator, memo, this.command, this); + }, yargs.reset().wrap(120)); + } + + /** + * Commander CLI Options Parse Handler + * @public + * @param {Error} [err] - Parse Error + * @param {Object} argv - parsed options + * @param {Object} output - yargs output + * @return {visitors.Commander} + **/ + onParse(err, argv, output) { + return this.emit(Commander.events.parse, err, argv, output); + } + + /** + * Commander Arguments + * @private + * @param {Array} [args = process.argv] - command line arguments verbatim + * @return {Array} + **/ + _args(args = process.argv) { + return args; + } + + /** + * Creates command nomenclature for yargs + * @private + * @param {Command} command - command instance + * @return {String} + **/ + _command(command) { + const { name, abbr } = command; + return chalk.green(`${name}` + (_.defined(abbr) ? `, -${abbr}` : '')); + } + + /** + * Default Strategy that returns Command's aliases + * @public + * @param {Command} command - command instance + * @return {Array} + **/ + _aliases(command) { + return command.aliases; + } + + /** + * Default Strategy that returns Command Description + * @private + * @param {Command} command - command instance + * @return {String} + **/ + _desc(command) { + return chalk.yellow(command.description); + } + + /** + * Default Strategy that returns command options + * @private + * @param {Command} command - command instance + * @return {Object} + **/ + _builder(command) { + return command.options; + } + + /** + * Default Strategy that returns Command Handler + * @private + * @param {Command} command - command instance + * @return {Function} + **/ + _handler(command) { + return (argv) => { return this._onHandler(command, argv); }; + } + + /** + * Default Command Handler + * @private + * @param {Command} command - command instance + * @param {Object} argv - yargs parsed arguments + * @return {visitors.Commander} + **/ + _onHandler(command, argv) { + Factory.register(command.path); + this.command.settings({ options: extend(false, _.omit(argv, Commander.ignore), { path: command.path }) }); + return this; + } + + /** + * Visitor Name + * @public + * @type {String} + **/ + get name() { + return 'CommanderVisitor'; + } + + /** + * Commander decorators + * @static + * @type {Array} + **/ + static decorators = [ + 'visitors/commander/version', + 'visitors/commander/usage', + 'visitors/commander/epilogue', + 'visitors/commander/commands', + 'visitors/commander/parse' + ]; + + /** + * Commander Events + * @static + * @type {Object} + **/ + static events = { + /** + * @event parse + **/ + parse: 'visitors:commander:parse' + } + + /** + * Yargs arguments to ignore + * @static + * @type {Array} + **/ + static ignore = ['$0', '_', 'version']; + + /** + * Static Constructor + * @static + * @override + * @param {Any} [...args] - constructor arguments + * @return {visitors.Commander} + **/ + static new(...args) { + Factory.registerAll(Commander.decorators); + return new this(...args); + } + +} + +export default Commander; diff --git a/lib/visitors/commander/commands.es6 b/lib/visitors/commander/commands.es6 new file mode 100644 index 0000000..2cd1f4c --- /dev/null +++ b/lib/visitors/commander/commands.es6 @@ -0,0 +1,54 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Yargs Command Builder Option Names +* @const +* @private +* @type {Array} +**/ +const Builder = ['command', 'aliases', 'desc', 'builder', 'handler']; + +/** +* Creates command's handler +* @const +* @private +* @param {visitors.Commander} ctx - commander visitor reference +* @param {Object} command - command options +* @param {Object} memo - memoized object +* @param {String} option - Yargs command option +* @return {Object} +**/ +const create = (ctx, command, memo, option) => { + memo[option] = ctx[`_${option}`](command); + return memo; +}; + +/** +* Create Single Yarg Command +* @const +* @private +* @param {visitors.Commander} ctx - commander visitor reference +* @param {yargs} memo - memoized yargs reference +* @param {Object} current - current command +* @return {yargs} +**/ +const command = (ctx, memo, current) => { + return memo.command(_.reduce(Builder, _.bind(create, this, ctx, current), {})); +}; + +/** +* Create Yargs Commands +* @static +* @param {yargs} yargs - yargs reference +* @param {Command} c - current command +* @param {visitors.Commander} ctx - commander visitor reference +* @return {yargs} +**/ +export default (yargs, c, ctx) => { + const { commands } = c.constructor; + return commands.reduce(_.bind(command, this, ctx), yargs); +}; diff --git a/lib/visitors/commander/epilogue.es6 b/lib/visitors/commander/epilogue.es6 new file mode 100644 index 0000000..5e16c20 --- /dev/null +++ b/lib/visitors/commander/epilogue.es6 @@ -0,0 +1,16 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import chalk from 'chalk'; + +/** +* Yargs Epilogue +* @static +* @param {yargs} yargs - yargs reference +* @return {yargs} +**/ +export default (yargs) => { + return yargs.epilogue(chalk.cyan(`For more information, please visit http://squarebox.nahuel.io/`)); +}; diff --git a/lib/visitors/commander/parse.es6 b/lib/visitors/commander/parse.es6 new file mode 100644 index 0000000..517caed --- /dev/null +++ b/lib/visitors/commander/parse.es6 @@ -0,0 +1,17 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Yargs Parse +* @static +* @param {yargs} yargs - yargs reference +* @param {Command} c - current command +* @param {visitors.Commander} ctx - commander visitor reference +* @return {util.visitor.Visited} +**/ +export default (yargs, command, ctx) => { + return yargs.parse(ctx._args(), _.bind(ctx.onParse, ctx)); +}; diff --git a/lib/visitors/commander/usage.es6 b/lib/visitors/commander/usage.es6 new file mode 100644 index 0000000..6e27312 --- /dev/null +++ b/lib/visitors/commander/usage.es6 @@ -0,0 +1,24 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import chalk from 'chalk'; + +/** +* Usage Message +* @const +* @private +* @type {String} +**/ +const message = 'sqbox [args]'; + +/** +* Yargs Usage +* @static +* @param {yargs} yargs - yargs reference +* @return {yargs} +**/ +export default (yargs) => { + return yargs.usage(chalk.white(message)); +}; diff --git a/lib/visitors/commander/version.es6 b/lib/visitors/commander/version.es6 new file mode 100644 index 0000000..4d9da36 --- /dev/null +++ b/lib/visitors/commander/version.es6 @@ -0,0 +1,23 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import { resolve } from 'path'; +import _ from 'underscore'; +import logger from 'util/logger/logger'; + +/** +* Yargs Version +* @static +* @param {yargs} yargs - yargs reference +* @param {Command} command - current command reference +* @return {yargs} +**/ +export default (yargs, command) => { + try { + return yargs.version(require(resolve(command.dirname, '..', 'package.json')).version); + } catch(ex) { + logger(`[WARN] SquareBox Version not detected - ${ex.message}`).debug(logger.yellow); + } + return yargs; +}; diff --git a/lib/visitors/configuration.es6 b/lib/visitors/configuration.es6 new file mode 100644 index 0000000..ef46199 --- /dev/null +++ b/lib/visitors/configuration.es6 @@ -0,0 +1,239 @@ +/** +* @module visitors +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Factory from 'util/factory/factory'; +import Visitor from 'util/visitor/visitor'; +import QueueAsync from 'util/adt/queue-async'; +import logger, { Logger } from 'util/logger/logger'; + +/** +* Class Configuration +* @extends {util.visitor.Visitor} +**/ +class Configuration extends Visitor { + + /** + * Constructor + * @public + * @override + * @param {Command} command - command visited by this visitor + * @return {visitors.Configuration} + **/ + constructor(command) { + super({ command }); + return extend(true, this, { queue: QueueAsync.new([], { capacity: Configuration.methods.length }) }); + } + + /** + * Create configuration retrieval method + * @private + * @param {String} method - configuration method path + * @return {util.visitor.Visitor} + **/ + _create(method) { + this.queue.offer(Factory.get(method, this.command)); + return this; + } + + /** + * Format and overrides command options from the CLI arguments + * @private + * @param {Object} opts - cli options for source + * @return {visitors.Configuration} + **/ + _override(opts) { + let toOverride = _.pick(opts, Configuration.cliOptions); + extend(true, this.command, _.reduce(toOverride, this._format, toOverride, this)); + return this; + } + + /** + * Format a given CLI option by evaluating the key to load the factory formatter and passing the value + * to the formatter. + * @public + * @param {Object} memo - options being memoized + * @param {Any} memo - options being memoized + * @param {String} memo - options being memoized + * @return {Object} + **/ + _format(memo, value, key) { + let ph = this.formatterPath(key); + if(Factory.exists(ph)) memo[key] = Factory.get(ph, value); + return memo; + } + + /** + * Merge configuration options (and possibly) applies overrides by CLI arguments. + * @private + * @param {Object} opts - configuration options for target + * @return {visitors.Configuration} + **/ + _source(opts) { + extend(true, this.command, _.pick(opts, Configuration.cliOptions)) + return this; + } + + /** + * Merge configuration options (and possibly) applies overrides by CLI arguments. + * @private + * @param {Object} opts - configuration options for target + * @return {visitors.Configuration} + **/ + _target(opts) { + extend(true, this.command, { + target: _.reduce(opts, (memo, cfg, name) => { + memo[name] = _.pick(cfg, Configuration.cliOptions); + return memo; + }, opts) + }); + return this; + } + + /** + * Sets Logger level + * @private + * @param {String} level - logger level + * @return {visitors.Configuration} + **/ + _logger(level) { + logger.level(Logger.level[level]); + return this; + } + + /** + * Visit Strategy + * @public + * @override + * @param {util.visitor.Visited} vi - instance to be visited by this visitor + * @param {Any} [...args] - arguments passed to the visitor + * @return {util.visitor.Visited} + **/ + visit(vi, ...args) { + return this.validate(vi) ? extend(false, vi, { configuration: this }) : vi; + } + + /** + * Parse Configuration based on source + * @public + * @return {Promise} + **/ + parse() { + Configuration.methods.forEach(_.bind(this._create, this)); + return this.queue.poll().then(_.bind(this.onParse, this), _.bind(this.onParseError, this)); + } + + /** + * Configuration Parse Complete Handler + * @public + * @param {Array} results - configuration results + * @return {visitors.Configuration} + **/ + onParse(results) { + _.each(_.compact(results), this.onOptions, this); + return this; + } + + /** + * Configuration options handler based on results + * @public + * @param {Object} result - result + * @return + **/ + onOptions(result) { + if(_.defined(result.warn)) return this.onParseError(result.warn); + this._source(result.source) + ._target(result.target) + ._logger(result.logLevel) + ._override(this.command.options); + return this; + } + + /** + * Configuration Parse Error Handler + * @public + * @param {String} message - error message reference + * @return {visitors.Configuration} + **/ + onParseError(message) { + logger(message).warn(); + return this; + } + + /** + * Resolves and return full formatter path + * @public + * @param {String} name - formatter name + * @return {String} + **/ + formatterPath(name) { + return `visitors/configuration/formatter/${name}`; + } + + /** + * Visitor Name + * @public + * @type {String} + **/ + get name() { + return 'ConfigurationVisitor'; + } + + /** + * Factory + * @static + * @type {util.factory.Factory} + **/ + static methods = [ + 'visitors/configuration/remote', + 'visitors/configuration/local' + ]; + + /** + * Factory Formatters for Options + * @static + * @type {Array} + **/ + static formatters = [ + 'visitors/configuration/formatter/alias', + 'visitors/configuration/formatter/exclude', + 'visitors/configuration/formatter/extensions', + 'visitors/configuration/formatter/target' + ]; + + /** + * CLI Argument Options + * @static + * @type {Array} + **/ + static cliOptions = ['scan', 'exclude', 'extensions', 'alias', 'target', 'destination', 'format']; + + /** + * Configuration Events + * @static + * @type {Object} + **/ + static events = { + /** + * @event parse + **/ + parse: 'visitors:configuration:parse' + }; + + /** + * Static Constructor + * @static + * @override + * @param {Any} [...args] - constructor arguments + * @return {visitors.Configuration} + **/ + static new(...args) { + Factory.registerAll(Configuration.methods.concat(Configuration.formatters)); + return new this(...args); + } + +} + +export default Configuration; diff --git a/lib/visitors/configuration/formatter/alias.es6 b/lib/visitors/configuration/formatter/alias.es6 new file mode 100644 index 0000000..14cb930 --- /dev/null +++ b/lib/visitors/configuration/formatter/alias.es6 @@ -0,0 +1,16 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Alias Formatter +* @static +* @param {String} [input = ''] - input to format +* @return {Object} +**/ +export default (input = '') => { + if(input.length === 0) return {}; + return _.chain(input.split(',')).invoke('split', ':').object().value(); +}; diff --git a/lib/visitors/configuration/formatter/exclude.es6 b/lib/visitors/configuration/formatter/exclude.es6 new file mode 100644 index 0000000..6e1d260 --- /dev/null +++ b/lib/visitors/configuration/formatter/exclude.es6 @@ -0,0 +1,15 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Exclude Formatter +* @static +* @param {String} [input = ''] - input to format +* @return {Object} +**/ +export default (input = '') => { + return (_.isString(input) && input.length > 0) ? input.split(',') : []; +}; diff --git a/lib/visitors/configuration/formatter/extensions.es6 b/lib/visitors/configuration/formatter/extensions.es6 new file mode 100644 index 0000000..f639ced --- /dev/null +++ b/lib/visitors/configuration/formatter/extensions.es6 @@ -0,0 +1,15 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Extensions Formatter +* @static +* @param {String} [input = ''] - input to format +* @return {Object} +**/ +export default (input = '') => { + return (_.isString(input) && input.length > 0) ? input.split(',') : []; +}; diff --git a/lib/visitors/configuration/formatter/target.es6 b/lib/visitors/configuration/formatter/target.es6 new file mode 100644 index 0000000..48044a7 --- /dev/null +++ b/lib/visitors/configuration/formatter/target.es6 @@ -0,0 +1,60 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; + +/** +* Supported Formats +* @const +* @private +* @type {Array} +**/ +const formats = ['ifie', 'umd', 'amd', 'cjs']; + +/** +* Validate Format +* @const +* @param {String} format - module format +* @return {Boolean} +**/ +const valid = (format) => { + return _.contains(formats, format); +}; + +/** +* Create Target +* @const +* @param {Object} m - memoized object version to export +* @return {Object} +**/ +const target = (m, v, ps) => { + m[v[0]] = { destination: ps[1], format: ps[0] }; + return m; +}; + +/** +* Create Reducer +* @const +* @param {Object} m - memoized object version to export +* @return {Object} +**/ +const create = (m, v) => { + if(!_.defined(v[1])) return m; + let ps = v[1].split(':'); + return valid(ps[0]) ? target(m, v, ps) : m; +}; + +/** +* Target Formatter +* @static +* @param {String} [input = ''] - input to format +* @return {Object} +**/ +export default (input = '') => { + return (_.isString(input) && input.length > 0) ? + _.chain(input.split(',')) + .invoke('split', '>') + .reduce(create, {}) + .value() : {}; +}; diff --git a/lib/visitors/configuration/local.es6 b/lib/visitors/configuration/local.es6 new file mode 100644 index 0000000..16e3af8 --- /dev/null +++ b/lib/visitors/configuration/local.es6 @@ -0,0 +1,135 @@ +/** +* @module visitors.configuration +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import fs from 'fs-extra'; +import { resolve } from 'path'; +import _ from 'util/mixins'; +import extend from 'extend'; +import Visited from 'util/visitor/visited'; +import logger from 'util/logger/logger'; + +/** +* Class Local +* @extends {util.visitor.Visited} +* +* @uses {visitors.async.Asynchronous} +**/ +class Local extends Visited { + + /** + * Constructor + * @public + * @param {Command} command - command reference + * @return {visitors.configuration.Local} + **/ + constructor(command) { + return super({ command }); + } + + /** + * Resolves absolute path to a given file + * @public + * @param {String} file - file name to evaluate + * @return {String} + **/ + resolvePath(file) { + return resolve(this.command.cwd, file); + } + + /** + * Returns true if local configuration exists, false otherwise + * @public + * @param {String} file - file name to evaluate + * @return {Boolean} + **/ + exists(file) { + try { + return _.defined(file) && _.defined(fs.statSync(file)); + } catch(ex) { + logger(`LocalConfiguration: ${ex.message}`).debug({}, logger.yellow); + return false; + } + } + + /** + * Will try to open configuration as a json + * @public + * @param {String} path - resolved path to configuration + * @return {Object} + **/ + tryJson(path) { + try { + return fs.readJsonSync(path); + } catch(ex) { + logger(`LocalConfiguration: ${ex.message}`).debug({}, logger.yellow); + return null; + } + } + + /** + * Will try to open configuration as javascript via module.exports + * @public + * @param {String} path - resolved path to configuration + * @return {Object} + **/ + tryJs(path) { + try { + return require(path); + } catch(ex) { + logger(`LocalConfiguration: ${ex.message}`).debug({}, logger.yellow); + return null; + } + } + + /** + * Outputs a warn message and returns the object + * @public + * @param {String} message - warning message + * @return {Object} + **/ + warn(warn) { + logger(warn).debug({}, logger.yellow); + return { warn }; + } + + /** + * Load local configuration file + * @public + * @param {String} name - file name + * @return {Object} + **/ + load(name) { + let file = this.resolvePath(name), output = ''; + if(!this.exists(file)) return this.warn(Local.messages.notFound); + if(!(output = (this.tryJson(file) || this.tryJs(file)))) return this.warn(Local.messages.invalid); + return output; + } + + /** + * Asynchronous next strategy + * @public + * @override + * @param adt {util.proxy.Asynchronous} adt used for asynchronous operations + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @return {Promise} + **/ + next(adt, resolve, reject) { + const { config } = this.command.getOptions(); + return resolve(this.load(config)); + } + + /** + * Local Configuration Messages + * @static + * @type {Object} + **/ + static messages = { + notFound: `Local configuration not found`, + invalid: `Local configuration: sqbox file is invalid` + } + +} + +export default Local; diff --git a/lib/visitors/configuration/remote.es6 b/lib/visitors/configuration/remote.es6 new file mode 100644 index 0000000..63256bb --- /dev/null +++ b/lib/visitors/configuration/remote.es6 @@ -0,0 +1,94 @@ +/** +* @module visitors.configuration +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'util/mixins'; +import extend from 'extend'; +import request from 'request-promise'; +import Visited from 'util/visitor/visited'; + +/** +* Class Remote +* @extends {util.visitor.Visited} +* +* @uses {visitors.async.Asynchronous} +**/ +class Remote extends Visited { + + /** + * Constructor + * @public + * @param {Command} command - command reference + * @return {visitors.configuration.Remote} + **/ + constructor(command) { + return super({ command }); + } + + /** + * Load remote configuration file from url + * @public + * @param {String} url - url to load remote configuration from + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @return {Promise} + **/ + load(url, resolve, reject) { + return request.get(url, { json: true }) + .then(_.bind(this.onResponse, this, resolve, reject)) + .catch(_.bind(this.onResponseError, this, resolve, reject)); + } + + /** + * Remote Response Success + * @public + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @param {Object} response - request response + * @return {visitors.configuration.Remote} + **/ + onResponse(resolve, reject, response) { + return resolve(response); + } + + /** + * Remote Response Error + * @public + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @param {Object} err - response error + * @return {Object} + **/ + onResponseError(resolve, reject, err) { + return resolve({ warn: Remote.messages.error({ err }) }); + } + + /** + * Asynchronous next strategy + * @public + * @override + * @param adt {util.proxy.Asynchronous} adt used for asynchronous operations + * @param resolve {Function} asynchronous promise's resolve + * @param reject {Function} asynchronous promise's reject + * @return {Promise} + **/ + next(adt, resolve, reject) { + const { url } = this.command.getOptions(); + return _.defined(url) ? + this.load(url, resolve, reject) : + resolve({ warn: Remote.messages.noUrl }); + } + + /** + * Remote Configuration Messages + * @static + * @type {Object} + **/ + static messages = { + noUrl: `No Url specified`, + error: _.template(`Remote Url Error - <%= err %>`) + } + +} + +export default Remote; diff --git a/lib/visitors/formatter/json.es6 b/lib/visitors/formatter/json.es6 new file mode 100644 index 0000000..b6b7a90 --- /dev/null +++ b/lib/visitors/formatter/json.es6 @@ -0,0 +1,76 @@ +/** +* @module visitors.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import Visitor from 'util/visitor/visitor'; +import InterfaceException from 'util/exception/proxy/interface'; + +/** +* Class JSON +* @extends {util.visitor.Visitor} +**/ +class Json extends Visitor { + + /** + * Constructor + * @public + * @override + * @param {Any} [...args] - constructor arguments + * @return {visitors.formatter.Json} + **/ + constructor(...args) { + return super(); + } + + /** + * Reducer Strategy to iterate over properties + * @public + * @param {Object} m - memoized object reference + * @param {Any} v - current object's value + * @param {String} k - current object's key + * @return {Object} + **/ + _filterObject(m, v, k) { + if(_.isAdt(v)) return this._clean(v, v); + if(!_.isFunction(v)) { m[k] = v; return m; } + if(_.isArray(m)) m.splice(k, 1) + if(_.isRealObject(m)) delete m[k]; + return m; + } + + /** + * Clean Functions from JSON representation + * @public + * @param {Any} current - current object + * @param {Object} [memo = {}] - memoized object + * @return {Object} + **/ + _clean(current, memo = {}) { + let keys = Object.getOwnPropertyNames(current); + return _.reduce(keys, (m, k) => this._filterObject(m, current[k], k), memo, this); + } + + /** + * Returns a json representation of the instance of this class + * This method uses recursion + * @public + * @param {visitors.formatter.Json} [ctx] - context reference + * @return {Object} + **/ + toJSON(ctx) { + return JSON.parse(JSON.stringify(this._clean(ctx))); + } + + /** + * Visitor Name + * @public + * @type {String} + **/ + get name() { + return 'JsonVisitor'; + } + +} + +export default Json; diff --git a/lib/visualize/graph.es6 b/lib/visualize/graph.es6 new file mode 100644 index 0000000..22b6811 --- /dev/null +++ b/lib/visualize/graph.es6 @@ -0,0 +1,29 @@ +/** +* @module visualize +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import extend from 'extend'; +import Command from 'command'; + +/** +* Class Graph +* @extends {Command} +**/ +class Graph extends Command { + + /** + * Run + * @public + * @override + * @return {visualize.Graph} + **/ + run() { + // TODO + console.log('Graph.run()...'); + return super.run(); + } + +} + +export default Graph; diff --git a/mocha.opts b/mocha.opts index 8a086db..a9837db 100644 --- a/mocha.opts +++ b/mocha.opts @@ -1,6 +1,8 @@ --require ./test/global +--require babel-register +--require babel-polyfill --recursive --ui bdd ---timeout 500 +--timeout 2000 --reporter spec --compilers es6:babel-register diff --git a/package.json b/package.json index 7ac680d..16e4d6c 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,18 @@ "sqbox": "bin/sqbox" }, "scripts": { - "test": "_mocha --opts mocha.opts --watch test/commands", + "test": "_mocha --opts mocha.opts --watch test/lib", "it": "_mocha --opts mocha.opts test/integration", - "coverage": "istanbul cover ./node_modules/.bin/_mocha -- --opts mocha.opts test/commands", + "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=html --reporter=text mocha --opts mocha.opts test/lib", "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls", - "docs": "esdoc", - "build": "node register" + "docs": "esdoc" + }, + "nyc": { + "require": [ + "babel-register" + ], + "sourceMap": true, + "instrument": false }, "files": [ "bin", @@ -30,24 +36,27 @@ "license": "MIT", "devDependencies": { "babel-cli": "6.23.0", - "babel-core": "6.23.1", - "babel-plugin-module-resolver": "2.5.0", - "babel-preset-es2015": "6.22.0", - "babel-preset-stage-2": "6.22.0", - "babel-register": "6.23.0", - "chai": "^3.5.0", - "coveralls": "^2.11.16", + "babel-plugin-istanbul": "4.0.0", + "chai": "3.5.0", + "coveralls": "2.11.16", + "cross-env": "3.1.4", "esdoc": "0.5.2", "esdoc-es7-plugin": "0.0.3", - "istanbul": "1.1.0-alpha.1", "mocha": "2.5.3", "mocha-lcov-reporter": "1.3.0", - "sinon": "1.17.7" + "nyc": "10.1.2", + "sinon": "1.17.7", + "sinon-stub-promise": "4.0.0" }, "dependencies": { - "async": "^2.1.5", + "acorn": "4.0.11", + "babel-plugin-module-resolver": "2.5.0", + "babel-polyfill": "6.23.0", + "babel-preset-es2015": "6.22.0", + "babel-preset-stage-2": "6.22.0", + "babel-register": "6.23.0", "chalk": "1.1.3", - "esprima": "3.1.3", + "d3": "4.7.3", "extend": "3.0.0", "fs-extra": "2.0.0", "glob": "7.1.1", @@ -55,6 +64,7 @@ "minimatch": "3.0.3", "node-watch": "0.4.1", "request": "2.79.0", + "request-promise": "4.2.0", "underscore": "1.8.3", "underscore.string": "3.3.4", "yargs": "6.6.0" diff --git a/release.js b/release.js deleted file mode 100644 index 2bfe03a..0000000 --- a/release.js +++ /dev/null @@ -1,6 +0,0 @@ -/** -* Release Squarebox -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -require('babel-register')(); -require('./src/build/build'); diff --git a/src/build/build.es6 b/src/build/build.es6 deleted file mode 100644 index a8613cc..0000000 --- a/src/build/build.es6 +++ /dev/null @@ -1,49 +0,0 @@ -/** -* @module build -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import Package from '../../../package.json'; -import Command from '../commands/command'; -import _ from 'underscore'; -import extend from 'extend'; -import yargs from 'yargs'; - -/** -* Class Build -* @extends command.Command -**/ -class Build extends Command { - - /** - * Build Run - * @public - * @override - * @return build.Build - **/ - run() { - return this; - } - - /** - * Setup Yargs - * @static - * @override - * @return build.Build - **/ - static setup() { - Command.setup(); - return this; - } - - /** - * Static Run - * @static - * @return build.Build - **/ - static run() { - return this.setup().new(this.args()); - } - -} - -export default Build.run(); diff --git a/src/commands/bin/sqbox.es6 b/src/commands/bin/sqbox.es6 deleted file mode 100644 index 7761fd5..0000000 --- a/src/commands/bin/sqbox.es6 +++ /dev/null @@ -1,65 +0,0 @@ -/** -* @module commands.bin -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import Command from 'commands/command'; -import extend from 'extend'; -import yargs from 'yargs'; - -/** -* Class SquareBox -* @extends commands.Command -**/ -class SquareBox extends Command { - - /** - * Command Defaults - * @static - * @type {Object} - **/ - static defaults = extend(true, Command.defaults, { - 'config': '.squarebox.json', - 'source-scan': './src', - 'source-extensions': ['.js', '.es6', '.es'], - 'source-alias': , - 'target-destination': './dist', - 'target-format': 'ifie' - }); - - /** - * Command options - * @static - * @type {Array} - **/ - static options = Command.options.concat([ - 'config', - 'source-scan', - 'source-extensions' - 'source-alias', - 'target-destination', - 'target-format' - ]); - - /** - * Setup Yargs - * @static - * @override - * @return - **/ - static setup() { - Command.setup(); - return this; - } - - /** - * Static Run - * @static - * @return commands.bin.SquareBox - **/ - static run() { - return this.setup().new(this.args()); - } - -} - -export default SquareBox.run(); diff --git a/src/commands/bundle/bundle.es6 b/src/commands/bundle/bundle.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/clean/clean.es6 b/src/commands/clean/clean.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/command.es6 b/src/commands/command.es6 deleted file mode 100644 index 2e7a94f..0000000 --- a/src/commands/command.es6 +++ /dev/null @@ -1,182 +0,0 @@ -/** -* @module commands -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import { EventEmitter } from 'events'; -import './util/mixins'; -import _ from 'underscore'; -import extend from 'extend'; -import yargs from 'yargs'; -import JSON from './util/proxy/json'; -import Collection from './util/adt/collection'; -import CommandException from './util/exception/command/command'; - -/** -* Class Command -* @extends {events.EventEmitter} -* -* @uses {commands.util.proxy.JSON} -**/ -export default class Command extends EventEmitter { - - /** - * Commands Collection - * @private - * @type {commands.util.adt.Collection} - **/ - _commands = new Collection([], { interface: Command }) - - /** - * Constructor - * @public - * @param [args = {}] {Object} Constructor arguments - * @return {commands.Command} - **/ - constructor(args) { - super(); - return JSON.proxy(extend(true, this, this.defaults, _.pick(args, this.constructor.options))); - } - - /** - * Command Chainning - * @public - * @throws {commands.util.exceptions.CommandException} - * @param command {commands.Command} command used to chain - * @return {commands.Command} - **/ - chain(command) { - if(!_.defined(command) || !_.instanceOf(command, Command)) - throw CommandException.new({ type: 'chain', level: CommandException.fatal }); - this._commands.add(command); - return this; - } - - /** - * Default Command Run - * @public - * @return {commands.Command} - **/ - run() { - return this; - } - - /** - * Retrieves source - * @public - * @return {Object} - **/ - source() { - return this.source; - } - - /** - * Retrieves and resolves scan directory (glob) - * @public - * @return {String} - **/ - scan() { - return this.source().scan; - } - - /** - * Retrieves list of extensions to scan - * @public - * @return {Array} - **/ - extensions() { - return this.source().extensions; - } - - /** - * Retrieves aliases for modules - * @public - * @return {Object} - **/ - alias() { - return this.source().alias; - } - - /** - * Retrieves target - * @public - * @return {Object} - **/ - target() { - return this.target; - } - - /** - * Retrieves and resolves destination - * @public - * @return {String} - **/ - destination() { - return this.target().destination; - } - - /** - * Retrieves export format - * @public - * @return {String} - **/ - format() { - return this.target().format; - } - - /** - * Retrieves Yargs Arguments - * @public - * @return {Object} - **/ - args() { - return yargs.argv; - } - - /** - * Command Defaults - * @static - * @type {Object} - **/ - static defaults = { - env: 'development' - }; - - /** - * Command options - * @static - * @type {Array} - **/ - static options = [ - 'env' - ]; - - /** - * Command Events - * @static - * @type {Object} - **/ - static events = { - start: 'commands:command:start', - end: 'commands:command:end' - }; - - /** - * Default Yargs setup - * @static - * @return {commands.Command} - **/ - static setup() { - return this; - } - - /** - * Static Constructor - * @static - * @param [...agrs] {Any} constructor arguments - * @return {commands.Command} - **/ - static new(...args) { - return new this(...args); - } - -} diff --git a/src/commands/help/help.es6 b/src/commands/help/help.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/util/exception/adt/queue.es6 b/src/commands/util/exception/adt/queue.es6 deleted file mode 100644 index 89a2536..0000000 --- a/src/commands/util/exception/adt/queue.es6 +++ /dev/null @@ -1,26 +0,0 @@ -/** -* @module commands.util.adt.exception -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; -import extend from 'extend'; -import Exception from '../exception'; - -/** -* Class QueueException -* @extends {commands.util.exception.Exception} -**/ -export default class QueueException extends Exception { - - /** - * Command Exception types - * @public - * @type {Object} - **/ - static type = extend(true, Exception.type, { - capacityUndefined: _.template(`Queue requires a 'capacity' property in order to be instanciate it`), - capacityViolation: _.template(`Queue element's collection passed overflows the current capacity - <%= capacity %>`) - }); - -} diff --git a/src/commands/util/exception/command/command.es6 b/src/commands/util/exception/command/command.es6 deleted file mode 100644 index f51be4e..0000000 --- a/src/commands/util/exception/command/command.es6 +++ /dev/null @@ -1,24 +0,0 @@ -/** -* @module commands.util.exception.command -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; -import extend from 'extend'; -import Exception from '../exception'; - -/** -* Class CommandException -* @extends {commands.util.exception.Exception} -**/ -export default class CommandException extends Exception { - - /** - * Command Exception types - * @public - * @type {Object} - **/ - static type = extend(true, Exception.type, { - chain: _.template('Required parameter `command` to be an instance of `commands.Command`') - }); - -} diff --git a/src/commands/util/exception/exception.es6 b/src/commands/util/exception/exception.es6 deleted file mode 100644 index e9ec61b..0000000 --- a/src/commands/util/exception/exception.es6 +++ /dev/null @@ -1,51 +0,0 @@ -/** -* @module commands.util.exception -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; -import extend from 'extend'; -import Logger from '../logger/logger'; - -/** -* Class Exception -* @extends {Error} -**/ -export default class Exception extends Error { - - /** - * Constructor - * @public - * @param [args = { type: 'unknown' }] {Object} constructor attribute - **/ - constructor(args = { type: 'unknown' }) { - super({ message: Command.type[args.type] }); - return extend(true, this, _.pick(args, 'level')); - } - - /** - * Exception Message - * @public - * @type {String} - **/ - get message() { - // TODO - } - - /** - * Command Exception types - * @public - * @type {Object} - **/ - static type = { - unknown: _.template('Unknown Exception') - } - - /** - * @static - * @param [...args] {Any} - **/ - static new(...args) { - return new this(...args); - } - -} diff --git a/src/commands/util/exception/proxy/interface.es6 b/src/commands/util/exception/proxy/interface.es6 deleted file mode 100644 index c3d34b7..0000000 --- a/src/commands/util/exception/proxy/interface.es6 +++ /dev/null @@ -1,24 +0,0 @@ -/** -* @module commands.util.exception.proxy -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; -import extend from 'extend'; -import Exception from '../exception'; - -/** -* Class InterfaceException -* @extends {commands.util.exception.Exception} -**/ -export default class InterfaceException extends Exception { - - /** - * Command Exception types - * @public - * @type {Object} - **/ - static type = extend(true, Exception.type, { - proxy: _.template(`Proxies require a 'target' in which their interface operates on`) - }); - -} diff --git a/src/commands/util/logger/logger.es6 b/src/commands/util/logger/logger.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/util/proxy/json.es6 b/src/commands/util/proxy/json.es6 deleted file mode 100644 index 9d80bf8..0000000 --- a/src/commands/util/proxy/json.es6 +++ /dev/null @@ -1,89 +0,0 @@ -/** -* @module commands.util.interface -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; - -/** -* Interface JSON -**/ -class Json { - - /** - * Proxy's `getPrototypeOf` trap strategy - * @public - * @param target {Any} proxy's target - * @return {Object} - **/ - getPrototypeOf(target) { - return target.constructor.prototype; - } - - /** - * Proxy's `get` trap strategy - * @public - * @param target {Any} proxy target - * @param property {String} property name - * @param receiver {Any} proxy receiver - * @return {Any} - **/ - get(target, property, receiver) { - let value = _.defined(this[property]) ? this[property] : target[property]; - return _.isFunction(value) ? _.bind(value, target, this) : value; - } - - /** - * Reducer Strategy to iterate over properties - * @public - * @param m {Object} memoized object reference - * @param v {Any} current object's value - * @param k {String} current object's key - * @return {Object} - **/ - _reduce(m, v, k) { - if(_.isAdt(v)) this._clean(v, v); - if(!_.isFunction(v)) { m[k] = v; return m; } - if(_.isArray(m)) m.splice(k, 1) - if(_.isRealObject(m)) delete m[k]; - return m; - } - - /** - * Clean Functions from JSON representation - * @public - * @param current {Any} current object - * @param [memo = {}] {Object} memoized object - * @return {Object} - **/ - _clean(current, memo = {}) { - return _.reduce(current, this._reduce, memo, this); - } - - /** - * Returns a json representation of the instance of this class - * This method uses recursion - * @public - * @param ctx {commands.util.proxy.Json} reference to the instance of this class - * @return {Object} - **/ - toJSON(ctx) { - return JSON.parse(JSON.stringify(ctx._clean(this))); - } - - /** - * Proxifies a given target with an instance of this class - * @static - * @throws {commands.util.exception.proxy.InterfaceException} - * @param target {Any} instance to proxify - * @param [...args] {Any} constructor arguments - * @return {commands.util.proxy.Json} - **/ - static proxy(target, ...args) { - if(!_.defined(target)) - throw InterfaceException.new({ type: 'proxy', level: InterfaceException.fatal }); - return new Proxy(target, new this(...args)); - } - -} - -export default Json; diff --git a/src/commands/visualize/visualize.es6 b/src/commands/visualize/visualize.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/test/commands/util/adt/collection.spec.es6 b/test/commands/util/adt/collection.spec.es6 deleted file mode 100644 index ffebec3..0000000 --- a/test/commands/util/adt/collection.spec.es6 +++ /dev/null @@ -1,233 +0,0 @@ -/** -* @module commands.util.adt -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import Collection from 'commands/util/adt/collection'; -import Command from 'commands/command'; - -describe('commands.util.adt.Collection', function() { - - before(() => { - this.sandbox = sinon.sandbox.create(); - }); - - beforeEach(() => { - this.mockCollection = this.sandbox.mock(Collection.prototype); - }); - - afterEach(() => { - this.sandbox.restore(); - delete this.mockCollection; - }); - - after(() => { - delete this.sandbox; - }); - - describe('constructor()', () => { - - it('Should get a new instance', () => { - assert.instanceOf(Collection.new(), Collection); - }); - - it('Should get a new instance with elements', () => { - const exp = Collection.new([{ option: 1 }, { option: 2}]); - assert.isFalse(exp.isEmpty()); - assert.equal(2, exp.size()); - }); - - it('Should get a new instance with elements (interface)', () => { - const exp = Collection.new([{ option: true }, { option: false }], { interface: Command }); - assert.instanceOf(exp.get(0), Command); - }); - - }); - - describe('set()', () => { - - it('Should set new elements (with Event)', () => { - const exp = Collection.new(); - const toSet = [1, 2, 3]; - const expReset = this.mockCollection.expects('reset') - .once() - .withArgs({ silent: true }) - .returns(exp); - - const expEmit = this.mockCollection.expects('emit') - .once() - .withArgs(Collection.events.set, exp, toSet) - .returns(exp); - - exp.set(toSet); - - assert.equal(3, exp.size()); - assert.equal(toSet[1], exp.get(1)); - - this.mockCollection.verify(); - }); - - it('Should set new elements (without Event)', () => { - const exp = Collection.new(); - const toSet = [1, 2, 3]; - const expReset = this.mockCollection.expects('reset') - .once() - .withArgs({ silent: true }) - .returns(exp); - - const expEmit = this.mockCollection.expects('emit').never(); - - exp.set(toSet, { silent: true }); - - assert.equal(3, exp.size()); - assert.equal(toSet[1], exp.get(1)); - - this.mockCollection.verify(); - }); - - it('Should NOT set new elements', () => { - const exp = Collection.new(); - const expReset = this.mockCollection.expects('reset').never(); - - assert.instanceOf(exp.set(), Collection); // undefined - assert.instanceOf(exp.set({ option: 'notAnArray' }), Collection); // not an array - - this.mockCollection.verify(); - }); - - }); - - describe('add()', () => { - - it('Should add a new element (with Event)', () => { - const toAdd = { env: 'production' }; - const exp = Collection.new([], { interface: Command }); - const expEmit = this.mockCollection.expects('emit') - .once() - .withArgs(Collection.events.add, exp, toAdd) - .returns(toAdd); - - exp.add(toAdd); - assert.isFalse(exp.isEmpty()); - assert.instanceOf(exp.get(0), Command); - - this.mockCollection.verify(); - }); - - it('Should add a new element (without Event)', () => { - const toAdd = { env: 'production' }; - const exp = Collection.new([], { interface: Command }); - const expEmit = this.mockCollection.expects('emit').never(); - - exp.add(toAdd, { silent: true }); - assert.isFalse(exp.isEmpty()); - assert.instanceOf(exp.get(0), Command); - - this.mockCollection.verify(); - }); - - it('Should NOT add a new element', () => {}); - - }); - - describe('addAll()', () => { - - it('Should add new elements', () => {}); - it('Should NOT add new elements', () => {}); - - }); - - describe('get()', () => { - - it('Should get an element', () => {}); - it('Should NOT get an element', () => {}); - - }); - - describe('contains()', () => { - - it('Should contain an element', () => {}); - it('Should NOT contain an element', () => {}); - - }); - - describe('containsAl()', () => { - - it('Should contain all elements', () => {}); - it('Should NOT contain at least one element', () => {}); - - }); - - describe('containsWhere()', () => { - - it('Should contain an element with condition', () => {}); - it('Should NOT contain an element with condition', () => {}); - - }); - - describe('remove()', () => { - - it('Should remove an element', () => {}); - it('Should NOT remove an element', () => {}); - - }); - - describe('removeAll()', () => { - - it('Should remove all the elements', () => {}); - it('Should NOT remove all the elements', () => {}); - - }); - - describe('removeBy()', () => { - - it('Should remove elments by predicate', () => {}); - it('Should NOT remove elements by predicate', () => {}); - - }); - - describe('sort()', () => { - - it('Should sort by comparator', () => {}); - it('Should NOT sort', () => {}); - - }); - - describe('iterator()', () => { - - it('Should get an iterator from collection', () => {}); - - }); - - describe('reset()', () => { - - it('Should reset the collection', () => {}); - - }); - - describe('size()', () => { - - it('Should get the size of the collection', () => {}); - - }); - - describe('isEmpty()', () => { - - it('Should be empty', () => {}); - it('Should NOT be empty', () => {}); - - }); - - describe('hasInterface()', () => { - - it('Should have interface defined', () => {}); - it('Should NOT have interface defined', () => {}); - - }); - - describe('toJSON()', () => { - - it('Should get a json representation', () => {}); - - }); - -}); diff --git a/test/commands/util/adt/queue.spec.es6 b/test/commands/util/adt/queue.spec.es6 deleted file mode 100644 index e69de29..0000000 diff --git a/test/commands/util/proxy/json.spec.es6 b/test/commands/util/proxy/json.spec.es6 deleted file mode 100644 index 24458c6..0000000 --- a/test/commands/util/proxy/json.spec.es6 +++ /dev/null @@ -1,63 +0,0 @@ -/** -* @module commands.util.proxy -* @author Patricio Ferreira <3dimentionar@gmail.com> -**/ -import _ from 'underscore'; -import Json from 'commands/util/proxy/json'; -import Command from 'commands/command'; - -describe('commands.util.proxy.Json', function() { - - before(() => { - this.sandbox = sinon.sandbox.create(); - }); - - beforeEach(() => { - this.target = () => { - return { - a: 1, - b: true, - regexp: new RegExp(), - func: () => {}, - obj: { o_a: 1, o_b: true, func: () => {}, arr: [1, true, () => {}] }, - arr: [{ - a_o_a: 1, - a_o_b: true, - func: () => {}, - command: Command.new() - }, 1, true, () => {}] - }; - }; - this.mockJson = this.sandbox.mock(Json.prototype); - }); - - afterEach(() => { - this.sandbox.restore(); - delete this.target; - delete this.mockJson; - }); - - after(() => { - delete this.sandbox; - }); - - describe('#constructor', () => { - - it('Should get a new instance', () => { - assert.instanceOf(Json.proxy(this.target), Function); - }); - - }); - - describe('#toJSON()', () => { - - it('Should return a json representation', () => { - const o = this.target(); - const exp = Json.proxy(o); - const out = exp.toJSON(); - assert.notEqual(o, out); - }); - - }); - -}); diff --git a/test/global.js b/test/global.js index 4e205ff..e9b957f 100644 --- a/test/global.js +++ b/test/global.js @@ -4,6 +4,8 @@ **/ global.fs = require('fs-extra'); global.path = require('path'); +global._ = require('underscore'); global.sinon = require('sinon'); global.assert = require('chai').assert; -global.basepath = path.join(path.resolve(__dirname, '..'), 'src'); +global.basepath = path.join(path.resolve(__dirname, '..'), 'lib'); +require('sinon-stub-promise')(sinon); diff --git a/test/lib/bin/sqbox.spec.es6 b/test/lib/bin/sqbox.spec.es6 new file mode 100644 index 0000000..9b16533 --- /dev/null +++ b/test/lib/bin/sqbox.spec.es6 @@ -0,0 +1,69 @@ +/** +* @module bin +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import SquareBox from 'bin/sqbox'; +import Commander from 'visitors/commander'; + +describe('bin.SquareBox', function() { + + before(() => { + this.cwd = path.resolve(process.cwd()); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + if(this.sqbox) this.mockProto = this.sandbox.mock(this.sqbox); + this.mockCommander = this.sandbox.mock(Commander.prototype); + this.input = [process.argv[0], this.cwd]; + }); + + afterEach(() => { + if(this.mockProto) this.mockProto.verify(); + this.mockCommander.verify(); + + this.sandbox.restore(); + + delete this.mockCommander; + delete this.mockProto; + delete this.input; + }); + + after(() => { + delete this.cwd; + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get an instance', () => { + this.sqbox = require('bin/sqbox').default.new(); + assert.instanceOf(this.sqbox, SquareBox); + }); + + }); + + describe('static->run()', () => { + + it('Should run the command', () => { + this.input = this.input.concat([ + 'sqbox', + 'bundle', + '--config', 'test/specs/.sqboxrc', + '--s', './source/**', + '--x', './source/dependencies/**,./source/package/**', + '--e', '.js,.es6', + '--a', 'common:./path/common', + '--t', 'add>umd:./dist/umd,other>cjs:./dist/cjs' + ]); + + this.mockCommander.expects('_args') + .once() + .returns(this.input); + + assert.instanceOf(this.sqbox.run(), SquareBox); + }); + + }); + +}); diff --git a/test/commands/command.spec.es6 b/test/lib/command.spec.es6 similarity index 68% rename from test/commands/command.spec.es6 rename to test/lib/command.spec.es6 index 5135c3c..c94b1bb 100644 --- a/test/commands/command.spec.es6 +++ b/test/lib/command.spec.es6 @@ -1,10 +1,10 @@ /** -* @module commands +* @module * @author Patricio Ferreira <3dimentionar@gmail.com> **/ -import Command from 'commands/command'; +import Command from 'command'; -describe('commands.Command', function() { +describe('Command', function() { before(() => { this.sandbox = sinon.sandbox.create(); @@ -23,7 +23,7 @@ describe('commands.Command', function() { delete this.sandbox; }); - describe('#constructor', () => { + describe('constructor()', () => { it('Should get a new instance', () => { assert.instanceOf(Command.new(), Command); @@ -31,10 +31,11 @@ describe('commands.Command', function() { }); - describe('#toJSON()', () => { + describe('toJSON()', () => { it('Should return a json representation', () => { - const exp = Command.new({ env: 'production' }); + const exp = Command.new({ env: 'production' }).toJSON(); + assert.property(exp, 'env'); }); }); diff --git a/test/lib/util/adt/collection.spec.es6 b/test/lib/util/adt/collection.spec.es6 new file mode 100644 index 0000000..f2e2d3e --- /dev/null +++ b/test/lib/util/adt/collection.spec.es6 @@ -0,0 +1,555 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Collection from 'util/adt/collection'; +import Command from 'command'; +import Iterator from 'util/adt/iterator'; + +describe('util.adt.Collection', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockStatic = this.sandbox.mock(Collection); + this.mockCollection = this.sandbox.mock(Collection.prototype); + }); + + afterEach(() => { + this.mockStatic.verify(); + this.mockCollection.verify(); + + this.sandbox.restore(); + + delete this.mockStatic; + delete this.mockCollection; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + assert.instanceOf(Collection.new(), Collection); + }); + + it('Should get a new instance with elements', () => { + const exp = Collection.new([{ option: 1 }, { option: 2}]); + assert.isFalse(exp.isEmpty()); + assert.equal(2, exp.size()); + }); + + it('Should get a new instance with elements (interface)', () => { + const exp = Collection.new([{ option: true }, { option: false }], { interface: Command }); + assert.instanceOf(exp.get(0), Command); + }); + + }); + + describe('set()', () => { + + it('Should set new elements (with Event)', () => { + const exp = Collection.new(); + const toSet = [1, 2, 3]; + const expReset = this.mockCollection.expects('reset') + .once() + .withArgs({ silent: true }) + .returns(exp); + + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.set, exp, toSet) + .returns(exp); + + exp.set(toSet); + + assert.equal(3, exp.size()); + assert.equal(toSet[1], exp.get(1)); + }); + + it('Should set new elements (without Event)', () => { + const exp = Collection.new(); + const toSet = [1, 2, 3]; + const expReset = this.mockCollection.expects('reset') + .once() + .withArgs({ silent: true }) + .returns(exp); + + const expEmit = this.mockCollection.expects('emit').never(); + + exp.set(toSet, { silent: true }); + + assert.equal(3, exp.size()); + assert.equal(toSet[1], exp.get(1)); + }); + + it('Should NOT set new elements', () => { + const exp = Collection.new(); + const expReset = this.mockCollection.expects('reset').never(); + + assert.instanceOf(exp.set(), Collection); // undefined + assert.instanceOf(exp.set({ option: 'notAnArray' }), Collection); // not an array + }); + + }); + + describe('add()', () => { + + it('Should add a new element (with Event)', () => { + const toAdd = { env: 'production' }; + const exp = Collection.new([], { interface: Command }); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.add, exp, sinon.match.instanceOf(Command)) + .returns(sinon.match.instanceOf(Command)); + + exp.add(toAdd); + assert.isFalse(exp.isEmpty()); + assert.instanceOf(exp.get(0), Command); + }); + + it('Should add a new element (without Event)', () => { + const toAdd = { env: 'production' }; + const exp = Collection.new([], { interface: Command }); + const expEmit = this.mockCollection.expects('emit').never(); + + exp.add(toAdd, { silent: true }); + assert.isFalse(exp.isEmpty()); + assert.instanceOf(exp.get(0), Command); + }); + + it('Should add a new element (with custom new instanciation)', () => { + const toAdd = { env: 'production' }; + const _new = (attrs) => new Command({ env: 'prod' }); + const spyNew = this.sandbox.spy(_new); + const exp = Collection.new([], { interface: Command }); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.add, exp, sinon.match.instanceOf(Command)) + .returns(sinon.match.instanceOf(Command)); + + assert.instanceOf(exp.add(toAdd, { new: spyNew }), Command); + assert.equal(true, spyNew.calledOnce); + }); + + it('Should NOT add a new element', () => { + const exp = Collection.new(); + const expEmit = this.mockCollection.expects('emit').never(); + + assert.isNull(exp.add()); + }); + + }); + + describe('addAll()', () => { + + it('Should add new elements', () => { + const toAdd = [4,5]; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.addall, exp, toAdd) + .returns(exp); + + assert.instanceOf(exp.addAll(toAdd), Collection); + assert.isFalse(exp.isEmpty()); + assert.equal(5, exp.size()); + assert.equal(4, exp.get(3)); + }); + + it('Should NOT add new elements', () => { + const exp = Collection.new(); + const expEmit = this.mockCollection.expects('emit').never(); + + assert.instanceOf(exp.addAll(), Collection); + assert.instanceOf(exp.addAll({ invalid: true }), Collection); + }); + + }); + + describe('get()', () => { + + it('Should get an element', () => { + const exp = Collection.new(['hello', 'world']); + assert.equal('world', exp.get(1)); + assert.equal('hello', exp.get()); + }); + + it('Should NOT get an element', () => { + const exp = Collection.new(['hello', 'world']); + assert.isUndefined(exp.get(3)); + }); + + }); + + describe('contains()', () => { + + it('Should contain an element (without interface)', () => { + const exp = Collection.new([{ option: 1 }, { option: 2 }]); + assert.isTrue(exp.contains({ option: 2 })); + }); + + it('Should contain an element (with interface)', () => { + const exp = Collection.new([{ env: 'production' }, { env: 'staging' }], { interface: Command }); + assert.isTrue(exp.contains(exp.get(1).toJSON())); + }); + + it('Should NOT contain an element', () => { + const exp = Collection.new([{ option: 1 }, { option: 2 }]); + assert.isFalse(exp.contains({ option: 3 })); + assert.isFalse(exp.contains()); + }); + + }); + + describe('containsAll()', () => { + + it('Should contain all elements', () => { + const exp = Collection.new([{ option: 1 }, { option: 2 }, { option: 3 }]); + + assert.isTrue(exp.containsAll([{ option: 1 }, { option: 3 }])); + assert.isFalse(exp.containsAll([{ option: 1 }, { option: 2 }, { option: 3 }, { option: 4 }])); + }); + + it('Should NOT contain at least one element', () => { + const exp = Collection.new([{ option: 1 }, { option: 2 }, { option: 3 }]); + + assert.isFalse(exp.containsAll([{ option: 1 }, { value: 2 }])); + assert.isFalse(exp.containsAll([])); + assert.isFalse(exp.containsAll()); + }); + + }); + + describe('containsWhere()', () => { + + it('Should contain an element with condition', () => { + const exp = Collection.new([{ prop: 'A' }, { prop: 'B' }]); + assert.isTrue(exp.containsWhere({ prop: 'B' })); + assert.isTrue(exp.containsWhere()); + }); + + it('Should NOT contain an element with condition', () => { + const exp = Collection.new([{ prop: 'A' }, { prop: 'B' }]); + assert.isFalse(exp.containsWhere({ prop: 'C' })); + }); + + }); + + describe('removeAt()', () => { + + it('Should remove an element at index (without interface)', () => { + const toRemove = 1; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.remove, exp, toRemove) + .returns(exp); + + assert.equal(1, exp.removeAt(toRemove)); + assert.equal(2, exp.size()); + assert.equal(3, exp.get(1)); + }); + + it('Should remove an element at index (with interface)', () => { + const exp = Collection.new([{ env: 'staging' }, { env: 'production' }], { interface: Command }); + const toRemove = 1; + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.remove, exp, toRemove) + .returns(exp); + + assert.equal(toRemove, exp.removeAt(1)); + assert.equal(1, exp.size()); + }); + + it('Should NOT remove an element at index (default index)', () => { + const exp = Collection.new(); + const expEmit = this.mockCollection.expects('emit').never(); + assert.isNull(exp.removeAt()); + }); + + it('Should NOT remove an element at index (index not a number)', () => { + const exp = Collection.new(); + const expEmit = this.mockCollection.expects('emit').never(); + assert.isNull(exp.removeAt('hello')); + }); + + it('Should NOT remove an element at index (index is greater that size - 1)', () => { + const exp = Collection.new([1,2]); + const expEmit = this.mockCollection.expects('emit').never(); + assert.isNull(exp.removeAt(2)); + }); + + }); + + describe('remove()', () => { + + it('Should remove an element (without interface)', () => { + const toRemove = 2; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.remove, exp, [toRemove]) + .returns(exp); + + assert.equal(toRemove, exp.remove(toRemove)); + assert.equal(2, exp.size()); + assert.equal(3, exp.get(1)); + }); + + it('Should remove an element (with interface)', () => { + const exp = Collection.new([{ env: 'staging' }, { env: 'production' }], { interface: Command }); + const toRemove = exp.get(1); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.remove, exp, [toRemove]) + .returns(exp); + + assert.equal(toRemove, exp.remove(toRemove)); + assert.equal(1, exp.size()); + assert.notEqual(toRemove, exp.get(0)); + }); + + it('Should NOT remove an element (element is invalid)', () => { + const exp = Collection.new([1,2]); + const expEmit = this.mockCollection.expects('emit').never(); + assert.isNull(exp.remove()); + }); + + + }); + + describe('removeAll()', () => { + + it('Should remove all the elements', () => { + const toRemove = [1,2]; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.removeall, exp, toRemove) + .returns(exp); + + assert.instanceOf(exp.removeAll(toRemove), Collection); + assert.isFalse(exp.isEmpty()); + assert.equal(1, exp.size()); + assert.equal(3, exp.get(0)); + }); + + it('Should remove a few elements (matching)', () => { + const toRemove = [1,5,2]; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.removeall, exp, [1,2]) + .returns(exp); + + assert.instanceOf(exp.removeAll(toRemove), Collection); + assert.isFalse(exp.isEmpty()); + assert.equal(1, exp.size()); + assert.equal(3, exp.get(0)); + }); + + it('Should NOT remove all the elements', () => { + const toRemove = [4,5]; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.removeall, exp, []) + .returns(exp); + + assert.instanceOf(exp.removeAll(toRemove), Collection); + assert.isFalse(exp.isEmpty()); + assert.equal(3, exp.size()); + assert.equal(1, exp.get(0)); + }); + + it('Should NOT remove all the elements (elements not an array)', () => { + const toRemove = { invalid: 1 }; + const exp = Collection.new([1,2,3]); + const expEmit = this.mockCollection.expects('emit').never(); + + assert.instanceOf(exp.removeAll(toRemove), Collection); + assert.instanceOf(exp.removeAll(), Collection); + + assert.isFalse(exp.isEmpty()); + assert.equal(3, exp.size()); + }); + + }); + + describe('removeBy()', () => { + + it('Should remove elments by predicate', () => { + const predicate = (e) => (e.env === 'stage' || e.env === 'dev'); + let spyPredicate = this.sandbox.spy(predicate); + const exp = Collection.new([{ env: 'stage' }, { env: 'dev' }, { env: 'prod' }], { interface: Command }); + const expEmit = this.mockCollection.expects('emit') + .exactly(2) + .withArgs(Collection.events.remove, exp, sinon.match(0).or(sinon.match(1))) + .returns(sinon.match(0).or(sinon.match(1))); + + assert.instanceOf(exp.removeBy(spyPredicate), Collection); + assert.equal(1, exp.size()); + assert.equal('prod', exp.get(0).env); + assert.equal(true, spyPredicate.calledThrice); + }); + + it('Should NOT remove elements by predicate', () => { + const predicate = (v) => v === 3; + let spyPredicate = this.sandbox.spy(predicate); + const exp = Collection.new([1,2]); + const expEmit = this.mockCollection.expects('emit').never(); + + assert.instanceOf(exp.removeBy(spyPredicate), Collection); + assert.equal(2, exp.size()); + assert.equal(true, spyPredicate.calledTwice); + }); + + it('Should NOT remove elements by predicate (no predicate)', () => { + const exp = Collection.new([1,2]); + const expEmit = this.mockCollection.expects('emit').never(); + + assert.instanceOf(exp.removeBy(), Collection); + assert.equal(2, exp.size()); + }); + + }); + + describe('sort()', () => { + + it('Should sort by comparator', () => { + const exp = Collection.new([{ v: 3 }, { v: 1 }, { v: 2 }]); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.sort, exp) + .returns(exp); + + exp.sort((a, b) => (a.v - b.v)); + + assert.equal(1, exp.get(0).v); + assert.equal(2, exp.get(1).v); + assert.equal(3, exp.get(2).v); + }); + + it('Should sort using default', () => { + const exp = Collection.new(['Hello', '1 Hello', '2 World', 'World']); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.sort, exp) + .returns(exp); + + exp.sort(); + + assert.equal('1 Hello', exp.get(0)); + assert.equal('2 World', exp.get(1)); + assert.equal('Hello', exp.get(2)); + assert.equal('World', exp.get(3)); + }); + + }); + + describe('iterator()', () => { + + it('Should get an iterator from collection', () => { + assert.instanceOf(Collection.new([1,2,3]).iterator(), Iterator); + }); + + }); + + describe('reset()', () => { + + it('Should reset the collection', () => { + const exp = Collection.new(); + const expEmit = this.mockCollection.expects('emit') + .once() + .withArgs(Collection.events.reset, exp) + .returns(exp); + + exp.addAll([1,2], { silent: true }); + assert.equal(2, exp.size()); + + exp.reset(); + assert.equal(0, exp.size()); + }); + + }); + + describe('size()', () => { + + it('Should get the size of the collection', () => { + assert.equal(0, Collection.new().size()); + assert.equal(2, Collection.new([1,2]).size()); + }); + + }); + + describe('isEmpty()', () => { + + it('Should be empty', () => { + assert.isTrue(Collection.new().isEmpty()); + }); + + it('Should NOT be empty', () => { + assert.isFalse(Collection.new([1]).isEmpty()); + }); + + }); + + describe('hasInterface()', () => { + + it('Should have interface defined', () => { + const exp = Collection.new([{ env: 'staging' }, { env: 'production' }], { interface: Command }); + assert.isTrue(exp.hasInterface()); + }); + + it('Should NOT have interface defined', () => { + const exp = Collection.new([]); + assert.isFalse(exp.hasInterface()); + }); + + }); + + describe('toJSON()', () => { + + it('Should get a json representation (without interface)', () => { + const exp = Collection.new([1,2,'value']).toJSON(); + assert.isArray(exp); + assert.equal(3, exp.length); + assert.equal('value', exp[2]); + }); + + it('Should get a json representation (with interface)', () => { + const exp = Collection.new([{ env: 'staging' }, { env: 'production' }], { interface: Command }).toJSON(); + assert.isArray(exp); + assert.equal(2, exp.length); + assert.equal('production', exp[1].env); + }); + + }); + + describe('static->_aggregate()', () => { + + it('Should NOT aggregate underscore methods (already implemented)', () => { + const expUNDERSCORE = this.mockStatic.expects('UNDERSCORE') + .once() + .returns(['remove']); + + assert.typeOf(Collection._aggregate(), 'Function'); + }); + + it('Should NOT aggregate underscore methods (not as part of underscore)', () => { + const expUNDERSCORE = this.mockStatic.expects('UNDERSCORE') + .once() + .returns(['unexistent']); + + assert.typeOf(Collection._aggregate(), 'Function'); + }); + + }); + +}); diff --git a/test/commands/util/adt/iterator.spec.es6 b/test/lib/util/adt/iterator.spec.es6 similarity index 89% rename from test/commands/util/adt/iterator.spec.es6 rename to test/lib/util/adt/iterator.spec.es6 index b23b6e3..9cfc77d 100644 --- a/test/commands/util/adt/iterator.spec.es6 +++ b/test/lib/util/adt/iterator.spec.es6 @@ -1,10 +1,10 @@ /** -* @module commands.util.adt +* @module util.adt * @author Patricio Ferreira <3dimentionar@gmail.com> **/ -import Iterator from 'commands/util/adt/iterator'; +import Iterator from 'util/adt/iterator'; -describe('commands.util.adt.Iterator', function() { +describe('util.adt.Iterator', function() { before(() => { this.sandbox = sinon.sandbox.create(); @@ -23,7 +23,7 @@ describe('commands.util.adt.Iterator', function() { delete this.sandbox; }); - describe('#constructor', () => { + describe('constructor()', () => { it('Should get a new instance', () => { assert.instanceOf(Iterator.new(), Iterator); @@ -38,7 +38,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#_valid()', () => { + describe('_valid()', () => { it('Should return true (valid element)', () => { const exp = Iterator.new(); @@ -53,7 +53,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#set()', () => { + describe('set()', () => { it('Should set the iterator', () => { const exp = Iterator.new(); @@ -78,7 +78,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#hasNext()', () => { + describe('hasNext()', () => { it('Should return true and false', () => { const exp = Iterator.new([1,2]); @@ -91,7 +91,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#next()', () => { + describe('next()', () => { it('Should return next element', () => { const exp = Iterator.new([1,2]); @@ -102,7 +102,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#rewind()', () => { + describe('rewind()', () => { it('Should rewind the iterator (pointer to the begin of the iterator)', () => { const exp = Iterator.new([1,2]); @@ -117,7 +117,7 @@ describe('commands.util.adt.Iterator', function() { }); - describe('#remove()', () => { + describe('remove()', () => { it('Should remove the current element by the pointer', () => { const exp = Iterator.new([1,2,3]); diff --git a/test/lib/util/adt/queue-async.spec.es6 b/test/lib/util/adt/queue-async.spec.es6 new file mode 100644 index 0000000..507dec4 --- /dev/null +++ b/test/lib/util/adt/queue-async.spec.es6 @@ -0,0 +1,93 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import QueueAsync from 'util/adt/queue-async'; +import Command from 'command'; +import Asynchronous from 'visitors/async/async'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('util.adt.QueueAsync', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockStatic = this.sandbox.mock(QueueAsync); + this.mockProto = this.sandbox.mock(QueueAsync.prototype); + }); + + afterEach(() => { + this.mockStatic.verify(); + this.mockProto.verify(); + + this.sandbox.restore(); + + delete this.mockStatic; + delete this.mockProto; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = QueueAsync.new([], { capacity: 3 }); + assert.instanceOf(exp, QueueAsync); + assert.instanceOf(exp._visitor, Asynchronous); + }); + + it('Should get a new instance (no initial and no opts)', () => { + assert.instanceOf(QueueAsync.new(), QueueAsync); + }); + + }); + + describe('async->poll()', () => { + + it('Should poll all elements asynchronously from queue (with Events)', (done) => { + const exp = QueueAsync.new([{ env: 'dev' }, { env: 'stage' }], { capacity: 2, interface: Command }); + + exp.on(QueueAsync.events.next, (element) => { + assert.instanceOf(element, Command); + }); + + exp.on(QueueAsync.events.end, (result) => { + assert.isArray(result); + assert.lengthOf(result, 2); + assert.instanceOf(result[0], Command); + + done(); + }); + + assert.instanceOf(exp.poll(), Promise); + }); + + it('Should poll all elements asynchronously from queue (without Events)', (done) => { + const exp = QueueAsync.new([], { capacity: 2, interface: Command }); + const expEmit = this.mockProto.expects('emit').never(); + + assert.isTrue(exp.offer({ env: 'dev' }, { silent: true })); + assert.isTrue(exp.offer({ env: 'stage' }, { silent: true })); + + const result = exp.poll({ silent: true }).then((results) => { + assert.instanceOf(results, Array); + assert.lengthOf(results, 2); + assert.instanceOf(results[0], Command); + assert.isTrue(exp.isEmpty()); + + done(); + }).catch((err) => console.log(err)); + }); + + it('Should Error: Element doesn\'t implement interface util.visitor.Visited', () => { + const message = InterfaceException.type.interface({ name: 'util.visitor.Visited' }); + assert.throws(() => QueueAsync.new([{ simple: true }], { capacity: 1 }), message); + }); + + }); + +}); diff --git a/test/lib/util/adt/queue.spec.es6 b/test/lib/util/adt/queue.spec.es6 new file mode 100644 index 0000000..14f893d --- /dev/null +++ b/test/lib/util/adt/queue.spec.es6 @@ -0,0 +1,158 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Queue from 'util/adt/queue'; +import Command from 'command'; +import QueueException from 'util/exception/adt/queue'; + +describe('util.adt.Queue', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockStatic = this.sandbox.mock(Queue); + this.mockQueue = this.sandbox.mock(Queue.prototype); + }); + + afterEach(() => { + this.mockStatic.verify(); + this.mockQueue.verify(); + + this.sandbox.restore(); + + delete this.mockStatic; + delete this.mockQueue; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + assert.instanceOf(Queue.new(), Queue); + }); + + }); + + describe('_valid()', () => { + + it('Should ERROR: elements is array and length is >= than capacity', () => { + const exp = Queue.new([], { capacity: 2 }); + assert.throws(() => exp._valid([1,2,3]), QueueException.type.capacityViolation({ capacity: 2 })); + }); + + it('Should NOT be valid - element is null', () => { + const exp = Queue.new([], { capacity: 2 }); + assert.isFalse(exp._valid()); + }); + + }); + + describe('_validCapacity()', () => { + + it('Should validate capacity to true - current size is <= than capacity', () => { + const exp = Queue.new([], { capacity: 2 }); + const expSize = this.mockQueue.expects('size').once().returns(2); + assert.isFalse(exp._validCapacity()); + }); + + }); + + describe('set()', () => { + + it('Should set elements', () => { + const toSet = [1, 2]; + const exp = Queue.new([], { capacity: 2 }); + const expEmit = this.mockQueue.expects('emit') + .once() + .withArgs(Queue.events.set, exp, toSet) + .returns(exp); + + assert.instanceOf(exp.set(toSet), Queue); + assert.equal(2, exp.size()); + }); + + it('Should NOT set elements (not valid)', () => { + const exp = Queue.new([], { capacity: 2 }); + const expEmit = this.mockQueue.expects('emit').never(); + assert.instanceOf(exp.set(), Queue); + assert.instanceOf(exp.set({}), Queue); + }); + + }); + + describe('offer()', () => { + + it('Should push elements into the queue', () => { + const toOffer = { env: 'stage' }; + const exp = Queue.new([{ env: 'dev' }], { capacity: 3, interface: Command, silent: true }); + const expEmit = this.mockQueue.expects('emit') + .once() + .withArgs(Queue.events.offer, exp, sinon.match.instanceOf(Command)) + .returns(exp); + + assert.isTrue(exp.offer(toOffer)); + assert.equal(2, exp.size()); + }); + + it('Should NOT push elements into the queue', () => { + const toOffer = { env: 'stage' }; + const exp = Queue.new([{ env: 'dev' }], { capacity: 1, interface: Command, silent: true }); + const expEmit = this.mockQueue.expects('emit').never(); + + assert.isFalse(exp.offer(toOffer)); + assert.equal(1, exp.size()); + }); + + }); + + describe('peek()', () => { + + it('Should retrieve, but not remove the next element in the queue', () => { + const exp = Queue.new([{ env: 'dev' }, { env: 'stage' }], { capacity: 2, interface: Command, silent: true }); + let next = exp.peek(); + assert.instanceOf(next, Command); + assert.equal('dev', next.env); + assert.equal(2, exp.size()); + }); + + it('Should NOT retrieve the next element in the queue', () => { + const exp = Queue.new([], { capacity: 2, interface: Command, silent: true }); + let next = exp.peek(); + assert.isNull(next); + assert.isTrue(exp.isEmpty()); + }); + + }); + + describe('poll()', () => { + + it('Should retrieve and remove the next element in the queue', () => { + const exp = Queue.new([{ env: 'dev' }, { env: 'stage' }], { capacity: 3, interface: Command, silent: true }); + const expEmit = this.mockQueue.expects('emit') + .once() + .withArgs(Queue.events.poll, exp, sinon.match.instanceOf(Command)) + .returns(exp); + + let next = exp.poll(); + assert.instanceOf(next, Command); + assert.equal('dev', next.env); + assert.equal(1, exp.size()); + }); + + it('Should NOT retrieve (nor remove) the next element in the queue', () => { + const exp = Queue.new([], { capacity: 2, interface: Command, silent: true }); + const expEmit = this.mockQueue.expects('emit').never(); + + assert.isNull(exp.poll()); + assert.isTrue(exp.isEmpty()); + }); + + }); + +}); diff --git a/test/lib/util/adt/stack-async.spec.es6 b/test/lib/util/adt/stack-async.spec.es6 new file mode 100644 index 0000000..0a76575 --- /dev/null +++ b/test/lib/util/adt/stack-async.spec.es6 @@ -0,0 +1,93 @@ +/** +* @module util.adt +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import StackAsync from 'util/adt/stack-async'; +import Command from 'command'; +import Asynchronous from 'visitors/async/async'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('util.adt.StackAsync', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockStatic = this.sandbox.mock(StackAsync); + this.mockProto = this.sandbox.mock(StackAsync.prototype); + }); + + afterEach(() => { + this.mockStatic.verify(); + this.mockProto.verify(); + + this.sandbox.restore(); + + delete this.mockStatic; + delete this.mockProto; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = StackAsync.new([]); + assert.instanceOf(exp, StackAsync); + assert.instanceOf(exp._visitor, Asynchronous); + }); + + it('Should get a new instance (no initial and no opts)', () => { + assert.instanceOf(StackAsync.new(), StackAsync); + }); + + }); + + describe('async->pop()', () => { + + it('Should pop all elements asynchronously from the stack (with Events)', (done) => { + const exp = StackAsync.new([{ env: 'dev' }, { env: 'stage' }], { interface: Command }); + + exp.on(StackAsync.events.next, (element) => { + assert.instanceOf(element, Command); + }); + + exp.on(StackAsync.events.end, (result) => { + assert.isArray(result); + assert.lengthOf(result, 2); + assert.instanceOf(result[0], Command); + + done(); + }); + + assert.instanceOf(exp.pop(), Promise); + }); + + it('Should pop all elements asynchronously from the stack (without Events)', (done) => { + const exp = StackAsync.new([], { interface: Command }); + const expEmit = this.mockProto.expects('emit').never(); + + assert.isTrue(exp.push({ env: 'dev' }, { silent: true })); + assert.isTrue(exp.push({ env: 'stage' }, { silent: true })); + + const result = exp.pop({ silent: true }).then((results) => { + assert.instanceOf(results, Array); + assert.lengthOf(results, 2); + assert.instanceOf(results[0], Command); + assert.isTrue(exp.isEmpty()); + + done(); + }).catch((err) => console.log(err)); + }); + + it('Should Error: Element doesn\'t implement interface util.visitor.Visited', () => { + const message = InterfaceException.type.interface({ name: 'util.visitor.Visited' }); + assert.throws(() => StackAsync.new([{ simple: true }]), message); + }); + + }); + +}); diff --git a/test/commands/util/adt/stack.spec.es6 b/test/lib/util/adt/stack.spec.es6 similarity index 74% rename from test/commands/util/adt/stack.spec.es6 rename to test/lib/util/adt/stack.spec.es6 index 0876ebc..aaaebae 100644 --- a/test/commands/util/adt/stack.spec.es6 +++ b/test/lib/util/adt/stack.spec.es6 @@ -1,11 +1,11 @@ /** -* @module commands.util.adt +* @module util.adt * @author Patricio Ferreira <3dimentionar@gmail.com> **/ -import Stack from 'commands/util/adt/stack'; -import Command from 'commands/command'; +import Stack from 'util/adt/stack'; +import Command from 'command'; -describe('commands.util.adt.Stack', function() { +describe('util.adt.Stack', function() { before(() => { this.sandbox = sinon.sandbox.create(); @@ -16,6 +16,7 @@ describe('commands.util.adt.Stack', function() { }); afterEach(() => { + this.mockStack.verify(); this.sandbox.restore(); delete this.mockStack; }); @@ -24,7 +25,7 @@ describe('commands.util.adt.Stack', function() { delete this.sandbox; }); - describe('#constructor()', () => { + describe('constructor()', () => { it('Should get an instance (initial emtpy elements)', () => { const exp = Stack.new(); @@ -40,37 +41,31 @@ describe('commands.util.adt.Stack', function() { }); - describe('#push()', () => { + describe('push()', () => { it('Should push a new element', () => { const toPush = { option: true }; const exp = Stack.new([], { interface: Command }); const expEmit = this.mockStack.expects('emit') .once() - .withArgs(Stack.events.push, exp, toPush) + .withArgs(Stack.events.push, exp, sinon.match.instanceOf(Command)) .returns(exp); - exp.push(toPush); - + assert.isTrue(exp.push(toPush)); assert.isFalse(exp.isEmpty()); - - this.mockStack.verify(); }); it('Should NOT push a new element', () => { const exp = Stack.new(); const expEmit = this.mockStack.expects('emit').never(); - exp.push(); - + assert.isFalse(exp.push()); assert.isTrue(exp.isEmpty()); - - this.mockStack.verify(); }); }); - describe('#peek()', () => { + describe('peek()', () => { it('Should get the first element', () => { const exp = Stack.new([1,2,3]); @@ -85,20 +80,19 @@ describe('commands.util.adt.Stack', function() { }); - describe('#pop()', () => { + describe('pop()', () => { it('Should remove and get the first element', () => { - const exp = Stack.new([{ option: true }, { option: false }], { interface: Command }); + const exp = Stack.new([{ env: 'dev' }, { env: 'stage' }], { interface: Command }); + const expPop = exp.last(); const expEmit = this.mockStack.expects('emit') .once() - .withArgs(Stack.events.pop, exp) + .withArgs(Stack.events.pop, exp, expPop) .returns(exp); - assert.instanceOf(exp.pop(), Stack); + assert.equal(expPop, exp.pop()); assert.instanceOf(exp.peek(), Command); assert.equal(1, exp.size()); - - this.mockStack.verify(); }); it('Should NOT remove and get the first element', () => { @@ -109,7 +103,7 @@ describe('commands.util.adt.Stack', function() { }); - describe('#search()', () => { + describe('search()', () => { it('Should search and retrieve the position of an element', () => { const exp = Stack.new(['a','b','c']); @@ -117,6 +111,12 @@ describe('commands.util.adt.Stack', function() { assert.equal(1, expPos); }); + it('Should NOT search (invalid element)', () => { + const exp = Stack.new(['a','b','c']); + const expPos = exp.search(); + assert.equal(-1, expPos); + }); + it('Should search and retrieve -1 position (element not found)', () => { const newCommand = Command.new({ env: 'production' }); const exp = Stack.new([{ env: 'staging' }, newCommand], { interface: Command }); diff --git a/test/lib/util/factory/factory.spec.es6 b/test/lib/util/factory/factory.spec.es6 new file mode 100644 index 0000000..7695568 --- /dev/null +++ b/test/lib/util/factory/factory.spec.es6 @@ -0,0 +1,311 @@ +/** +* @module util.factory +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import factory from 'util/factory/factory'; +import logger from 'util/logger/logger'; +import Command from 'command'; +import Collection from 'util/adt/collection'; + +describe('util.factory.Factory', function() { + + before(() => { + factory.reset(); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockCommand = this.sandbox.mock(Command); + this.mockLogger = this.sandbox.mock(logger); + this.mockProto = this.sandbox.mock(factory); + }); + + afterEach(() => { + this.mockCommand.verify(); + this.mockLogger.verify(); + this.mockProto.verify(); + + this.sandbox.restore(); + + delete this.mockCommand; + delete this.mockLogger; + delete this.mockProto; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should not be a constructor', () => { + assert.instanceOf(factory, Object); + assert.isNotNull(factory._factories); + assert.instanceOf(factory._factories, Map); + assert.isNotNull(factory.register); + }); + + }); + + describe('basePath()', () => { + + it('Should set a basePath', () => { + const input = './lib'; + assert.typeOf(factory.basePath(input), 'object'); + assert.equal(input, factory.path); + }); + + it('Should be same basePath when importing multiple times (singleton check)', () => { + const singleton = require('util/factory/factory').default; + assert.deepEqual(singleton, factory); + singleton.basePath('./lib/util/adt'); + assert.equal(singleton.path, factory.path); + }); + + }); + + describe('_resolve()', () => { + + it('Should resolve a given path with the basepath', () => { + assert.equal(factory._resolve('collection'), path.resolve(factory.path, 'collection')); + }); + + }); + + describe('_validate()', () => { + + it('Should return true: path is valid', () => { + assert.isTrue(factory._validate('collection')); + }); + + it('Should return false: path is not defined', () => { + assert.isFalse(factory._validate()); + }); + + it('Should return false: path is not a string', () => { + assert.isFalse(factory._validate(1)); + }); + + it('Should return false: path is not an empty string', () => { + assert.isFalse(factory._validate('')); + }); + + it('Should return false: resolved path doesn\'t exists (fs.statSync)', () => { + const expLoggerOutput = this.mockLogger.expects('_stdout').atLeast(1).returns(logger); + assert.isFalse(factory._validate('file-unexistent')); + }); + + }); + + describe('_new()', () => { + + it('Should return factory verbatim', () => { + const verbatimFactory = { option: true } + assert.equal(factory._new(verbatimFactory), verbatimFactory); + }); + + it('Should instanciate factory using static constructor', () => { + const expNew = this.mockCommand.expects('new').returns('Command'); + assert.equal(factory._new(Command), 'Command'); + }); + + it('Should instanciate factory using operator `new`', () => { + const FakeFactory = function() {}; + assert.instanceOf(factory._new(FakeFactory), FakeFactory); + }); + + }); + + describe('register()', () => { + + it('Should register a factory', () => { + const input = 'collection'; + const expValidate = this.mockProto.expects('_validate') + .once() + .withArgs(input) + .returns(true); + + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(input) + .returns(false); + + assert.instanceOf(factory.register(input), Object); + }); + + it('Should NOT register a factory (not valid)', () => { + const input = 'non-existent'; + const expValidate = this.mockProto.expects('_validate') + .once() + .withArgs(input) + .returns(false); + + const expExists = this.mockProto.expects('exists').never(); + + assert.instanceOf(factory.register(input), Object); + assert.equal(1, factory._factories.size); + }); + + it('Should NOT register a factory (already registered)', () => { + const input = 'non-existent'; + const expValidate = this.mockProto.expects('_validate') + .once() + .withArgs(input) + .returns(false); + + const expExists = this.mockProto.expects('exists').never(); + + assert.instanceOf(factory.register(input), Object); + assert.equal(1, factory._factories.size); + }); + + }); + + describe('registerAll()', () => { + + it('Should register a list of factories', () => { + const input = ['stack', 'queue']; + const expValidate = this.mockProto.expects('_validate') + .twice() + .withArgs(sinon.match('stack').or(sinon.match('queue'))) + .returns(true); + + const expExists = this.mockProto.expects('exists') + .twice() + .withArgs(sinon.match('stack').or(sinon.match('queue'))) + .returns(false); + + assert.instanceOf(factory.registerAll(input), Object); + assert.equal(3, factory._factories.size); + }); + + it('Should register some out of a list of factories', () => { + const input = ['stack', 'queue-async']; + const expValidate = this.mockProto.expects('_validate') + .twice() + .withArgs(sinon.match('stack').or(sinon.match('queue'))) + .returns(true); + + const expExists = this.mockProto.expects('exists') + .twice() + .withArgs(sinon.match('stack').or(sinon.match('queue'))); + + expExists.onFirstCall().returns(true); // with stack + expExists.onSecondCall().returns(false); // with queue-async + + assert.instanceOf(factory.registerAll(input), Object); + assert.equal(4, factory._factories.size); + }); + + it('Should NOT register an empty list of factory paths', () => { + assert.instanceOf(factory.registerAll(), Object); + assert.equal(4, factory._factories.size); + }); + + }); + + describe('unregister()', () => { + + it('Should unregister a factory', () => { + const input = 'queue-async'; + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(input) + .returns(true); + + assert.instanceOf(factory.unregister(input), Object); + assert.equal(3, factory._factories.size); + }); + + it('Should NOT unregister a factory (doesn\'t exists)', () => { + const input = 'non-existent'; + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(input) + .returns(false); + + assert.instanceOf(factory.unregister(input), Object); + assert.equal(3, factory._factories.size); + }); + + }); + + describe('unregisterAll()', () => { + + it('Should unregister a list of factories', () => { + const input = ['stack', 'queue']; + const expExists = this.mockProto.expects('exists') + .twice() + .withArgs(sinon.match('stack').or(sinon.match('queue'))) + .returns(true); + + assert.instanceOf(factory.unregisterAll(input), Object); + assert.equal(1, factory._factories.size); + }); + + it('Should unregister some, out of a list of factories', () => { + const input = ['collection', 'non-existent']; + const expExists = this.mockProto.expects('exists') + .twice() + .withArgs(sinon.match('collection').or(sinon.match('non-existent'))); + + expExists.onFirstCall().returns(true); // with collection + expExists.onSecondCall().returns(false); // with non-existent + + assert.instanceOf(factory.unregisterAll(input), Object); + assert.equal(0, factory._factories.size); + }); + + it('Should NOT unregister an empty list of factory paths', () => { + assert.instanceOf(factory.unregisterAll(), Object); + assert.equal(0, factory._factories.size); + }); + + }); + + describe('find()', () => { + + it('Should return the factory (factory found)', () => { + factory.registerAll(['collection', 'stack', 'queue']); // Setup a few factories + const input = 'stack'; + + const exp = factory.find(input); + assert.isNotNull(exp); + assert.equal(factory._resolve(input), exp); + }); + + it('Should return null (factory not found)', () => { + const input = 'non-existent'; + const exp = factory.find(input); + assert.isNull(exp); + assert.isNull(factory.find()); + }); + + }); + + describe('exists()', () => { + + it('Should return true (factory exists)', () => { + assert.isTrue(factory.exists('collection')); + }); + + it('Should return false (factory doesn\'t exists)', () => { + assert.isFalse(factory.exists('non-existent')); + assert.isFalse(factory.exists()); + }); + + }); + + describe('get()', () => { + + it('Should get a new instance', () => { + assert.instanceOf(factory.get('collection'), Collection); + }); + + it('Should NOT get a new instance (factory is not registered)', () => { + assert.isNull(factory.get('non-existent')); + }); + + }); + +}); diff --git a/test/lib/util/logger/logger.spec.es6 b/test/lib/util/logger/logger.spec.es6 new file mode 100644 index 0000000..6c71bcf --- /dev/null +++ b/test/lib/util/logger/logger.spec.es6 @@ -0,0 +1,231 @@ +/** +* @module util.logger +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import logger, { Logger } from 'util/logger/logger'; + +describe('util.logger.Logger', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(logger); + this.mockProcess = this.sandbox.mock(process); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockProcess.verify(); + + this.sandbox.restore(); + + delete this.mockProto; + delete this.mockProcess; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should check the singleton instance', () => { + assert.instanceOf(logger, Logger); + }); + + it('Should adds to the buffer by using constructor function', () => { + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + logger('hello')('world').out(logger.magenta); + }); + + }); + + describe('_stdout()', () => { + + it('Should use console.warn to output', () => { + // ISOLATED: To avoid dangerous issues with unit tests (stubbing `console.warn`) + const stubConsoleWarn = this.sandbox.stub(console, 'warn', () => {}); + assert.instanceOf(logger._stdout('Hello'), Logger); + stubConsoleWarn.restore(); + }); + + }); + + describe('_validate()', () => { + + it('Should validate to true, if level = silent and type is fatal', () => { + logger.level(Logger.level.silent); + assert.isTrue(logger._validate(Logger.type.fatal)); + }); + + it('Should validate to true, if level = output and type is fatal, warn or output only', () => { + logger.level(Logger.level.output); + assert.isTrue(logger._validate(Logger.type.fatal)); + assert.isTrue(logger._validate(Logger.type.warning)); + assert.isTrue(logger._validate(Logger.type.output)); + assert.isFalse(logger._validate(Logger.type.debug)); + }); + + it('Should validate to true, if level = debug with all types', () => { + logger.level(Logger.level.debug); + assert.isTrue(logger._validate(Logger.type.fatal)); + assert.isTrue(logger._validate(Logger.type.warning)); + assert.isTrue(logger._validate(Logger.type.output)); + assert.isTrue(logger._validate(Logger.type.debug)); + }); + + it('Should validate to false, if level = silent with type is other than fatal', () => { + logger.level(Logger.level.silent); + assert.isFalse(logger._validate(Logger.type.warning)); + assert.isFalse(logger._validate(Logger.type.output)); + assert.isFalse(logger._validate(Logger.type.debug)); + }); + + }); + + describe('out()', () => { + + it('Should write to standard output (out - with event)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Logger.events.output, logger); + + assert.instanceOf(logger('hello').out(), Logger); + }); + + it('Should write to standard out (out - without event)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit').never(); + + assert.instanceOf(logger('hello').out({ silent: true }, logger.magenta), Logger); + }); + + it('Should NOT write to standard out (due to validation)', () => { + logger.level(Logger.level.silent); + const expOut = this.mockProto.expects('_stdout').never(); + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Logger.events.output, logger); + + assert.instanceOf(logger('Not Logged').out(), Logger); + }); + + }); + + describe('debug()', () => { + + it('Should write to standard output (debug - with event)', () => { + logger.level(Logger.level.debug); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Logger.events.debug, logger); + + assert.instanceOf(logger('hello').debug(), Logger); + }); + + it('Should write to standard out (debug - without event)', () => { + logger.level(Logger.level.debug); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit').never(); + + assert.instanceOf(logger('hello').debug({ silent: true }), Logger); + }); + + }); + + describe('warn()', () => { + + it('Should write to standard output (warning - with event)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Logger.events.warning, logger); + + assert.instanceOf(logger('hello').warn(), Logger); + }); + + it('Should write to standard out (warning - without event)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expEmit = this.mockProto.expects('emit').never(); + + assert.instanceOf(logger('hello').warn({ silent: true }), Logger); + }); + + }); + + describe('fatal()', () => { + + it('Should write to standard output (fatal - with event) and process.exit(1)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expExit = this.mockProcess.expects('exit') + .once() + .withArgs(1); + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Logger.events.fatal, logger); + + assert.isUndefined(logger('hello').fatal()); + }); + + it('Should write to standard out (fatal - without event) and process.exit(1)', () => { + logger.level(Logger.level.output); + const expOut = this.mockProto.expects('_stdout') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expExit = this.mockProcess.expects('exit') + .once() + .withArgs(1); + + const expEmit = this.mockProto.expects('emit').never(); + + assert.isUndefined(logger('hello').fatal({ silent: true })); + }); + + }); + +}); diff --git a/test/lib/util/mixins.spec.es6 b/test/lib/util/mixins.spec.es6 new file mode 100644 index 0000000..557eeef --- /dev/null +++ b/test/lib/util/mixins.spec.es6 @@ -0,0 +1,130 @@ +/** +* @module util +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'util/mixins'; + +describe('util.mixins', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + this.test = { + str: 'one', + num: 1, + obj: { + 'obj-str': 'two', + '-obj-num': 2, + bool: false + }, + arr: ['one', 1, true], + bool: true, + nul: null + }; + }); + + beforeEach(() => { + this.mockStatic = this.sandbox.mock(_); + }); + + afterEach(() => { + this.mockStatic.verify(); + + this.sandbox.restore(); + + delete this.mockStatic; + }); + + after(() => { + delete this.test; + delete this.sandbox; + }); + + describe('parametrize()', () => { + + it('Should parametrized object structure', () => { + const exp = _.parametrize(this.test); + assert.include(exp, '--str'); + assert.include(exp, '--num'); + assert.include(exp, '--obj-obj-str'); + assert.include(exp, '--obj-obj-num'); + assert.include(exp, '--obj-bool'); + assert.include(exp, '--arr'); + assert.include(exp, '--bool'); + assert.include(exp, '--nul'); + }); + + it('Should return empty array (empty object)', () => { + assert.lengthOf(_.parametrize(), 0); + }); + + }); + + describe('isRealObject()', () => { + + it('Should return true', () => { + assert.isTrue(_.isRealObject({})); + }); + + it('Should return false', () => { + assert.isFalse(_.isRealObject(new RegExp())); + assert.isFalse(_.isRealObject([])); + assert.isFalse(_.isRealObject("String")); + assert.isFalse(_.isRealObject(1)); + }); + + }); + + describe('isAdt()', () => { + + it('Should return true', () => { + assert.isTrue(_.isAdt({})); + assert.isTrue(_.isAdt([])); + }); + + it('Should return false', () => { + assert.isFalse(_.isAdt()); + assert.isFalse(_.isAdt(1)); + assert.isFalse(_.isAdt("string")); + assert.isFalse(_.isAdt(new RegExp())); + }); + + }); + + describe('instanceOf()', () => { + + it('Should return true', () => { + assert.isTrue(_.instanceOf({}, Object)); + assert.isTrue(_.instanceOf([], Array)); + assert.isTrue(_.instanceOf([], Object)); + assert.isTrue(_.instanceOf(new Number(1), Number)); + assert.isTrue(_.instanceOf(new String("string"), String)); + }); + + it('Should return false', () => { + assert.isFalse(_.instanceOf()); + assert.isFalse(_.instanceOf({}, Array)); + assert.isFalse(_.instanceOf([], String)); + assert.isFalse(_.instanceOf(new Number(1), String)); + assert.isFalse(_.instanceOf(new String("string"), RegExp)); + }); + + }); + + describe('defined()', () => { + + it('Should return true', () => { + assert.isTrue(_.defined(-1)); + assert.isTrue(_.defined(0)); + assert.isTrue(_.defined(false)); + assert.isTrue(_.defined("")); + assert.isTrue(_.defined(NaN)); + }); + + it('Should return false', () => { + assert.isFalse(_.defined(null)); + assert.isFalse(_.defined(undefined)); + }); + + }); + +}); diff --git a/test/lib/util/visitor/visited.spec.es6 b/test/lib/util/visitor/visited.spec.es6 new file mode 100644 index 0000000..01bb71f --- /dev/null +++ b/test/lib/util/visitor/visited.spec.es6 @@ -0,0 +1,98 @@ +/** +* @module util.visitor +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Visited from 'util/visitor/visited'; +import Visitor from 'util/visitor/visitor'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('util.visitor.Visited', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Visited.prototype); + this.mockVisitor = this.sandbox.mock(Visitor.prototype); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockVisitor.verify(); + this.sandbox.restore(); + delete this.mockProto; + delete this.mockVisitor; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = Visited.new({ property: 'Visited' }); + assert.instanceOf(exp, Visited); + assert.equal('Visited', exp.property); + }); + + }); + + describe('validate()', () => { + + it('Should return true', () => { + const inputVisitor = Visitor.new(); + const exp = Visited.new({}); + assert.isTrue(exp.validate(inputVisitor)); + }); + + it('Should return false: visitor not defined', () => { + const exp = Visited.new({}); + assert.isFalse(exp.validate()); + }); + + it('Should return false: visitor not implementing Visited', () => { + const exp = Visited.new({}); + assert.isFalse(exp.validate({})); + }); + + }); + + describe('accept()', () => { + + it('Should accept visitor', () => { + const inputVisitor = Visitor.new(); + const exp = Visited.new({}); + + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(inputVisitor) + .returns(true); + + const expVisit = this.mockVisitor.expects('visit') + .once() + .withArgs(exp) + .returns(exp); + + assert.instanceOf(exp.accept(inputVisitor), Object); + assert.isTrue(expValidate.calledBefore(expVisit)); + }); + + it('Should NOT accept visitor', () => { + const inputVisitor = Visitor.new(); + const exp = Visited.new({}); + + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(inputVisitor) + .returns(false); + + const expVisit = this.mockVisitor.expects('visit').never(); + + assert.instanceOf(exp.accept(inputVisitor), Object); + }); + + }); + +}); diff --git a/test/lib/util/visitor/visitor.spec.es6 b/test/lib/util/visitor/visitor.spec.es6 new file mode 100644 index 0000000..a10d885 --- /dev/null +++ b/test/lib/util/visitor/visitor.spec.es6 @@ -0,0 +1,138 @@ +/** +* @module util.visitor +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Visitor from 'util/visitor/visitor'; +import Visited from 'util/visitor/visited'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('util.visitor.Visitor', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Visitor.prototype); + }); + + afterEach(() => { + this.mockProto.verify(); + this.sandbox.restore(); + delete this.mockProto; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = Visitor.new(); + assert.instanceOf(exp, Visitor); + assert.equal(exp.name, 'Visitor'); + }); + + }); + + describe('validate()', () => { + + it('Should return true', () => { + const exp = Visitor.new(); + const input = Visited.new({}); + assert.isTrue(exp.validate(input)); + }); + + it('Should return false: visited not defined', () => { + const exp = Visitor.new(); + assert.isFalse(exp.validate()); + }); + + it('Should throw InterfaceException: visited doesn\'t implement his interface', () => { + const exp = Visitor.new(); + assert.throws(() => exp.validate({}), + InterfaceException.type.interface({ name: 'util.visitor.Visited' })); + }); + + }); + + describe('_methods()', () => { + + it('Should filter visitor internal methods', () => { + const vi = Visitor.new({ someMethod: () => {} }); + const exp = vi._methods(); + assert.lengthOf(exp, 1); + assert.equal('someMethod', exp[0]); + }); + + }); + + describe('_decorate()', () => { + + it('Should decorate visited object', () => { + const arg = { prop: 'one' }; + const exp = Visitor.new({ func: this.sandbox.spy() }); + const input = Visited.new(); + + const spyWith = exp.func.withArgs(input, arg); + + const expMethods = this.mockProto.expects('_methods') + .once() + .returns(['func']); + + assert.instanceOf(exp._decorate(input), Visited); + + input.func(arg); + + assert.isTrue(spyWith.calledOnce); + assert.isTrue(spyWith.calledOn(exp)); + assert.isTrue(spyWith.calledWith(input, arg)); + }); + + it('Should skip methods already defined in visited', () => { + const exp = Visitor.new({ func: () => {}, existent: this.sandbox.spy() }); + const input = Visited.new({ existent: () => {} }); + + assert.instanceOf(exp._decorate(input), Visited); + input.existent(); + assert.isFalse(exp.existent.calledOnce); + }); + + }); + + describe('visit()', () => { + + it('Should visit the visted instance', () => { + const exp = Visitor.new(); + const input = Visited.new({}); + const expDecorate = this.mockProto.expects('_decorate') + .once() + .withArgs(input) + .returns(input); + + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(input) + .returns(true); + + assert.instanceOf(exp.visit(input), Object); + assert.isTrue(expValidate.calledBefore(expDecorate)); + }); + + it('Should NOT visit the visted instance', () => { + const exp = Visitor.new(); + const input = {}; + const expDecorate = this.mockProto.expects('_decorate').never(); + + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(input) + .returns(false); + + assert.isNull(exp.visit(input)); + }); + + }); + +}); diff --git a/test/lib/visitors/async/async.spec.es6 b/test/lib/visitors/async/async.spec.es6 new file mode 100644 index 0000000..979e671 --- /dev/null +++ b/test/lib/visitors/async/async.spec.es6 @@ -0,0 +1,79 @@ +/** +* @module visitors.async +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import Asynchronous from 'visitors/async/async'; +import Visited from 'util/visitor/visited'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('visitors.async.Asynchronous', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.target = () => { return { next: (adt, resolve, reject) => {} }; }; + this.mockAsync = this.sandbox.mock(Asynchronous.prototype); + }); + + afterEach(() => { + this.sandbox.restore(); + delete this.target; + delete this.mockAsync; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = Asynchronous.new(this.target()); + assert.instanceOf(exp, Asynchronous); + assert.equal('AsynchronousVisitor', exp.name); + }); + + }); + + describe('next()', () => { + + it('Should call promise\'s resolve as a default strategy', () => { + const resolveSpy = this.sandbox.spy(); + const exp = Asynchronous.new({}); + assert.instanceOf(exp.next({}, resolveSpy), Asynchronous); + assert.isTrue(resolveSpy.calledOnce); + }); + + }); + + describe('visit()', () => { + + it('Should visit the target', () => { + const input = Visited.new({ property: 'target', next: this.sandbox.spy() }); + const exp = Asynchronous.new(); + assert.instanceOf(exp.visit(input), Visited); + }); + + it('Should NOT visit the target: super.validate returns false', () => { + const exp = Asynchronous.new(); + assert.isNull(exp.visit()); + }); + + }); + + describe('execute()', () => { + + it('Should return a decorated target with asynchronous capabilities', () => { + const input = this.target(); + const visited = Visited.new(input); + const exp = Asynchronous.new().visit(visited); + assert.isNotNull(exp.execute); + assert.instanceOf(exp.execute(), Promise); + }); + + }); + +}); diff --git a/test/lib/visitors/commander.spec.es6 b/test/lib/visitors/commander.spec.es6 new file mode 100644 index 0000000..84538c9 --- /dev/null +++ b/test/lib/visitors/commander.spec.es6 @@ -0,0 +1,217 @@ +/** +* @module visitors +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Commander from 'visitors/commander'; +import Factory from 'util/factory/factory'; +import yargs from 'yargs'; +import Command from 'command'; +import chalk from 'chalk'; + +describe('visitors.Commander', function() { + + before(() => { + this.command = Command.new(); + if(!this.commander) this.commander = Commander.new(this.command); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Commander.prototype); + this.mockYargs = this.sandbox.mock(yargs); + this.mockCommand = this.sandbox.mock(Command.prototype); + this.mockFactory = this.sandbox.mock(Factory); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockYargs.verify(); + this.mockFactory.verify(); + this.mockCommand.verify(); + + this.sandbox.restore(); + + delete this.mockProto; + delete this.mockYargs; + delete this.mockFactory; + delete this.mockCommand; + }); + + after(() => { + delete this.command; + delete this.commander; + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get an instance', () => { + assert.instanceOf(this.commander, Commander); + assert.instanceOf(this.commander.command, Command); + assert.equal('CommanderVisitor', this.commander.name); + }); + + }); + + describe('_args()', () => { + + it('Should return default arguments', () => { + const res = this.commander._args(); + assert.isArray(res); + }); + + it('Should return custom arguments', () => { + const res = this.commander._args(['param', '--value']); + assert.isArray(res); + assert.lengthOf(res, 2); + }); + + }); + + describe('_command()', () => { + + it('Should return string representation of a command for yargs (with abbr)', () => { + const input = { name: 'bundle', abbr: 'be' }; + const exp = chalk.green(`${input.name}, -${input.abbr}`); + const res = this.commander._command(input); + assert.equal(exp, res); + }); + + it('Should return string representation of a command for yargs (without abbr)', () => { + const input = { name: 'bundle' }; + const exp = chalk.green(`${input.name}`); + const res = this.commander._command(input); + assert.equal(exp, res); + }); + + }); + + describe('_aliases()', () => { + + it('Should return aliases', () => { + const input = { aliases: ['b'] }; + assert.equal(this.commander._aliases(input), input.aliases); + }); + + }); + + describe('_desc()', () => { + + it('Should return command description', () => { + const input = { description: 'my description' }; + assert.equal(this.commander._desc(input), chalk.yellow(input.description)); + }); + + }); + + describe('_builder()', () => { + + it('Should return options (defaults)', () => { + const input = { options: { url: { default: 'http://squarebox.nahuel.io/' } } }; + assert.deepEqual(this.commander._builder(input), input.options); + }); + + }); + + describe('_handler()', () => { + + it('Should return a function handler for the command', () => { + const input = { option: 1, option: 2 }; + const command = {}; + const expHandler = this.mockProto.expects('_onHandler') + .once() + .withArgs(command, input) + .returns(this.commander); + + const exp = this.commander._handler(command, {}); + assert.isFunction(exp); + exp(input); + }); + + }); + + describe('_onHandler()', () => { + + it('Should execute command handler for yargs', () => { + const command = { path: 'bundle/bundle' }; + const inputArgv = { $0: '/path/to/script.js', url: 'http://squarebox.nahuel.io' }; + const expCommand = { options: { path: command.path, url: inputArgv.url } }; + const exp = Commander.new(this.command); + + const expRegister = this.mockFactory.expects('register') + .once() + .withArgs(command.path) + .returns(Factory); + + const expSettings = this.mockCommand.expects('settings') + .once() + .withArgs(expCommand) + .returns(this.command); + + assert.instanceOf(this.commander._onHandler(command, inputArgv), Commander); + assert.isTrue(expRegister.calledBefore(expSettings)); + }); + + }); + + describe('visit()', () => { + + it('Should visit commander visitor', () => { + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(this.command) + .returns(true); + + const exp = this.commander.visit(this.command); + assert.instanceOf(exp, Command); + assert.property(exp, 'commander'); + }); + + it('Should NOT visit commander visitor (invalid)', () => { + const input = Command.new(); + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(input) + .returns(false); + + const exp = this.commander.visit(input); + assert.instanceOf(exp, Command); + assert.notProperty(exp, 'commander'); + }); + + }); + + describe('onParse()', () => { + + it('Should execute onParse handler', () => { + const err = null; + const inputArgv = { $0: '/path/to/script.js', url: 'http://squarebox.nahuel.io' }; + const output = {}; + + const expEmit = this.mockProto.expects('emit') + .once() + .withArgs(Commander.events.parse, err, inputArgv, output) + .returns(true); + + assert.isTrue(this.commander.onParse(err, inputArgv, output)); + }); + + }); + + describe('read()', () => { + + it('Should read CLI arguments via this visitor', () => { + const expReset = this.mockYargs.expects('reset').once().returns(yargs); + const expWrap = this.mockYargs.expects('wrap').once().withArgs(120).returns(yargs); + + const expFactoryGet = this.mockFactory.expects('get') + .exactly(5) + .withArgs(sinon.match((v) => _.contains(Commander.decorators, v)), yargs, this.command, this.commander) + .returns(yargs); + + assert.typeOf(this.commander.read(), 'Function'); + }); + + }); + +}); diff --git a/test/lib/visitors/commander/commands.spec.es6 b/test/lib/visitors/commander/commands.spec.es6 new file mode 100644 index 0000000..11ab10d --- /dev/null +++ b/test/lib/visitors/commander/commands.spec.es6 @@ -0,0 +1,103 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import helper from 'visitors/commander/commands'; +import Collection from 'util/adt/collection'; +import yargs from 'yargs'; + +describe('visitors.commander.Commands', function() { + + before(() => { + this.command = { + constructor: { + commands: Collection.new([{ + name: 'one', + aliases: ['uno'], + description: 'Description One', + options: { optionOne: {} } + }, { + name: 'two', + aliases: ['dos'], + description: 'Description Two', + options: { optionTwo: {} } + }]) + } + }; + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockYargs = this.sandbox.mock(yargs); + }); + + afterEach(() => { + this.mockYargs.verify(); + + this.sandbox.restore(); + + delete this.mockYargs; + }); + + after(() => { + delete this.command; + delete this.sandbox; + }); + + describe('commands()', () => { + + it('Should build and apply a list of commands to yargs', () => { + const { commands } = this.command.constructor; + const matcher = sinon.match((value) => { + const { command } = value; + return (_.has(command, 'name') && + _.has(command, 'aliases') && + _.has(command, 'description') && + _.has(command, 'options')); + }); + + const fake = { + _command: this.sandbox.stub(), + _aliases: this.sandbox.stub(), + _desc: this.sandbox.stub(), + _builder: this.sandbox.stub(), + _handler: this.sandbox.stub() + }; + + fake._command.withArgs(sinon.match.object) + .onFirstCall().returns(commands.get(0)) + .onSecondCall().returns(commands.get(1)); + + fake._aliases.withArgs(sinon.match.object) + .onFirstCall().returns(commands.get(0)) + .onSecondCall().returns(commands.get(1)); + + fake._desc.withArgs(sinon.match.object) + .onFirstCall().returns(commands.get(0)) + .onSecondCall().returns(commands.get(1)); + + fake._builder.withArgs(sinon.match.object) + .onFirstCall().returns(commands.get(0)) + .onSecondCall().returns(commands.get(1)); + + fake._handler.withArgs(sinon.match.object) + .onFirstCall().returns(commands.get(0)) + .onSecondCall().returns(commands.get(1)); + + const expYargsCommand = this.mockYargs.expects('command') + .twice() + .withArgs(matcher) + .returns(yargs); + + assert.equal(yargs, helper(yargs, this.command, fake)); + + assert.isTrue(fake._command.calledWith(commands.get(0))); + assert.isTrue(fake._aliases.calledWith(commands.get(0))); + assert.isTrue(fake._desc.calledWith(commands.get(0))); + assert.isTrue(fake._builder.calledWith(commands.get(0))); + assert.isTrue(fake._handler.calledWith(commands.get(0))); + }); + + }); + +}); diff --git a/test/lib/visitors/commander/epilogue.spec.es6 b/test/lib/visitors/commander/epilogue.spec.es6 new file mode 100644 index 0000000..9261d9e --- /dev/null +++ b/test/lib/visitors/commander/epilogue.spec.es6 @@ -0,0 +1,44 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import epilogue from 'visitors/commander/epilogue'; +import chalk from 'chalk'; +import yargs from 'yargs'; + +describe('visitors.commander.Epilogue', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockYargs = this.sandbox.mock(yargs); + }); + + afterEach(() => { + this.mockYargs.verify(); + + this.sandbox.restore(); + + delete this.mockYargs; + }); + + after(() => { + delete this.sandbox; + }); + + describe('epilogue()', () => { + + it('Should apply epilogue directive to yargs', () => { + const expEpilogue = this.mockYargs.expects('epilogue') + .once() + .withArgs(chalk.cyan('For more information, please visit http://squarebox.nahuel.io/')) + .returns(yargs); + + assert.equal(yargs, epilogue(yargs)); + }); + + }); + +}); diff --git a/test/lib/visitors/commander/parse.spec.es6 b/test/lib/visitors/commander/parse.spec.es6 new file mode 100644 index 0000000..512ecdd --- /dev/null +++ b/test/lib/visitors/commander/parse.spec.es6 @@ -0,0 +1,55 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import parse from 'visitors/commander/parse'; +import Command from 'command'; +import yargs from 'yargs'; + +describe('visitors.commander.Parse', function() { + + before(() => { + this.command = Command.new(); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockYargs = this.sandbox.mock(yargs); + }); + + afterEach(() => { + this.mockYargs.verify(); + + this.sandbox.restore(); + + delete this.mockYargs; + }); + + after(() => { + delete this.command; + delete this.sandbox; + }); + + describe('parse()', () => { + + it('Should call yargs parse', () => { + const input = [process.argv[0], this.cwd].concat(['sqbox', 'bundle']); + const ctx = { + _args: this.sandbox.stub().returns(input), + onParse: this.sandbox.spy() + }; + const expParse = this.mockYargs.expects('parse') + .once() + .withArgs(input, sinon.match.func) + .returns(yargs); + + assert.equal(yargs, parse(yargs, this.command, ctx)); + expParse.callArg(1); + + assert.isTrue(ctx._args.called); + assert.isTrue(ctx.onParse.called); + }); + + }); + +}); diff --git a/test/lib/visitors/commander/usage.spec.es6 b/test/lib/visitors/commander/usage.spec.es6 new file mode 100644 index 0000000..d55e1f0 --- /dev/null +++ b/test/lib/visitors/commander/usage.spec.es6 @@ -0,0 +1,44 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import usage from 'visitors/commander/usage'; +import yargs from 'yargs'; +import chalk from 'chalk'; + +describe('visitors.commander.Usage', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockYargs = this.sandbox.mock(yargs); + }); + + afterEach(() => { + this.mockYargs.verify(); + + this.sandbox.restore(); + + delete this.mockYargs; + }); + + after(() => { + delete this.sandbox; + }); + + describe('usage()', () => { + + it('Should apply usage directive to yargs', () => { + const expUsage = this.mockYargs.expects('usage') + .once() + .withArgs(chalk.white('sqbox [args]')) + .returns(yargs); + + assert.equal(yargs, usage(yargs)); + }); + + }); + +}); diff --git a/test/lib/visitors/commander/version.spec.es6 b/test/lib/visitors/commander/version.spec.es6 new file mode 100644 index 0000000..8b4566a --- /dev/null +++ b/test/lib/visitors/commander/version.spec.es6 @@ -0,0 +1,66 @@ +/** +* @module visitors.commander +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import version from 'visitors/commander/version'; +import Command from 'command'; +import yargs from 'yargs'; +import logger, { Logger } from 'util/logger/logger'; + +describe('visitors.commander.Version', function() { + + before(() => { + this.command = Command.new(); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockYargs = this.sandbox.mock(yargs); + this.mockLogger = this.sandbox.mock(Logger.prototype); + }); + + afterEach(() => { + this.mockYargs.verify(); + this.mockLogger.verify(); + + this.sandbox.restore(); + + delete this.mockYargs; + delete this.mockLogger; + }); + + after(() => { + delete this.command; + delete this.sandbox; + }); + + describe('version()', () => { + + it('Should apply version directive to yargs', () => { + const expVersion = this.mockYargs.expects('version') + .once() + .withArgs(sinon.match(/^[0-9]\.[0-9]\.[0-9]$/)) + .returns(yargs); + + assert.equal(yargs, version(yargs, this.command)); + }); + + it('Should NOT apply version directive to yargs', () => { + this.command.dirname = './notexistent'; + const expVersion = this.mockYargs.expects('version').never(); + const expLogger = this.mockLogger.expects('_add') + .once() + .withArgs(sinon.match.string) + .returns(logger); + + const expLoggerDebug = this.mockLogger.expects('debug') + .once() + .withArgs(sinon.match.func) + .returns(logger); + + assert.equal(yargs, version(yargs, this.command)); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration.spec.es6 b/test/lib/visitors/configuration.spec.es6 new file mode 100644 index 0000000..eadaa94 --- /dev/null +++ b/test/lib/visitors/configuration.spec.es6 @@ -0,0 +1,357 @@ +/** +* @module visitors +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Configuration from 'visitors/configuration'; +import Command from 'command'; +import Factory from 'util/factory/factory'; +import QueueAsync from 'util/adt/queue-async'; +import logger, { Logger } from 'util/logger/logger'; + +describe('visitors.Configuration', function() { + + before(() => { + this.command = Command.new(); + if(!this.configuration) this.configuration = Configuration.new(this.command); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Configuration.prototype); + this.mockQueueAsync = this.sandbox.mock(QueueAsync.prototype); + this.mockFactory = this.sandbox.mock(Factory); + this.mockCommand = this.sandbox.mock(Command.prototype); + this.mockLogger = this.sandbox.mock(Logger.prototype); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockQueueAsync.verify(); + this.mockFactory.verify(); + this.mockCommand.verify(); + this.mockLogger.verify(); + + this.sandbox.restore(); + + delete this.mockProto; + delete this.mockQueueAsync; + delete this.mockFactory; + delete this.mockCommand; + delete this.mockLogger; + }); + + after(() => { + delete this.command; + delete this.configuration; + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + assert.instanceOf(this.configuration, Configuration); + assert.equal('ConfigurationVisitor', this.configuration.name); + assert.property(this.configuration, 'queue'); + assert.instanceOf(this.configuration.queue, QueueAsync); + }); + + }); + + describe('visit()', () => { + + it('Should visit configuration visitor', () => { + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(this.command) + .returns(true); + + const exp = this.configuration.visit(this.command); + assert.instanceOf(exp, Command); + assert.property(exp, 'configuration'); + }); + + it('Should NOT visit configuration visitor (invalid)', () => { + const input = Command.new(); + const expValidate = this.mockProto.expects('validate') + .once() + .withArgs(input) + .returns(false); + + const exp = this.configuration.visit(input); + assert.instanceOf(exp, Command); + assert.notProperty(exp, 'configuration'); + }); + + }); + + describe('_create()', () => { + + it('Should enqueue configuration decorator methods', () => { + const options = { option: true }; + const expGet = this.mockFactory.expects('get') + .once() + .withArgs('visitors/configuration/remote') + .returns(options); + + const expOffer = this.mockQueueAsync.expects('offer') + .once() + .withArgs(options) + .returns(this.configuration.queue); + + const exp = this.configuration._create('visitors/configuration/remote'); + assert.instanceOf(exp, Configuration); + }); + + }); + + describe('_format()', () => { + + it('Should apply formatter to the current configuration option', () => { + const options = { scan: './src/**', extensions: '.js,.es6' }; + const expTransform = ['.js', '.es6']; + const formatterPath = 'visitors/configuration/formatter/extensions'; + const expFormatterPath = this.mockProto.expects('formatterPath') + .once() + .withArgs('extensions') + .returns(formatterPath); + + const expGet = this.mockFactory.expects('get') + .once() + .withArgs(formatterPath, options.extensions) + .returns(expTransform); + + const expExists = this.mockFactory.expects('exists') + .once() + .withArgs(formatterPath) + .returns(true); + + const exp = this.configuration._format(options, options.extensions, 'extensions'); + assert.isTrue(expFormatterPath.calledBefore(expExists)); + assert.isTrue(expExists.calledBefore(expGet)); + assert.isObject(exp); + assert.property(exp, 'scan'); + assert.property(exp, 'extensions'); + assert.equal(expTransform, exp.extensions); + }); + + it('Should NOT apply formatter to the current configuration option', () => { + const options = { scan: './src/**' }; + const formatterPath = 'visitors/configuration/formatter/scan'; + const expFormatterPath = this.mockProto.expects('formatterPath') + .once() + .withArgs('scan') + .returns(formatterPath); + + const expGet = this.mockFactory.expects('get').never(); + + const expExists = this.mockFactory.expects('exists') + .once() + .withArgs(formatterPath) + .returns(false); + + const exp = this.configuration._format(options, options.scan, 'scan'); + assert.isTrue(expFormatterPath.calledBefore(expExists)); + assert.isObject(exp); + assert.property(exp, 'scan'); + assert.equal(options.scan, exp.scan); + }); + + }); + + describe('_source()', () => { + + it('Should apply source configuration options to the current command', () => { + const options = { scan: './src/**', extensions: ['.js', '.es6'], unrecognized: true }; + assert.instanceOf(this.configuration._source(options), Configuration); + + assert.property(this.command, 'scan'); + assert.property(this.command, 'extensions'); + assert.notProperty(this.command, 'unrecognized'); + + delete this.command.scan; + delete this.command.extensions; + }); + + }); + + describe('_target()', () => { + + it('Should apply target configuration options to the current command', () => { + const options = { cjs: { destination: './dist', format: 'cjs', unrecognized: false }, unrecognized: true }; + assert.instanceOf(this.configuration._target(options), Configuration); + + assert.property(this.command, 'target'); + assert.property(this.command.target, 'cjs'); + assert.notProperty(this.command.target.cjs, 'unrecognized'); + }); + + }); + + describe('_logger()', () => { + + it('Should apply logger configuration option to the singleton logger', () => { + assert.instanceOf(this.configuration._logger('silent'), Configuration); + logger.level(Logger.level.output); + }); + + }); + + describe('_override()', () => { + + it('Should override configuration options with cli options', () => { + const options = { scan: './src/**', extensions: ['.js', '.es6'], unrecognized: true }; + const filtered = _.omit(options, 'unrecognized'); + const expFormat = this.mockProto.expects('_format') + .exactly(2) + .withArgs(filtered, sinon.match.any, sinon.match.string) + .returns(filtered); + + assert.instanceOf(this.configuration._override(options), Configuration); + assert.property(this.command, 'scan'); + assert.property(this.command, 'extensions'); + assert.notProperty(this.command, 'unrecognized'); + + delete this.command.scan; + delete this.command.extensions; + }); + + }); + + describe('formatterPath()', () => { + + it('Should resolve formatter path to the factory', () => { + const exp = 'visitors/configuration/formatter/target'; + assert.equal(exp, this.configuration.formatterPath('target')); + }); + + }); + + describe('onParse()', () => { + + it('Should iterate over results gather from commander to perform transformations', () => { + const expResults = [{ option: 1 }, { option: 2 }, null]; + const expOnOptions = this.mockProto.expects('onOptions') + .exactly(2) + .withArgs(sinon.match.object) + .returns(this.configuration); + + assert.instanceOf(this.configuration.onParse(expResults), Configuration); + }); + + }); + + describe('onParseError()', () => { + + it('Should output possible warning', () => { + const expMessage = 'Warning Message'; + const expLogger = this.mockLogger.expects('_add') + .once() + .withArgs(expMessage) + .returns(logger); + + const expWarn = this.mockLogger.expects('warn') + .once() + .returns(logger); + + assert.instanceOf(this.configuration.onParseError(expMessage), Configuration); + }); + + }); + + describe('onOptions()', () => { + + it('Should perform options transformations', () => { + const expResult = { + source: { scan: './src/**' }, + target: { cjs: { destination: './dist', format: 'cjs' } }, + logLevel: 'silent' + }; + const expSource = this.mockProto.expects('_source') + .once() + .withArgs(expResult.source) + .returns(this.configuration); + const expTarget = this.mockProto.expects('_target') + .once() + .withArgs(expResult.target) + .returns(this.configuration); + const expLogger = this.mockProto.expects('_logger') + .once() + .withArgs(expResult.logLevel) + .returns(this.configuration); + const expOverride = this.mockProto.expects('_override') + .once() + .withArgs(this.command.options) + .returns(this.configuration); + + assert.instanceOf(this.configuration.onOptions(expResult), Configuration); + }); + + it('Should NOT perform option transformations (Parse Error)', () => { + const expResult = { warn: 'Warning Message' }; + + const expParseError = this.mockProto.expects('onParseError') + .once() + .withArgs(expResult.warn) + .returns(this.configuration); + const expSource = this.mockProto.expects('_source').never(); + const expTarget = this.mockProto.expects('_target').never(); + const expLogger = this.mockProto.expects('_logger').never(); + const expOverride = this.mockProto.expects('_override').never(); + + assert.instanceOf(this.configuration.onOptions(expResult), Configuration); + }); + + }); + + describe('parse()', () => { + + it('Should create methods and resolve configuration option source', () => { + const expResult = { source: { scan: './src/**' } }; + const queueStubPromise = this.sandbox.stub().returnsPromise(); + const expCreate = this.mockProto.expects('_create') + .exactly(2) + .withArgs(sinon.match.string) + .returns(this.configuration); + + const expOnParse = this.mockProto.expects('onParse') + .once() + .withArgs(expResult) + .returns(this.configuration); + + const expOnParseError = this.mockProto.expects('onParseError').never(); + + const expPoll = this.mockQueueAsync.expects('poll') + .once() + .returns(queueStubPromise.resolves(expResult)()); + + const exp = this.configuration.parse(); + assert.isTrue(exp.resolved); + assert.instanceOf(exp.resolveValue, Configuration); + }); + + it('Should create methods and reject configuration option (warn message from metthod)', () => { + const expResult = { warn: 'Warning Message' }; + let queueStubPromise = this.sandbox.stub().returnsPromise(); + const expCreate = this.mockProto.expects('_create') + .exactly(2) + .withArgs(sinon.match.string) + .returns(this.configuration); + + const expOnParse = this.mockProto.expects('onParse').never(); + + const expOnParseError = this.mockProto.expects('onParseError') + .once() + .withArgs(expResult.warn) + .returns(this.configuration); + + const expPoll = this.mockQueueAsync.expects('poll') + .once() + .returns(queueStubPromise.rejects(expResult.warn)()); + + const exp = this.configuration.parse(); + assert.instanceOf(exp.resolveValue, Configuration); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/formatter/alias.spec.es6 b/test/lib/visitors/configuration/formatter/alias.spec.es6 new file mode 100644 index 0000000..3cb2e9c --- /dev/null +++ b/test/lib/visitors/configuration/formatter/alias.spec.es6 @@ -0,0 +1,29 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import alias from 'visitors/configuration/formatter/alias'; + +describe('visitors.configuration.formatter.Alias', function() { + + describe('alias()', () => { + + it('Should transform parameter alias', () => { + const input = 'one:./path/one,two:./path/two'; + const exp = alias(input); + + assert.isObject(exp); + assert.property(exp, 'one'); + assert.property(exp, 'two'); + assert.propertyVal(exp, 'one', './path/one'); + assert.propertyVal(exp, 'two', './path/two'); + }); + + it('Should NOT transform parameter alias', () => { + const exp = alias(); + assert.isTrue(_.isEmpty(exp)); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/formatter/exclude.spec.es6 b/test/lib/visitors/configuration/formatter/exclude.spec.es6 new file mode 100644 index 0000000..5f2fa80 --- /dev/null +++ b/test/lib/visitors/configuration/formatter/exclude.spec.es6 @@ -0,0 +1,30 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import exclude from 'visitors/configuration/formatter/exclude'; + +describe('visitors.configuration.formatter.Exclude', function() { + + describe('exclude()', () => { + + it('Should transform parameter exclude', () => { + const input = './path/one,./path/two'; + const exp = exclude(input); + + assert.isArray(exp); + assert.oneOf('./path/one', exp); + assert.oneOf('./path/two', exp); + }); + + it('Should NOT transform parameter exclude', () => { + let exp = exclude(); + assert.isTrue(_.isEmpty(exp)); + + exp = exclude({}); + assert.isTrue(_.isEmpty(exp)); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/formatter/extensions.spec.es6 b/test/lib/visitors/configuration/formatter/extensions.spec.es6 new file mode 100644 index 0000000..6290b28 --- /dev/null +++ b/test/lib/visitors/configuration/formatter/extensions.spec.es6 @@ -0,0 +1,31 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import extensions from 'visitors/configuration/formatter/extensions'; + +describe('visitors.configuration.formatter.Extensions', function() { + + describe('extensions()', () => { + + it('Should transform parameter extensions', () => { + const input = '.js,.es6,.jsx'; + const exp = extensions(input); + + assert.isArray(exp); + assert.oneOf('.js', exp); + assert.oneOf('.es6', exp); + assert.oneOf('.jsx', exp); + }); + + it('Should NOT transform parameter extensions', () => { + let exp = extensions(); + assert.isTrue(_.isEmpty(exp)); + + exp = extensions({}); + assert.isTrue(_.isEmpty(exp)); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/formatter/target.spec.es6 b/test/lib/visitors/configuration/formatter/target.spec.es6 new file mode 100644 index 0000000..8de6fde --- /dev/null +++ b/test/lib/visitors/configuration/formatter/target.spec.es6 @@ -0,0 +1,50 @@ +/** +* @module visitors.configuration.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import target from 'visitors/configuration/formatter/target'; + +describe('visitors.configuration.formatter.Target', function() { + + describe('target()', () => { + + it('Should transform parameter target', () => { + const input = 't1>umd:./dist/t1,t2>cjs:./dist/t2,t3>inv:./dist/t3'; + const exp = target(input); + + assert.isObject(exp); + assert.property(exp, 't1'); + assert.property(exp, 't2'); + assert.notProperty(exp, 't3'); + + assert.property(exp.t1, 'destination'); + assert.property(exp.t1, 'format'); + + assert.property(exp.t2, 'destination'); + assert.property(exp.t2, 'format'); + }); + + it('Should NOT transform ALL parameter targets (invalid nomenclature)', () => { + let input = 't1>umd:./dist/t1,t2:./dist/t2'; + let exp = target(input); + + assert.isObject(exp); + assert.property(exp, 't1'); + assert.notProperty(exp, 't2'); + + input = 't1,t2>cjs:./dist/t2'; + exp = target(input); + + assert.isObject(exp); + assert.property(exp, 't2'); + assert.notProperty(exp, 't1'); + }); + + it('Should NOT transform parameter target', () => { + const exp = target(); + assert.isTrue(_.isEmpty(exp)); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/local.spec.es6 b/test/lib/visitors/configuration/local.spec.es6 new file mode 100644 index 0000000..1d58c44 --- /dev/null +++ b/test/lib/visitors/configuration/local.spec.es6 @@ -0,0 +1,214 @@ +/** +* @module visitors.configuration +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Local from 'visitors/configuration/local'; +import Command from 'command'; +import QueueAsync from 'util/adt/queue-async'; +import logger, { Logger } from 'util/logger/logger'; + +describe('visitors.configuration.Local', function() { + + before(() => { + this.local = null; + this.options = { config: 'test/specs/.sqboxrc' }; + this.command = Command.new(); + this.configPath = path.resolve(this.command.cwd, this.options.config); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Local.prototype); + this.mockCommand = this.sandbox.mock(Command.prototype); + this.mockLogger = this.sandbox.mock(Logger.prototype); + this.mockFs = this.sandbox.mock(fs); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockCommand.verify(); + this.mockLogger.verify(); + this.mockFs.verify(); + + this.sandbox.restore(); + + delete this.mockProto; + delete this.mockCommand; + delete this.mockLogger; + delete this.mockFs; + }); + + after(() => { + delete this.local; + delete this.options; + delete this.command; + delete this.configPath; + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + this.local = Local.new(this.command); + assert.instanceOf(this.local, Local); + assert.property(this.local, 'command'); + }); + + }); + + describe('resolvePath()', () => { + + it('Should resolve absolute path to a file', () => { + const exp = this.local.resolvePath(this.options.config); + assert.equal(this.configPath, exp); + }); + + }); + + describe('exists()', () => { + + it('Should return true: file exists', () => { + const expStatSync = this.mockFs.expects('statSync') + .once() + .withArgs(this.configPath) + .returns({}); + + assert.isTrue(this.local.exists(this.configPath)); + }); + + it('Should return false: file doesn\'t exists (file is undefined)', () => { + const expStatSync = this.mockFs.expects('statSync').never(); + assert.isFalse(this.local.exists()); + }); + + it('Should return false: file doesn\'t exists (file not found in path)', () => { + const expStatSync = this.mockFs.expects('statSync') + .once() + .withArgs(this.configPath) + .throws(new Error('Not Found')); + + assert.isFalse(this.local.exists(this.configPath)); + }); + + }); + + describe('tryJson()', () => { + + it('Should return an object: valid json', () => { + const expReadJsonSync = this.mockFs.expects('readJsonSync') + .once() + .withArgs(this.configPath) + .returns({ valid: true }); + assert.isObject(this.local.tryJson(this.configPath)); + }); + + it('Should return null: invalid json', () => { + const expReadJsonSync = this.mockFs.expects('readJsonSync') + .once() + .withArgs(this.configPath) + .throws(new Error('Invalid JSON')); + assert.isNull(this.local.tryJson(this.configPath)); + }); + + }); + + describe('tryJs()', () => { + + it('Should return an object: valid javascript', () => { + const jspath = path.resolve(this.command.cwd, 'test/specs/.sqbox.js'); + assert.isObject(this.local.tryJs(jspath)); + }); + + it('Should return null: invalid javascript (file not found)', () => { + const jspath = path.resolve(this.command.cwd, 'test/specs/.sqbox.ext'); + assert.isNull(this.local.tryJs(jspath)); + }); + + }); + + describe('load()', () => { + + it('Should load configuration file', () => { + const input = { source: { scan: './src/**' } }; + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(sinon.match.string) + .returns(true); + + const expTryJson = this.mockProto.expects('tryJson') + .once() + .withArgs(sinon.match.string) + .returns(input); + + const exp = this.local.load(this.options.config); + assert.isObject(exp); + assert.deepEqual(input, exp); + }); + + it('Should NOT load configuration file (file doesn\'t exists)', () => { + const input = { warn: Local.messages.notFound }; + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(sinon.match.string) + .returns(false); + + const expTryJson = this.mockProto.expects('tryJson').never(); + const expTryJs = this.mockProto.expects('tryJs').never(); + + const exp = this.local.load(this.options.config); + assert.isObject(exp); + assert.deepEqual(input, exp); + }); + + it('Should NOT load configuration file (Invalid Json && Javascript)', () => { + const input = { warn: Local.messages.invalid }; + const expExists = this.mockProto.expects('exists') + .once() + .withArgs(sinon.match.string) + .returns(true); + + const expTryJson = this.mockProto.expects('tryJson') + .once() + .withArgs(sinon.match.string) + .returns(null); + + const expTryJs = this.mockProto.expects('tryJs') + .once() + .withArgs(sinon.match.string) + .returns(null); + + const exp = this.local.load(this.options.config); + assert.isObject(exp); + assert.deepEqual(input, exp); + }); + + }); + + describe('next()', () => { + + it('Should resolve promise for asynchronous operation', (done) => { + const expResolved = { result: true }; + const resolvePromise = this.sandbox.stub().returnsPromise(); + const spyResolve = this.sandbox.spy(); + + const expGetOptions = this.mockCommand.expects('getOptions') + .once() + .returns(this.options); + + const expLoad = this.mockProto.expects('load') + .once() + .withArgs(this.options.config) + .returns(expResolved); + + const exp = this.local.next({}, _.bind(resolvePromise.resolves, resolvePromise), spyResolve)(); + exp.then((result) => { + assert.isTrue(exp.resolved); + assert.deepEqual(expResolved, exp.resolveValue); + assert.isTrue(spyResolve.notCalled); + done(); + }); + }); + + }); + +}); diff --git a/test/lib/visitors/configuration/remote.spec.es6 b/test/lib/visitors/configuration/remote.spec.es6 new file mode 100644 index 0000000..bd0a22d --- /dev/null +++ b/test/lib/visitors/configuration/remote.spec.es6 @@ -0,0 +1,174 @@ +/** +* @module visitors.configuration +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import Remote from 'visitors/configuration/remote'; +import Command from 'command'; +import QueueAsync from 'util/adt/queue-async'; +import request from 'request-promise'; +import logger, { Logger } from 'util/logger/logger'; + +describe('visitors.configuration.Remote', function() { + + before(() => { + this.remote = null; + this.options = { url: 'http://squarebox.nahuel.io/.sqboxrc' }; + this.command = Command.new(); + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.mockProto = this.sandbox.mock(Remote.prototype); + this.mockCommand = this.sandbox.mock(Command.prototype); + this.mockLogger = this.sandbox.mock(Logger.prototype); + }); + + afterEach(() => { + this.mockProto.verify(); + this.mockCommand.verify(); + this.mockLogger.verify(); + + this.sandbox.restore(); + + delete this.mockProto; + delete this.mockCommand; + delete this.mockLogger; + }); + + after(() => { + delete this.remote; + delete this.options; + delete this.command; + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + this.remote = Remote.new(this.command); + assert.instanceOf(this.remote, Remote); + assert.property(this.remote, 'command'); + }); + + }); + + describe('onResponse()', () => { + + it('Should resolve promise successfuly', (done) => { + const output = { option: true }; + const expPromise = this.sandbox.stub().returnsPromise(); + const bindResolves = _.bind(expPromise.resolves, expPromise); + const bindRejects = _.bind(expPromise.rejects, expPromise); + + const exp = this.remote.onResponse(bindResolves, bindRejects, output)(); + exp.then((result) => { + assert.equal(output, result); + done(); + }); + }); + + }); + + describe('onResponseError()', () => { + + it('Should resolve promise with Error', () => { + const err = 'Remote Error'; + const output = { warn: Remote.messages.error({ err }) }; + const expPromise = this.sandbox.stub().returnsPromise(); + const bindResolves = _.bind(expPromise.resolves, expPromise); + const bindRejects = _.bind(expPromise.rejects, expPromise); + + const exp = this.remote.onResponseError(bindResolves, bindRejects, err)(); + exp.then((result) => { + assert.deepEqual(output, result); + done(); + }); + }); + + }); + + describe('load()', () => { + + it('Should execute a remote request (resolves)', () => { + const output = { option: true }; + const expPromise = this.sandbox.stub(request, 'get').returnsPromise(); + + const spyInnerResolve = this.sandbox.spy(); + const spyInnerReject = this.sandbox.spy(); + + const expOnResponseError = this.mockProto.expects('onResponseError').never(); + const expOnResponse = this.mockProto.expects('onResponse') + .once() + .withArgs(spyInnerResolve, spyInnerReject, output) + .returns(spyInnerResolve(output)); + + const exp = this.remote.load(this.options.url, spyInnerResolve, spyInnerReject); + expPromise.resolves(output); + + assert.isTrue(spyInnerResolve.called); + assert.isFalse(spyInnerReject.called); + }); + + it('Should execute a remote request (rejects)', () => { + const output = 'Remote Error'; + const expPromise = this.sandbox.stub(request, 'get').returnsPromise(); + + const spyInnerResolve = this.sandbox.spy(); + const spyInnerReject = this.sandbox.spy(); + + const expOnResponse = this.mockProto.expects('onResponse').never(); + const expOnResponseError = this.mockProto.expects('onResponseError') + .once() + .withArgs(spyInnerResolve, spyInnerReject, output) + .returns(spyInnerResolve(output)); + + const exp = this.remote.load(this.options.url, spyInnerResolve, spyInnerReject); + expPromise.rejects(output); + + assert.isTrue(spyInnerResolve.called); + assert.isFalse(spyInnerReject.called); + }); + + }); + + describe('next()', () => { + + it('Should resolve promise with not warning (Url available)', () => { + const output = { option: true }; + const spyResolve = this.sandbox.spy(); + const spyReject = this.sandbox.spy(); + + const expGetOptions = this.mockCommand.expects('getOptions') + .once() + .returns(this.options); + + const expLoad = this.mockProto.expects('load') + .once() + .withArgs(this.options.url, spyResolve, spyReject) + .returns(spyResolve(output)); + + const exp = this.remote.next({}, spyResolve, spyReject); + + assert.equal(output.option, spyResolve.firstCall.args[0].option); + assert.isTrue(spyResolve.called); + assert.isFalse(spyReject.called); + }); + + it('Should resolve promise with warning (No Url available)', () => { + const spyResolve = this.sandbox.spy(); + const spyReject = this.sandbox.spy(); + + const expGetOptions = this.mockCommand.expects('getOptions') + .once() + .returns({}); + + const exp = this.remote.next({}, spyResolve, spyReject); + + assert.equal(Remote.messages.noUrl, spyResolve.firstCall.args[0].warn); + assert.isTrue(spyResolve.called); + assert.isFalse(spyReject.called); + }); + + }); + +}); diff --git a/test/lib/visitors/formatter/json.spec.es6 b/test/lib/visitors/formatter/json.spec.es6 new file mode 100644 index 0000000..c45594e --- /dev/null +++ b/test/lib/visitors/formatter/json.spec.es6 @@ -0,0 +1,86 @@ +/** +* @module visitors.formatter +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +import _ from 'underscore'; +import Json from 'visitors/formatter/json'; +import Visited from 'util/visitor/visited'; +import Command from 'command'; +import InterfaceException from 'util/exception/proxy/interface'; + +describe('visitors.formatter.Json', function() { + + before(() => { + this.sandbox = sinon.sandbox.create(); + }); + + beforeEach(() => { + this.target = () => { + return { + a: 1, + b: true, + regexp: new RegExp(), + func: () => {}, + obj: { o_a: 1, o_b: true, func: () => {}, arr: [1, true, () => {}] }, + arr: [{ + a_o_a: 1, + a_o_b: true, + func: () => {}, + command: Command.new() + }, 1, true, () => {}] + }; + }; + this.mockJson = this.sandbox.mock(Json.prototype); + }); + + afterEach(() => { + this.mockJson.verify(); + this.sandbox.restore(); + delete this.target; + delete this.mockJson; + }); + + after(() => { + delete this.sandbox; + }); + + describe('constructor()', () => { + + it('Should get a new instance', () => { + const exp = Json.new(); + assert.instanceOf(exp, Json); + assert.equal('JsonVisitor', exp.name); + }); + + }); + + describe('visit()', () => { + + it('Should visit the target', () => { + const input = Visited.new({ property: 'target' }); + const exp = Json.new(); + assert.instanceOf(exp.visit(input), Visited); + }); + + it('Should NOT visit the target', () => { + const input = { property: 'target' }; + const exp = Json.new(); + assert.throws(() => exp.visit(input), InterfaceException.type + .interface({ name: 'util.visitor.Visited' })); + }); + + }); + + describe('toJSON()', () => { + + it('Should return a json representation', () => { + const input = this.target(); + const visited = Visited.new(input); + const exp = Json.new().visit(visited); + const out = exp.toJSON(); + assert.notDeepEqual(input, out); + }); + + }); + +}); diff --git a/test/specs/.sqbox.js b/test/specs/.sqbox.js new file mode 100644 index 0000000..d78e20d --- /dev/null +++ b/test/specs/.sqbox.js @@ -0,0 +1,34 @@ +/** +* Config Example using module.exports +* @author Patricio Ferreira <3dimentionar@gmail.com> +**/ +module.exports = { + source: { + scan: './src/**', + exclude: ['./src/dependencies/**'], + extensions: ['.js', '.es6', '.es'], + alias: { + common: 'shared/common', + libraries: 'libs' + } + }, + target: { + global: { + destination: './dist/global', + format: 'ifie' + }, + umd: { + destination: './dist/umd', + format: 'umd' + }, + cjs: { + destination: './dist/cjs', + format: 'cjs' + }, + amd: { + destination: './dist/amd', + format: 'amd' + } + }, + logLevel: 'debug' +}; diff --git a/test/specs/.sqboxrc b/test/specs/.sqboxrc new file mode 100644 index 0000000..2d6e97e --- /dev/null +++ b/test/specs/.sqboxrc @@ -0,0 +1,30 @@ +{ + "source": { + "scan": "./src/**", + "exclude": ["./src/dependencies/**"], + "extensions": [".js", ".es6"], + "alias": { + "common": "shared/common", + "libraries": "libs" + } + }, + "target": { + "global": { + "destination": "./dist/global", + "format": "ifie" + }, + "umd": { + "destination": "./dist/umd", + "format": "umd" + }, + "cjs": { + "destination": "./dist/cjs", + "format": "cjs" + }, + "amd": { + "destination": "./dist/amd", + "format": "amd" + } + }, + "logLevel": "debug" +} diff --git a/test/specs/config-basic.json b/test/specs/config-basic.json deleted file mode 100644 index 06b89cc..0000000 --- a/test/specs/config-basic.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "source": { - "scan": "./src/**", - "extensions": [".js", ".es6"], - "alias": { - "common": "shared/common", - "libraries": "libs" - } - }, - "target": { - "destination": "./dist", - "format": "ifie" - } -}