├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE-MIT ├── README.md ├── lib ├── cocktail.js └── processor │ ├── NoOp.js │ ├── annotation │ ├── Annotation.js │ ├── Exports.js │ ├── Extends.js │ ├── Merge.js │ ├── Properties.js │ ├── Requires.js │ ├── Static.js │ ├── Talents.js │ └── Traits.js │ ├── defaults.js │ └── sequence.js ├── package.json └── test ├── .eslintrc ├── helper └── RestoreProcessors.js ├── integration ├── cocktail-annotations.js ├── cocktail-exports.js ├── cocktail-extends.js ├── cocktail-merge.js ├── cocktail-properties.js ├── cocktail-single-arg.js └── cocktail-static.js └── unit ├── cocktail.js └── processor ├── NoOp.js ├── annotation ├── Annotation.js ├── Exports.js ├── Extends.js ├── Merge.js ├── Properties.js ├── Requires.js ├── Static.js ├── Talents.js └── Traits.js └── sequence.js /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | node: true 4 | 5 | rules: 6 | # ERRORS 7 | 8 | brace-style: 2 9 | keyword-spacing: 2 10 | strict: [2, "global"] 11 | no-unused-expressions: 2 12 | block-scoped-var: 2 13 | eol-last: 2 14 | dot-notation: 2 15 | consistent-return: 2 16 | no-unused-vars: [2, args: none] 17 | quotes: [2, 'single'] 18 | 19 | # DISABLED 20 | 21 | # We use this for private/internal variables 22 | no-underscore-dangle: 0 23 | 24 | key-spacing: 0 25 | no-multi-spaces: 0 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /coverage/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | test*/ 3 | .DS_Store 4 | .jshintrc 5 | .eslintrc 6 | .travis.yml 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | - "0.12" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGE LOG 2 | 3 | - 0.7.3 4 | - Fixed issue #27: `cocktail.use` issues with named function class constructors. 5 | - Added test to cover #27. 6 | - Copyright notice updated. 7 | - Updated dev dependencies. 8 | - Fixed linting issues. 9 | 10 | - 0.7.2 11 | - Fixed issue #26: Recursive loop when nesting `callSuper`. 12 | 13 | - 0.7.1 14 | - `@traits` & `@talents` support for ES6 Classes defintion. 15 | 16 | - 0.7.0 17 | - status: Beta 18 | - `@traits` and `@talents` annotations accept Object definitions. See issue #23. 19 | - Improved code for Merge, Trait and Talent processors. 20 | - Removed grunt dependencies. Using npm scripts. 21 | - Implemented ESLint for code style. 22 | - Added coverage check using istanbul. 23 | 24 | - 0.6.0 25 | - status: Beta 26 | - New `@merge` annotation parameter: `"properties"` to only apply properties in the mix. 27 | - Fixed issue with Traits / Talents same method detection not working properly when comparing methods in different modules. 28 | 29 | - 0.5.3 30 | - status: Alpha 31 | - Fixed issue with `@talents` applied to a Class. See #19 32 | 33 | - 0.5.2 34 | - status: Alpha 35 | - Fixed issue with `@properties` annotation to not override value when subject is an object and it has a property already defined with the same name. See #18. 36 | 37 | - 0.5.1 38 | - status: Alpha 39 | - Added 'module' as parameter in `@as` pseudo-annotation to export module as single object 40 | 41 | - 0.5.0 42 | - status: Alpha 43 | - Added new method to register custom annotations (cocktail.use()) 44 | - Refactored `@annotation` processor. 45 | - Small code improvements and tests. 46 | 47 | - 0.4.6 48 | - status: Alpha 49 | - Fixed issue with Traits/Talents throwing errors on excluded/aliased methods. See #13. 50 | 51 | - 0.4.5 52 | - status: Alpha 53 | - Fixed issue with Traits/Talents required methods check with traits defined in different modules. 54 | 55 | - 0.4.4 56 | - status: Alpha 57 | - Renamed file lib/Cocktail.js to lib/cocktail.js to agree on module name conventions. 58 | - Changed examples to use `require('cocktail')` to avoid issues on Case Sensitive File Systems (like Linux) 59 | 60 | - 0.4.3 61 | - status: Alpha 62 | - Fixed issue with mix being called from an annotation process. 63 | - Test added. 64 | 65 | - 0.4.2 66 | - status: Alpha 67 | - Fixed issue with constructor chain parameters. 68 | - Test added for constructor chain parameters. 69 | 70 | - 0.4.1 71 | - status: Alpha 72 | - Fixed issue with constructor chain. 73 | - Test added for single parameter class definition constructor chain. 74 | 75 | - 0.4.0 76 | - status: Alpha 77 | - Added `@static` annotation to define static members on class mix. 78 | - Tests for `@static` annotation. 79 | - Refactored Merge processor. Added constructor parameter to reuse functionality on Static processor. 80 | 81 | - 0.3.0 82 | - status: Alpha 83 | - Introduced pseudo-annotation `@as` intended for single parameter class definition 84 | - Tests for pseudo-annotation `@as` 85 | 86 | - 0.2.0 87 | - status: Alpha 88 | - Added single parameter class/trait definition. If the first parameter is an object literal and it contains a 89 | constructor definition, or the annotation '@extends', '@traits', '@requires' or '@annotation' it will be treated as 90 | a class definition. 91 | - Tests for single parameter definition. 92 | 93 | - 0.1.1 94 | - status: Alpha 95 | - Added `@exports` annotation. 96 | - Documentation update. 97 | - Tests for @exports annotation. 98 | 99 | - 0.1.0 100 | - status: Alpha 101 | - Added sequence to define custom annotations priorities. 102 | - Documentation update. 103 | - Tests for Sequence. 104 | 105 | - 0.0.4 106 | - status: Alpha 107 | - Added new merge strategies: mine (default -same as single), their, deep-mine and deep-their. 108 | - Test for Merge strategies. 109 | 110 | - 0.0.3 111 | - status: Alpha. 112 | - Annotation, traits, talents, extends, and properties features are stable and tested. 113 | - Adde custom Annotations definitions mechanism thru @annotation. 114 | - Added merge strategy: single. 115 | 116 | - 0.0.2 117 | - status: Alpha. 118 | - Alpha version for traits, talents, extends, and properties features. 119 | - Tests. 120 | 121 | - 0.0.1 122 | - status: Alpha. 123 | - First draft version 124 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2016 Maximiliano Fierro 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cocktail JS 2 | 3 | [![Build Status](https://travis-ci.org/CocktailJS/cocktail.svg?branch=master)](https://travis-ci.org/CocktailJS/cocktail) 4 | [![npm version](https://badge.fury.io/js/cocktail.svg)](http://badge.fury.io/js/cocktail) 5 | [![bitHound Score](https://www.bithound.io/CocktailJS/cocktail/badges/score.svg)](https://www.bithound.io/CocktailJS/cocktail) 6 | [![Code Climate](https://codeclimate.com/github/CocktailJS/cocktail/badges/gpa.svg)](https://codeclimate.com/github/CocktailJS/cocktail) 7 | 8 | Cocktail is a small but yet powerful library with very simple principles: 9 | 10 | - Reuse code 11 | - Keep it simple 12 | 13 | ## Reuse code 14 | Cocktail explores three mechanisms to share/reuse/mix code: 15 | 16 | - **Extends**: OOP inheritance implemented in Javascript. 17 | - **Traits**: Traits are composable behavior units that can be added to a Class. 18 | - **Talents**: Same idea as Traits but applied to instances of a Class. 19 | 20 | 21 | ## Keep it simple 22 | Cocktail has only one public method `cocktail.mix()` but it relies on `annotations` to tag some meta-data that describe the mix. 23 | 24 | ## Annotations 25 | Annotations are simple meta-data Cocktail uses to perform some tasks over the given mix. They become part of the process but usually they are not kept in the result of a mix. 26 | 27 | ```js 28 | var cocktail = require('cocktail'), 29 | MyClass = function(){}; 30 | 31 | cocktail.mix(MyClass, { 32 | '@properties': { 33 | name: 'default name' 34 | } 35 | }); 36 | ``` 37 | 38 | In the example above we created a "Class" named _MyClass_, and we use the `@properties` annotation to create the property _name_ and the corresponding _setName_ and _getName_ methods. 39 | 40 | As it was mentioned before, annotations are meta-data, which means that they are not part of _MyClass_ or its prototype. 41 | 42 | ## Defining a Class / Module 43 | Using cocktail to define a class is easy and elegant. 44 | 45 | ```js 46 | var cocktail = require('cocktail'); 47 | 48 | cocktail.mix({ 49 | '@exports': module, 50 | '@as': 'class', 51 | 52 | '@properties': { 53 | name: 'default name' 54 | }, 55 | 56 | constructor: function(name){ 57 | this.setName(name); 58 | }, 59 | 60 | sayHello: function() { 61 | return 'Hello, my name is ' + this.getName(); 62 | } 63 | }); 64 | 65 | ``` 66 | In this example our class definition uses `@exports` to tell the mix we want to export the result in the `module.exports` and `@as` tells it is a class. 67 | 68 | ## Traits 69 | _Traits_ are **Composable Units of Behaviour** (You can read more from [this paper](http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf)). 70 | Basically, a Trait is a Class, but a special type of Class that has only behaviour (methods) and no state. 71 | Traits are an alternative to reuse behaviour in a more predictable manner. They are more robust than _Mixins_, or 72 | _Multiple Inheritance_ since name collisions must be solved by the developer beforehand. If you compose your class 73 | with one or more Traits and you have a method defined in more than one place, your program will fail giving no magic rule 74 | or any kind of precedence definition. 75 | 76 | > Enumerable.js 77 | 78 | ```js 79 | var cocktail = require('cocktail'); 80 | 81 | cocktail.mix({ 82 | '@exports': module, 83 | '@as': 'class', 84 | 85 | '@requires': ['getItems'], 86 | 87 | first: function() { 88 | var items = this.getItems(); 89 | return items[0] || null; 90 | }, 91 | 92 | last: function() { 93 | var items = this.getItems(), 94 | l = items.length; 95 | return items[l-1]; 96 | } 97 | 98 | }); 99 | 100 | 101 | ``` 102 | The class above is a Trait declaration for an Enumerable functionality. 103 | In this case we only defined `first` and `last` methods to retrieve the 104 | corresponding elements from an array retrieved by `getItems` methods. 105 | 106 | > List.js 107 | 108 | ```js 109 | var cocktail = require('cocktail'), 110 | Enumerable = require('./Enumerable'); 111 | 112 | cocktail.mix({ 113 | '@exports': module, 114 | '@as': 'class', 115 | '@traits': [Enumerable], 116 | 117 | '@properties': { 118 | items: undefined 119 | }, 120 | 121 | '@static': { 122 | /* factory method*/ 123 | create: function(options) { 124 | var List = this; 125 | return new List(options); 126 | } 127 | }, 128 | 129 | constructor: function (options) { 130 | this.items = options.items || []; 131 | } 132 | }); 133 | 134 | 135 | ``` 136 | 137 | The List class uses the Enumerable Trait, the getItems is defined by the `@properties` annotation. 138 | 139 | > index.js 140 | 141 | ```js 142 | var List = require('./List'), 143 | myArr = ['one', 'two', 'three'], 144 | myList; 145 | 146 | myList = List.create({items: myArr}); 147 | 148 | console.log(myList.first()); // 'one' 149 | console.log(myList.last()); // 'three' 150 | 151 | 152 | ``` 153 | 154 | 155 | ## Talents 156 | _Talents_ are very similar to Traits, in fact a Trait can be applied as a Talent in CocktailJS. 157 | The main difference is that a Talent can be applied to an _object_ or _module_. 158 | So we can define a Talent as a **Dynamically Composable Unit of Reuse** 159 | (you can read more from [this paper](http://scg.unibe.ch/archive/papers/Ress11a-Talents.pdf)). 160 | 161 | Using the _Enumerable_ example, we can use a Trait as a Talent. 162 | 163 | > index.js 164 | 165 | ```js 166 | var cocktail = require('cocktail'), 167 | enumerable = require('./Enumerable'), 168 | myArr; 169 | 170 | myArr = ['one', 'two', 'three']; 171 | 172 | cocktail.mix(myArr, { 173 | '@talents': [enumerable], 174 | 175 | /* glue code for enumerable talent*/ 176 | getItems: function () { 177 | return this; 178 | } 179 | }); 180 | 181 | console.log(myArr.first()); // 'one' 182 | console.log(myArr.last()); // 'three' 183 | 184 | 185 | ``` 186 | 187 | We can also create a new Talent to define the getItems method for an Array to retrive the current instance. 188 | 189 | > ArrayAsItems.js 190 | 191 | ```js 192 | var cocktail = require('cocktail'); 193 | 194 | cocktail.mix({ 195 | '@exports': module, 196 | '@as': 'class', 197 | 198 | getItems: function () { 199 | return this; 200 | } 201 | }); 202 | 203 | ``` 204 | And then use it with Enumerable: 205 | 206 | 207 | ```js 208 | var cocktail = require('cocktail'), 209 | enumerable = require('./Enumerable'), 210 | arrayAsItems = require('./ArrayAsItems'); 211 | 212 | var myArr = ['one', 'two', 'three']; 213 | 214 | cocktail.mix(myArr, { '@talents': [enumerable, arrayAsItems] }); 215 | 216 | console.log(myArr.first()); // 'one' 217 | console.log(myArr.last()); // 'three' 218 | 219 | ``` 220 | 221 | ## Getting Started 222 | 223 | - Install the module with: `npm install cocktail` or add cocktail to your `package.json` and then `npm install` 224 | - Start playing by just adding a `var cocktail = require('cocktail')` in your file. 225 | 226 | ## Guides 227 | Guides can be found at [CocktailJS Guides](http://cocktailjs.github.io/guides/) 228 | 229 | ## Documentation 230 | The latest documentation is published at [CocktailJS Documentation](http://cocktailjs.github.io/docs/) 231 | 232 | ## Examples 233 | A Cocktail playground can be found in [cocktail recipes](https://github.com/CocktailJS/cocktail-recipes) repo. 234 | 235 | ## Contributing 236 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. 237 | 238 | ### Running Lint & Tests 239 | Add your unit and/or integration tests and execute 240 | 241 | $ npm test 242 | 243 | 244 | ### Run unit tests 245 | 246 | $npm run unit 247 | 248 | 249 | ### Run integration tests 250 | 251 | $npm run integration 252 | 253 | 254 | ### Lint your code 255 | 256 | $ npm run lint 257 | 258 | 259 | ### Before Commiting 260 | Run `npm test` to check lint and execute tests 261 | 262 | $ npm test 263 | 264 | 265 | ### Check test code coverage with instanbul 266 | 267 | $ npm run coverage 268 | 269 | 270 | ## Release History 271 | 272 | see [CHANGELOG](https://github.com/CocktailJS/cocktail/blob/master/CHANGELOG.md) 273 | 274 | ## License 275 | Copyright (c) 2013 - 2016 Maximiliano Fierro 276 | Licensed under the MIT license. 277 | -------------------------------------------------------------------------------- /lib/cocktail.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('./processor/sequence'); 9 | var defaults = require('./processor/defaults'); 10 | 11 | var ANNOTATION_REG_EXP = /^@/; 12 | 13 | var cocktail; 14 | 15 | cocktail = { 16 | /** 17 | * @public 18 | * SEQUENCE is used to define an enumeration of priorities for annotations 19 | */ 20 | SEQUENCE: sequence, 21 | 22 | /** 23 | * @private 24 | * The processors class list. 25 | */ 26 | _DEFAULT_PROCESSORS: defaults, 27 | /** 28 | * @private 29 | * Stack of _queues 30 | */ 31 | _qStack: [], 32 | /** 33 | * @private 34 | * The queue of processors instances for the given mix 35 | */ 36 | _queue: [], 37 | 38 | /** 39 | *@private 40 | * Current processor list map 41 | */ 42 | _processors: {}, 43 | 44 | /** 45 | * @protected 46 | * Returns the processor list map 47 | */ 48 | getProcessors : function () { 49 | return this._processors; 50 | }, 51 | 52 | /** 53 | * @protected 54 | * sets the processor object list. It is an Object used as a map 55 | */ 56 | setProcessors: function (processor) { 57 | this._processors = processor; 58 | }, 59 | 60 | /** 61 | * @protected 62 | * returns the list of default processors 63 | */ 64 | getDefaultProcessors: function () { 65 | return cocktail._DEFAULT_PROCESSORS; 66 | }, 67 | 68 | /** 69 | * @protected 70 | * registers a processor definition 71 | * @param processorsConfig {Object} a key-value pair of processors 72 | */ 73 | registerProcessors: function (processorsConfig) { 74 | var processors = this.getProcessors(), 75 | key; 76 | for (key in processorsConfig){ 77 | if (processorsConfig.hasOwnProperty(key)) { 78 | processors[key] = processorsConfig[key]; 79 | } 80 | } 81 | }, 82 | 83 | /** 84 | * @public 85 | */ 86 | use: function (annotation) { 87 | var name = (annotation.prototype && annotation.prototype.name), 88 | processor = {}; 89 | 90 | if (ANNOTATION_REG_EXP.test(name) && annotation.prototype) { 91 | processor[name] = annotation; 92 | this.registerProcessors(processor); 93 | } 94 | }, 95 | 96 | /** 97 | * @private 98 | * returns a processor instance for the given key or a NoOp instance if it is not found. 99 | */ 100 | _getProcessorFor: function (key) { 101 | var processors = this.getProcessors(), 102 | P; 103 | P = (processors[key] || processors['no-op']); 104 | return new P(); 105 | }, 106 | 107 | /** 108 | * @private 109 | * applies default options to the given options parameter. 110 | * As of today, the only default option is the configuration for the merge annotation 111 | */ 112 | _applyDefaultsOptions: function (options) { 113 | if (options && !('@merge' in options)) { 114 | options['@merge'] = 'single'; 115 | } 116 | }, 117 | 118 | /** 119 | * @private 120 | * iterates over options to find annotations and adds processors to the queue. 121 | */ 122 | _configureProcessorsWith: function (options) { 123 | var key, value, processor; 124 | 125 | this._cleanQueue(); 126 | 127 | if (options) { 128 | for (key in options) { 129 | if (options.hasOwnProperty(key) && ANNOTATION_REG_EXP.test(key)) { 130 | value = options[key]; 131 | //get the processor instance for this annotation 132 | processor = this._getProcessorFor(key); 133 | //configure the annotation parameter 134 | processor.setParameter(value); 135 | //check if the annotation should be removed 136 | if (!processor.retain) { 137 | delete options[key]; 138 | } 139 | //add the processor to the queue 140 | this._addProcessorToQueue(processor); 141 | } 142 | } 143 | } 144 | }, 145 | 146 | /** 147 | * @private 148 | * stacks current queue 149 | */ 150 | _pushQueue: function () { 151 | this._qStack.push(this._queue); 152 | this._queue = []; 153 | }, 154 | 155 | /** 156 | * @private 157 | * restore current queue 158 | */ 159 | _popQueue: function () { 160 | this._queue = this._qStack.pop(); 161 | }, 162 | 163 | 164 | /** 165 | * @private 166 | * Cleans the processor queue 167 | */ 168 | _cleanQueue: function () { 169 | this._queue.length = 0; 170 | }, 171 | 172 | /** 173 | * @private 174 | * Adds the given processor to the queue 175 | */ 176 | _addProcessorToQueue: function (processor) { 177 | if (processor && processor.priority !== -1) { 178 | this._queue.push(processor); 179 | } 180 | }, 181 | 182 | /** 183 | * @private 184 | * Sorts the queue by its processor's priorities 185 | */ 186 | _sortQueueByPriority: function () { 187 | this._queue.sort(function(a, b){ 188 | return a.priority - b.priority; 189 | }); 190 | }, 191 | 192 | /** 193 | * @private 194 | * Runs all the processors in the queue over the given subject 195 | */ 196 | _executeProcessorsOn: function (subject, options) { 197 | var processors = this._queue, 198 | l = processors.length, 199 | i; 200 | 201 | this._sortQueueByPriority(); 202 | 203 | for (i = 0; i < l; i++) { 204 | processors[i].process(subject, options); 205 | } 206 | 207 | }, 208 | 209 | /** 210 | * @private 211 | * returns true if the given subject has a pseudo annotation `@as` with the given value. 212 | */ 213 | _isSubjectDefinedAs: function (subject, asType) { 214 | return (subject && subject['@as'] && subject['@as'].toLowerCase() === asType); 215 | }, 216 | 217 | /** 218 | * @private 219 | * returns true if the given subject is a class definition object. 220 | */ 221 | _isClassDefition: function (subject) { 222 | var isClassDef = this._isSubjectDefinedAs(subject, 'class'), 223 | definitionProps = ['constructor', '@extends', '@traits', '@requires', '@annotation'], 224 | key; 225 | 226 | if (!isClassDef) { 227 | for (key in subject) { 228 | if (definitionProps.indexOf(key) > -1) { 229 | isClassDef = true; 230 | break; 231 | } 232 | } 233 | } 234 | 235 | return isClassDef; 236 | }, 237 | 238 | /** 239 | * @private 240 | * returns true if the given subject is a module definition object. 241 | */ 242 | _isModuleDefinition: function (subject) { 243 | return this._isSubjectDefinedAs(subject, 'module'); 244 | }, 245 | 246 | /** 247 | * @private 248 | * If the subject has a property construtor returns it, 249 | * if no constructor on subject but it extends then return a function() calling super constructor, 250 | * or a function definition otherwise. 251 | */ 252 | _getDefaultClassConstructor: function (subject) { 253 | var ctor, parent; 254 | 255 | if (this._isPropertyDefinedIn('constructor', subject)) { 256 | ctor = subject.constructor; 257 | } else if (this._isPropertyDefinedIn('@extends', subject)) { 258 | parent = subject['@extends']; 259 | ctor = function(){ 260 | parent.prototype.constructor.apply(this, arguments); 261 | }; 262 | } else { 263 | ctor = function(){}; 264 | } 265 | 266 | return ctor; 267 | }, 268 | 269 | /** 270 | * @private 271 | * checks if the given property is enumerable and defined in the obj 272 | */ 273 | _isPropertyDefinedIn: function (property, obj) { 274 | var k; 275 | 276 | for (k in obj) { 277 | if (property === k) { 278 | return true; 279 | } 280 | } 281 | 282 | return false; 283 | }, 284 | 285 | /** 286 | * @private 287 | * returns a call to mix() with the subject constructor and options 288 | */ 289 | _processClassDefition: function (subject) { 290 | var defaultConstructor, options; 291 | 292 | defaultConstructor = this._getDefaultClassConstructor(subject); 293 | options = subject; 294 | 295 | return this.mix(defaultConstructor, options); 296 | }, 297 | 298 | /** 299 | * @private 300 | * @experimental 0.5.1 301 | * returns a call to mix() with the subject module and options 302 | */ 303 | _processModuleDefinition: function (subject) { 304 | var options = subject; 305 | return this.mix(subject, options); 306 | }, 307 | 308 | /** 309 | * @public 310 | */ 311 | mix: function (subject, options) { 312 | if (!options) { 313 | if (this._isClassDefition(subject)) { 314 | return this._processClassDefition(subject); 315 | } 316 | if (this._isModuleDefinition(subject)) { 317 | return this._processModuleDefinition(subject); 318 | } 319 | } 320 | 321 | if (subject) { 322 | this._pushQueue(); 323 | this._applyDefaultsOptions(options); 324 | this._configureProcessorsWith(options); 325 | this._executeProcessorsOn(subject, options); 326 | this._popQueue(); 327 | } 328 | 329 | return subject; 330 | } 331 | 332 | }; 333 | 334 | //register processors 335 | cocktail.registerProcessors(cocktail._DEFAULT_PROCESSORS); 336 | 337 | //export module 338 | module.exports = cocktail; 339 | -------------------------------------------------------------------------------- /lib/processor/NoOp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('./sequence'); 9 | 10 | var NoOp = function () {}; 11 | 12 | NoOp.prototype = { 13 | retain : false, 14 | priority : sequence.NO_OP, 15 | name : 'noOp', 16 | 17 | setParameter: function(){}, 18 | getParameter: function(){} 19 | }; 20 | 21 | module.exports = NoOp; 22 | -------------------------------------------------------------------------------- /lib/processor/annotation/Annotation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | function Annotation () {} 11 | 12 | Annotation.prototype = { 13 | retain : false, 14 | priority : sequence.ANNOTATION, 15 | name : '@annotation', 16 | 17 | _value: undefined, 18 | 19 | setParameter: function (value) { 20 | this._value = value; 21 | }, 22 | 23 | getParameter: function () { 24 | return this._value; 25 | }, 26 | 27 | process: function (subject) { 28 | var name = '@' + this.getParameter(); 29 | 30 | subject.prototype.name = name; 31 | } 32 | 33 | }; 34 | 35 | module.exports = Annotation; 36 | -------------------------------------------------------------------------------- /lib/processor/annotation/Exports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | function Exports () {} 11 | 12 | Exports.prototype = { 13 | retain : false, 14 | priority : sequence.EXPORTS, 15 | name : '@extends', 16 | 17 | _parameter: undefined, 18 | 19 | 20 | setParameter: function (value) { 21 | this._parameter = value; 22 | }, 23 | 24 | getParameter: function () { 25 | return this._parameter; 26 | }, 27 | 28 | process: function (subject /*, proto*/) { 29 | var value = this.getParameter(); 30 | 31 | if (value && typeof value === 'object') { 32 | value.exports = subject; 33 | } 34 | } 35 | }; 36 | 37 | module.exports = Exports; 38 | -------------------------------------------------------------------------------- /lib/processor/annotation/Extends.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | function Extends () {} 11 | 12 | Extends.prototype = { 13 | retain : false, 14 | priority : sequence.EXTENDS, 15 | name : '@extends', 16 | 17 | _parameter: undefined, 18 | 19 | setParameter: function (value) { 20 | if (!(value && value.prototype)) { 21 | throw new Error('@extends parameter should have a prototype'); 22 | } 23 | this._parameter = value; 24 | }, 25 | 26 | getParameter: function () { 27 | return this._parameter; 28 | }, 29 | 30 | process: function (subject) { 31 | var parent = this.getParameter(), 32 | sp; 33 | 34 | subject.prototype = sp = Object.create(parent.prototype); 35 | 36 | sp.callSuper =(function() { 37 | var _stack = [], 38 | _idx = 0, 39 | mthdArgs; 40 | 41 | function _clear(){ 42 | _stack = []; 43 | _idx = 0; 44 | } 45 | 46 | function _createStack(methodName, instance) { 47 | var hasProp = {}.hasOwnProperty, 48 | isCtor = (methodName === 'constructor'), 49 | next = isCtor ? Object.getPrototypeOf(instance) : instance, 50 | mthd; 51 | 52 | while (next) { 53 | if (hasProp.call(next, methodName)) { 54 | mthd = (next[methodName]); 55 | _stack.push(mthd); 56 | } 57 | next = Object.getPrototypeOf(next); 58 | } 59 | } 60 | 61 | return function(methodName){ 62 | var mthd, ret; 63 | 64 | if (_idx === 0) { 65 | mthdArgs = Array.prototype.slice.call(arguments, 1); 66 | _createStack(methodName, this); 67 | } 68 | mthd = _stack[_idx+1]; 69 | 70 | if (!mthd) { 71 | throw new Error('callSuper: There is no method named ' + mthd + ' in parent class.'); 72 | } 73 | 74 | _idx++; 75 | 76 | ret = mthd.apply(this, mthdArgs); 77 | 78 | _clear(); 79 | 80 | return ret; 81 | }; 82 | })(); 83 | 84 | } 85 | 86 | }; 87 | 88 | module.exports = Extends; 89 | -------------------------------------------------------------------------------- /lib/processor/annotation/Merge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | var _STRATEGIES_ = { 11 | 'single' : '_mergeMine', 12 | 'mine' : '_mergeMine', 13 | 'their' : '_mergeTheir', 14 | 'deep-mine' : '_mergeDeepMine', 15 | 'deep-their' : '_mergeDeepTheir', 16 | 'properties' : '_mergeOnlyProperties' 17 | }; 18 | 19 | /** 20 | * @constructor 21 | */ 22 | function Merge (options) { 23 | var useProto; 24 | if (options) { 25 | useProto = options.usePrototypeWhenSubjectIsClass; 26 | this._usePrototypeWhenSubjectIsClass = (useProto === false) ? useProto : true; 27 | } 28 | } 29 | 30 | Merge.prototype = { 31 | retain : false, 32 | priority : sequence.MERGE, 33 | name : '@merge', 34 | 35 | _parameter : undefined, 36 | 37 | _usePrototypeWhenSubjectIsClass: true, 38 | 39 | 40 | setParameter: function (value) { 41 | this._parameter = value; 42 | }, 43 | 44 | getParameter: function () { 45 | return this._parameter; 46 | }, 47 | 48 | _doMerge : function (mine, their, method) { 49 | var key; 50 | 51 | for (key in their) { 52 | if (their.hasOwnProperty(key)) { 53 | method.call(this, key); 54 | } 55 | } 56 | }, 57 | 58 | /** 59 | * mine merge strategy: mine params over their. If params is already defined it gets overriden. 60 | */ 61 | _mergeMine : function (mine, their) { 62 | this._doMerge(mine, their, function(k){ 63 | mine[k] = their[k]; 64 | }); 65 | 66 | return mine; 67 | }, 68 | 69 | _mergeOnlyProperties : function (mine, their) { 70 | this._doMerge(mine, their, function(k){ 71 | if (typeof their[k] !== 'function'){ 72 | mine[k] = their[k]; 73 | } 74 | }); 75 | 76 | return mine; 77 | }, 78 | 79 | /** 80 | * deepMine merge strategy: mine params over their. 81 | * If params is already defined and it is an object it is merged with strategy mine, 82 | * if params is already defined and it is an array it is concatenated, 83 | * otherwise it gets overriden with mine. 84 | */ 85 | _mergeDeepMine : function (mine, their) { 86 | return this._mergeDeep(mine, their, '_mergeMine'); 87 | }, 88 | 89 | /** 90 | * their merge strategy: their params over mine. If params is already defined it doesn't get overriden. 91 | */ 92 | _mergeTheir : function (mine, their) { 93 | this._doMerge(mine, their, function(k){ 94 | if (mine[k] === undefined) { 95 | mine[k] = their[k]; 96 | } 97 | }); 98 | 99 | return mine; 100 | }, 101 | 102 | 103 | /** 104 | * deepMine merge strategy: their params over mine. 105 | * If params is already defined and it is an object it is merged with strategy their, 106 | * if params is already defined and it is an array it is concatenated, 107 | * otherwise it gets overriden with mine. 108 | */ 109 | _mergeDeepTheir : function (mine, their) { 110 | return this._mergeDeep(mine, their, '_mergeTheir'); 111 | }, 112 | 113 | /** 114 | * runs the deep merge using the given strategy 115 | */ 116 | _mergeDeep: function (mine, their, strategy) { 117 | this._doMerge(mine, their, function(key){ 118 | if (typeof their[key] === 'object') { 119 | if (their[key] instanceof Array) { 120 | mine[key] = [].concat(mine[key], their[key]); 121 | } else { 122 | mine[key] = this[strategy](mine[key], their[key]); 123 | } 124 | } else if (mine[key] === undefined ) { 125 | mine[key] = their[key]; 126 | } 127 | }); 128 | 129 | return mine; 130 | }, 131 | 132 | _shouldUsePrototypeWhenSubjectIsClass: function () { 133 | return this._usePrototypeWhenSubjectIsClass; 134 | }, 135 | 136 | process: function (subject, options) { 137 | var their = options, 138 | useProto = this._shouldUsePrototypeWhenSubjectIsClass(), 139 | mine = (useProto && subject.prototype) || subject, 140 | strategy = _STRATEGIES_[this.getParameter()]; 141 | 142 | this[strategy](mine, their); 143 | } 144 | }; 145 | 146 | module.exports = Merge; 147 | -------------------------------------------------------------------------------- /lib/processor/annotation/Properties.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | function Properties () {} 11 | 12 | Properties.prototype = { 13 | retain : false, 14 | priority : sequence.PROPERTIES, 15 | name : '@properties', 16 | 17 | _parameter: undefined, 18 | 19 | setParameter: function (value) { 20 | if (Object.prototype.toString.call(value) !== '[object Object]') { 21 | throw new Error('@properties parameter should be an Object'); 22 | } 23 | this._parameter = value; 24 | }, 25 | 26 | getParameter: function () { 27 | return this._parameter; 28 | }, 29 | 30 | _capitalizeName: function (name) { 31 | return (name.charAt(0).toUpperCase() + name.slice(1)); 32 | }, 33 | 34 | _getterName: function (property, value) { 35 | return (value !== false && value !== true ? 'get' : 'is') + this._capitalizeName(property); 36 | }, 37 | 38 | _setterName: function (property) { 39 | return 'set' + this._capitalizeName(property); 40 | }, 41 | 42 | _createPropertyFor: function (subject, name, value, doNotOverride) { 43 | 44 | if (typeof subject[name] === 'undefined' || doNotOverride !== true) { 45 | subject[name] = value; 46 | } 47 | subject[this._getterName(name, value)] = function(){ 48 | return this[name]; 49 | }; 50 | subject[this._setterName(name)] = function(set){ 51 | this[name] = set; 52 | }; 53 | }, 54 | 55 | process: function (subject) { 56 | var properties = this.getParameter(), 57 | isObject = !(subject.prototype), 58 | key; 59 | 60 | for (key in properties) { 61 | if (properties.hasOwnProperty(key)) { 62 | this._createPropertyFor(subject.prototype || subject, key, properties[key], isObject); 63 | } 64 | } 65 | } 66 | 67 | }; 68 | 69 | module.exports = Properties; 70 | -------------------------------------------------------------------------------- /lib/processor/annotation/Requires.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | 10 | function $$required$$ () { 11 | throw new Error('method is marked as required but it has not been defined'); 12 | } 13 | 14 | function Requires () {} 15 | 16 | Requires.requiredMethod = $$required$$; 17 | 18 | Requires.prototype = { 19 | retain : false, 20 | priority : sequence.REQUIRES, 21 | name : '@requires', 22 | 23 | _parameter: [], 24 | 25 | setParameter: function (value) { 26 | //TODO: validate parameter 27 | this._parameter = [].concat(value); 28 | }, 29 | 30 | getParameter: function () { 31 | return this._parameter; 32 | }, 33 | 34 | process: function (subject) { 35 | var reqs = this.getParameter(), // always an [] 36 | l = reqs.length, 37 | i; 38 | 39 | for (i = 0; i < l; i++) { 40 | this._createRequiredMethod(subject, reqs[i]); 41 | } 42 | }, 43 | 44 | _createRequiredMethod: function(sub, methodName){ 45 | var subject = (sub.prototype || sub); 46 | 47 | if (!subject[methodName]) { 48 | subject[methodName] = Requires.requiredMethod; 49 | } 50 | 51 | } 52 | 53 | }; 54 | 55 | module.exports = Requires; 56 | -------------------------------------------------------------------------------- /lib/processor/annotation/Static.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | var Merge = require('./Merge'); 10 | 11 | function Static () {} 12 | 13 | Static.prototype = { 14 | retain : false, 15 | priority : sequence.POST_MERGE, 16 | name : '@static', 17 | 18 | _parameter: undefined, 19 | 20 | setParameter: function (value) { 21 | if (Object.prototype.toString.call(value) !== '[object Object]') { 22 | throw new Error('@static parameter should be an Object'); 23 | } 24 | this._parameter = value; 25 | }, 26 | 27 | getParameter: function () { 28 | return this._parameter; 29 | }, 30 | 31 | process: function (subject) { 32 | var statics = this.getParameter(), 33 | merger = new Merge({usePrototypeWhenSubjectIsClass: false}); 34 | 35 | merger.setParameter('mine'); 36 | 37 | merger.process(subject, statics); 38 | 39 | } 40 | 41 | }; 42 | 43 | module.exports = Static; 44 | -------------------------------------------------------------------------------- /lib/processor/annotation/Talents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var Traits = require('./Traits'); 9 | 10 | function Talents () { 11 | Traits.call(this); 12 | } 13 | 14 | Talents.prototype = Object.create(Traits.prototype); 15 | 16 | Talents.prototype._configName = 'talent'; 17 | 18 | Talents.prototype.process = function (subject) { 19 | var talents = this.getParameter(), // always an [] 20 | l = talents.length, 21 | i; 22 | 23 | for (i = 0; i < l; i++) { 24 | this._applyTo(subject, talents[i]); 25 | } 26 | }; 27 | 28 | module.exports = Talents; 29 | -------------------------------------------------------------------------------- /lib/processor/annotation/Traits.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var sequence = require('../sequence'); 9 | var Requires = require('./Requires'); 10 | 11 | function Traits () {} 12 | 13 | Traits.prototype = { 14 | retain : false, 15 | priority : sequence.TRAITS, 16 | name : '@traits', 17 | 18 | _configName: 'trait', 19 | 20 | _parameter: [], 21 | 22 | setParameter: function (value) { 23 | //TODO: validate parameter 24 | this._parameter = [].concat(value); 25 | }, 26 | 27 | getParameter: function () { 28 | return this._parameter; 29 | }, 30 | 31 | process: function (subject) { 32 | var traits = this.getParameter(), // always an [] 33 | l = traits.length, 34 | i; 35 | 36 | for (i = 0; i < l; i++) { 37 | this._applyTo(subject.prototype || subject, traits[i]); 38 | } 39 | }, 40 | 41 | _isApplicable: function (option) { 42 | var type = this._configName; 43 | return (typeof option === 'function') || (option && !option[type]); 44 | }, 45 | 46 | _applyTo: function (subject, options) { 47 | var me = this, 48 | type = me._configName, 49 | o = {}, 50 | tp, excluded, aliases, t; 51 | 52 | if (me._isApplicable(options)) { 53 | o[type] = options; 54 | me._applyTo(subject, o); 55 | return; 56 | } 57 | 58 | excluded = [].concat(options.excludes); 59 | aliases = options.alias || {}; 60 | t = options[type]; 61 | tp = t.prototype || t; 62 | 63 | Object.getOwnPropertyNames(tp) 64 | .filter(function(key){ 65 | return !key.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/); 66 | }) 67 | .forEach(function(key){ 68 | me._raiseErrorIfItIsState(key, tp); 69 | me._applyIfNotExcluded(key, excluded, aliases, subject, tp); 70 | }); 71 | }, 72 | 73 | _applyIfNotExcluded: function (key, excluded, aliases, subject, tp) { 74 | var alias; 75 | if (excluded.indexOf(key) === -1) { 76 | alias = aliases[key] || key; 77 | 78 | this._raiseErrorIfConflict(alias, subject, tp); 79 | 80 | if (!subject[alias] || subject[alias] === Requires.requiredMethod) { 81 | Object.defineProperty(subject, alias, Object.getOwnPropertyDescriptor(tp, key)); 82 | } 83 | } 84 | }, 85 | 86 | _raiseErrorIfItIsState: function (key, traitProto) { 87 | if (typeof traitProto[key] !== 'function') { 88 | throw new Error('Trait MUST NOT contain any state. Found: ' + key + ' as state while processing trait'); 89 | } 90 | }, 91 | 92 | _raiseErrorIfConflict: function (methodName, subjectProto, traitProto) { 93 | var requiredMethodName = Requires.requiredMethod.name, 94 | subjectMethod = subjectProto[methodName], 95 | traitMethod = traitProto[methodName], 96 | sameMethodName = (subjectMethod && traitMethod), 97 | methodsAreNotTheSame = sameMethodName && (subjectMethod.toString() !== traitMethod.toString()), 98 | traitMethodIsNotARequired = sameMethodName && (traitMethod.name !== requiredMethodName), 99 | subjecMethodIsNotARequired = sameMethodName && (subjectMethod.name !== requiredMethodName); 100 | 101 | 102 | if (sameMethodName && methodsAreNotTheSame && traitMethodIsNotARequired && subjecMethodIsNotARequired) { 103 | throw new Error('Same method named: ' + methodName + ' is defined in trait and Class.' ); 104 | } 105 | } 106 | }; 107 | 108 | module.exports = Traits; 109 | -------------------------------------------------------------------------------- /lib/processor/defaults.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | module.exports = { 9 | 'no-op' : require('./NoOp'), 10 | '@as' : undefined, /*pseudo-processor*/ 11 | '@merge' : require('./annotation/Merge'), 12 | '@extends' : require('./annotation/Extends'), 13 | '@properties' : require('./annotation/Properties'), 14 | '@traits' : require('./annotation/Traits'), 15 | '@requires' : require('./annotation/Requires'), 16 | '@talents' : require('./annotation/Talents'), 17 | '@annotation' : require('./annotation/Annotation'), 18 | '@exports' : require('./annotation/Exports'), 19 | '@static' : require('./annotation/Static') 20 | }; 21 | -------------------------------------------------------------------------------- /lib/processor/sequence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2016 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | /** 8 | * sequence list 9 | The processors will use one of the defined priorities in this list 10 | 11 | The priorities are organized in groups: 12 | 13 | [-1] No Op. 14 | [1..99) Object/Class creation. 15 | [99..999) Merge - Traits and talents are considered merge stage since we copy the structure into the subject. 16 | [999..) Miscelaneous - Annotation definition makes no changes over the Subject itself. 17 | 18 | */ 19 | 20 | module.exports = { 21 | NO_OP : -1, 22 | 23 | PRE_EXTENDS : 9, 24 | EXTENDS : 10, 25 | POST_EXTENDS : 11, 26 | 27 | PRE_PROPERTIES : 19, 28 | PROPERTIES : 20, 29 | POST_PROPERTIES : 21, 30 | 31 | PRE_REQUIRES : 29, 32 | REQUIRES : 30, 33 | POST_REQUIRES : 31, 34 | 35 | PRE_MERGE : 99, 36 | MERGE : 100, 37 | POST_MERGE : 101, 38 | 39 | PRE_TRAITS : 109, 40 | TRAITS : 110, 41 | POST_TRAITS : 111, 42 | 43 | PRE_ANNOTATION : 999, 44 | ANNOTATION : 1000, 45 | POST_ANNOTATION : 1001, 46 | 47 | PRE_EXPORTS : 1009, 48 | EXPORTS : 1010, 49 | POST_EXPORTS : 1011 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cocktail", 3 | "description": "CocktailJS is a small library to explore traits, talents, inheritance and annotations concepts in nodejs - Shake your objects and classes with Cocktail!", 4 | "version": "0.7.3", 5 | "homepage": "http://cocktailjs.github.io", 6 | "author": { 7 | "name": "Maximiliano Fierro", 8 | "email": "elmasse@gmail.com" 9 | }, 10 | "licenses": [ 11 | { 12 | "type": "MIT", 13 | "url": "https://github.com/CocktailJS/cocktail/blob/master/LICENSE-MIT" 14 | } 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/CocktailJS/cocktail.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/CocktailJS/cocktail/issues" 22 | }, 23 | "main": "lib/cocktail", 24 | "engines": { 25 | "node": ">= 0.10.0" 26 | }, 27 | "scripts": { 28 | "test": "npm run lint && npm run unit && npm run integration", 29 | "unit": "mocha --recursive test/unit/", 30 | "integration": "mocha --recursive test/integration/", 31 | "lint": "eslint --quiet lib/ test/", 32 | "coverage": "istanbul cover _mocha -- -u exports --recursive test" 33 | }, 34 | "devDependencies": { 35 | "chai": "~3.5.0", 36 | "eslint": "^2.11.1", 37 | "istanbul": "^0.4.3", 38 | "mocha": "~2.5.3", 39 | "sinon": "~1.17.4", 40 | "sinon-chai": "~2.8" 41 | }, 42 | "keywords": [ 43 | "oop", 44 | "extends", 45 | "annotations", 46 | "traits", 47 | "talents", 48 | "properties", 49 | "mix", 50 | "class", 51 | "inheritance" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | node: true 4 | mocha: true 5 | 6 | rules: 7 | 8 | no-shadow: 0 9 | no-unused-expressions: 0 10 | -------------------------------------------------------------------------------- /test/helper/RestoreProcessors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2013 - 2015 Maximiliano Fierro 4 | * Licensed under the MIT license. 5 | */ 6 | 'use strict'; 7 | 8 | var cocktail = require('../../lib/cocktail'); 9 | 10 | /** 11 | * @Talent 12 | * @Trait 13 | * RestoreProcessors is a talent to provide cocktail a way to restore the modified processors list 14 | * back to the default configuration. 15 | * We separate this behavior since it might be used ONLY for TESTS. 16 | */ 17 | cocktail.mix({ 18 | '@exports': module, 19 | 20 | '@requires': [ 21 | 'setProcessors', 22 | 'getDefaultProcessors' 23 | ], 24 | 25 | /** 26 | * @method restoreDefaultProcessors 27 | * Wipes out the current processor list and restore back the default one. 28 | */ 29 | restoreDefaultProcessors: function () { 30 | var key, 31 | defaultProcessors = this.getDefaultProcessors(), 32 | processors = {}; 33 | 34 | for (key in defaultProcessors) { 35 | if (defaultProcessors.hasOwnProperty(key)) { 36 | processors[key] = defaultProcessors[key]; 37 | } 38 | } 39 | 40 | this.setProcessors(processors); 41 | }, 42 | 43 | /** 44 | * @method clearProcessors 45 | * Removes all the processors but NO-OP to create a state of a clear instance for tests purposes. 46 | */ 47 | clearProcessors: function() { 48 | var processors = this.getProcessors(), 49 | key; 50 | for (key in processors) { 51 | if (processors.hasOwnProperty(key) && key !== 'no-op') { 52 | delete processors[key]; 53 | } 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /test/integration/cocktail-annotations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinon = require('sinon'), 5 | sinonChai = require('sinon-chai'), 6 | cocktail = require('../../lib/cocktail'), 7 | RestoreProcessors = require('../helper/RestoreProcessors'); 8 | 9 | var expect = chai.expect; 10 | chai.use(sinonChai); 11 | 12 | cocktail.mix(cocktail, { 13 | '@talents': [RestoreProcessors] 14 | }); 15 | 16 | describe('cocktail Integration Test: annotations', function(){ 17 | beforeEach(function(){ 18 | cocktail.restoreDefaultProcessors(); 19 | }); 20 | 21 | afterEach(function(){ 22 | cocktail.restoreDefaultProcessors(); 23 | }); 24 | 25 | describe('`@annotation` annotation and cocktail.use() to register a custom annotation', function(){ 26 | var Custom = function(){}, 27 | Subject, 28 | aProcess = sinon.spy(), 29 | aSetter = sinon.spy(); 30 | 31 | beforeEach(function(){ 32 | Custom = function(){}; 33 | Subject = function(){}; 34 | }); 35 | 36 | it('adds custom annotation definition using cocktail.use()', function(){ 37 | var annotationName = 'custom', 38 | customValue = 1; 39 | 40 | cocktail.mix(Custom, { 41 | '@annotation': annotationName, 42 | 43 | setParameter: aSetter, 44 | process: aProcess 45 | }); 46 | 47 | expect(Custom.prototype).to.have.property('name'); 48 | expect(Custom.prototype.name).to.be.equal('@' + annotationName); 49 | 50 | cocktail.use(Custom); 51 | 52 | cocktail.mix(Subject, { 53 | '@custom': customValue 54 | }); 55 | 56 | expect(aSetter).to.have.been.calledWith(customValue); 57 | expect(aProcess).to.have.been.calledWith(Subject); 58 | 59 | }); 60 | 61 | it('if annotation has retain=true the annotation property is retained', function(){ 62 | var annotationName = 'custom-with-retain', 63 | customValue = 1; 64 | 65 | cocktail.mix(Custom, { 66 | '@annotation': annotationName, 67 | retain: true, 68 | setParameter: aSetter, 69 | process: aProcess 70 | }); 71 | 72 | expect(Custom.prototype).to.have.property('name'); 73 | expect(Custom.prototype.name).to.be.equal('@' + annotationName); 74 | 75 | cocktail.use(Custom); 76 | 77 | cocktail.mix(Subject, { 78 | '@custom-with-retain': customValue 79 | }); 80 | 81 | expect(aSetter).to.have.been.calledWith(customValue); 82 | expect(aProcess).to.have.been.calledWith(Subject); 83 | expect(Subject.prototype).to.have.property('@' + annotationName); 84 | }); 85 | 86 | it('if annotation already exists it gets overriden by the current definition as a custom annotation', function(){ 87 | var customValue = 1; 88 | 89 | cocktail.mix(Custom, { 90 | '@annotation': 'traits', 91 | 92 | setParameter: aSetter, 93 | process: aProcess 94 | }); 95 | 96 | cocktail.use(Custom); 97 | 98 | cocktail.mix(Subject, { 99 | '@traits': customValue 100 | }); 101 | 102 | expect(aSetter).to.have.been.calledWith(customValue); 103 | expect(aProcess).to.have.been.calledWith(Subject); 104 | 105 | }); 106 | 107 | it('if the annotation process invokes mix() the processor queue should be independent.', function(){ 108 | var customValue = 1; 109 | 110 | cocktail.mix(Custom, { 111 | '@annotation': 'custom', 112 | 113 | setParameter: aSetter, 114 | process: function(){ 115 | var proto = {}; 116 | 117 | cocktail.mix(proto, { 118 | '@merge': 'mine', 119 | a: 2 120 | }); 121 | } 122 | }); 123 | 124 | cocktail.use(Custom); 125 | 126 | cocktail.mix(Subject, { 127 | '@custom': customValue 128 | }); 129 | 130 | expect(aSetter).to.have.been.calledWith(customValue); 131 | }); 132 | 133 | }); 134 | 135 | }); 136 | -------------------------------------------------------------------------------- /test/integration/cocktail-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../lib/cocktail'), 5 | RestoreProcessors = require('../helper/RestoreProcessors'); 6 | 7 | var expect = chai.expect; 8 | 9 | cocktail.mix(cocktail, { 10 | '@talents': [RestoreProcessors] 11 | }); 12 | 13 | describe('cocktail Integration Test: exports', function(){ 14 | beforeEach(function(){ 15 | cocktail.restoreDefaultProcessors(); 16 | }); 17 | 18 | afterEach(function(){ 19 | cocktail.restoreDefaultProcessors(); 20 | }); 21 | 22 | describe('`@exports` annotation registers the current mix as the specified value', function(){ 23 | var Custom = function(){}; 24 | 25 | it('exports the current mix in the specified value', function() { 26 | var module = { 27 | exports: undefined 28 | }; 29 | 30 | cocktail.mix(Custom, { 31 | '@exports': module, 32 | 33 | some: 'a', 34 | 35 | aMethod: function(){} 36 | }); 37 | 38 | expect(module.exports).to.not.be.an('undefined'); 39 | expect(module.exports).to.respondTo('aMethod'); 40 | }); 41 | 42 | it('exports the current class definition in the specified value', function() { 43 | var module = { 44 | exports: undefined 45 | }; 46 | 47 | cocktail.mix({ 48 | '@exports': module, 49 | 50 | constructor: function() {/*Body*/}, 51 | 52 | some: 'a', 53 | 54 | aMethod: function(){/*Body*/} 55 | }); 56 | 57 | expect(module.exports).to.not.be.an('undefined'); 58 | expect(module.exports).to.respondTo('aMethod'); 59 | }); 60 | 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/integration/cocktail-extends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinon = require('sinon'), 5 | sinonChai = require('sinon-chai'), 6 | cocktail = require('../../lib/cocktail'), 7 | RestoreProcessors = require('../helper/RestoreProcessors'); 8 | 9 | var expect = chai.expect; 10 | chai.use(sinonChai); 11 | 12 | cocktail.mix(cocktail, { 13 | '@talents': [RestoreProcessors] 14 | }); 15 | 16 | describe('cocktail Integration Test: extends', function(){ 17 | 18 | beforeEach(function(){ 19 | cocktail.restoreDefaultProcessors(); 20 | }); 21 | 22 | afterEach(function(){ 23 | cocktail.restoreDefaultProcessors(); 24 | }); 25 | 26 | describe('`@extends` annotation makes the subject to inherit from the given base class ', function(){ 27 | var Base = function(){}, 28 | aMethod = sinon.spy(), 29 | ClassA, 30 | instanceA; 31 | 32 | Base.prototype.aMethod = aMethod; 33 | Base.prototype.aProperty = 1; 34 | 35 | beforeEach(function(){ 36 | ClassA = function(){}; 37 | instanceA = undefined; 38 | }); 39 | 40 | it('makes methods and properties from base available on the given subject', function(){ 41 | cocktail.mix(ClassA, { 42 | '@extends': Base 43 | }); 44 | 45 | expect(ClassA).to.respondTo('aMethod'); 46 | expect(ClassA.prototype).to.have.property('aProperty').that.equal(1); 47 | 48 | instanceA = new ClassA(); 49 | 50 | expect(instanceA).to.be.an.instanceOf(ClassA); 51 | expect(instanceA).to.be.an.instanceOf(Base); 52 | }); 53 | 54 | it('methods and properties can be overriden in the subject', function(){ 55 | cocktail.mix(ClassA, { 56 | '@extends': Base, 57 | 58 | aMethod: function(){} 59 | 60 | }); 61 | 62 | expect(ClassA).to.respondTo('aMethod'); 63 | expect(ClassA.prototype.aMethod).to.not.be.equal(aMethod); 64 | 65 | instanceA = new ClassA(); 66 | 67 | instanceA.aMethod(1); 68 | 69 | expect(aMethod).to.not.have.been.calledWith(1); 70 | }); 71 | 72 | it('throws an error if it is not called with a Class to extend from', function(){ 73 | var anObject = {}; 74 | 75 | expect(function(){ 76 | cocktail.mix(ClassA, { 77 | '@extends': anObject 78 | }); 79 | }).to.throw(Error); 80 | 81 | }); 82 | 83 | it('adds a `callSuper` method so an overriden method can be called', function(){ 84 | cocktail.mix(ClassA, { 85 | '@extends': Base, 86 | 87 | aMethod: function(param){ 88 | this.callSuper('aMethod', param); 89 | } 90 | 91 | }); 92 | 93 | expect(ClassA).to.respondTo('aMethod'); 94 | expect(ClassA.prototype.aMethod).to.not.be.equal(aMethod); 95 | 96 | instanceA = new ClassA(); 97 | 98 | instanceA.aMethod(1); 99 | 100 | expect(aMethod).to.have.been.calledWith(1); 101 | }); 102 | 103 | it('`callSuper` method in the constructor calls parent constructor', function(){ 104 | var ClassA, ClassB, 105 | constructorBase = sinon.spy(), 106 | param = 1; 107 | 108 | ClassA = function (param) { 109 | constructorBase(param); 110 | }; 111 | 112 | ClassB = cocktail.mix({ 113 | '@extends': ClassA, 114 | 115 | constructor: function(param){ 116 | this.callSuper('constructor', param); 117 | } 118 | }); 119 | 120 | instanceA = new ClassB(param); 121 | expect(constructorBase).to.have.been.calledOnce; 122 | expect(constructorBase).to.have.been.calledWith(param); 123 | }); 124 | 125 | 126 | it('`callSuper` method with multiple inheritance level should call parent methods in order', function () { 127 | var ClassA, ClassB, ClassC, 128 | fooA, fooB, fooC, 129 | bazA, bazB, 130 | barB, barInstance, 131 | instance; 132 | 133 | fooA = sinon.spy(); 134 | fooB = sinon.spy(); 135 | fooC = sinon.spy(); 136 | 137 | bazA = sinon.spy(); 138 | bazB = sinon.spy(); 139 | 140 | barInstance = sinon.spy(); 141 | barB = sinon.spy(); 142 | 143 | ClassA = cocktail.mix({ 144 | 145 | constructor: function() { 146 | }, 147 | 148 | foo: function() { 149 | fooA(); 150 | }, 151 | 152 | baz: function() { 153 | bazA(); 154 | } 155 | }); 156 | 157 | ClassB = cocktail.mix({ 158 | '@extends': ClassA, 159 | 160 | foo: function() { 161 | this.callSuper('foo'); 162 | fooB(); 163 | }, 164 | 165 | baz: function () { 166 | this.callSuper('baz'); 167 | bazB(); 168 | }, 169 | 170 | bar: function () { 171 | barB(); 172 | } 173 | }); 174 | 175 | ClassC = cocktail.mix({ 176 | '@extends': ClassB, 177 | 178 | foo: function() { 179 | this.callSuper('foo'); 180 | fooC(); 181 | } 182 | }); 183 | 184 | instance = new ClassC(); 185 | 186 | instance.foo(); 187 | 188 | expect(fooA).to.have.been.calledOnce; 189 | expect(fooB).to.have.been.calledOnce; 190 | expect(fooC).to.have.been.calledOnce; 191 | 192 | instance.baz(); 193 | 194 | expect(bazA).to.have.been.calledOnce; 195 | expect(bazB).to.have.been.calledOnce; 196 | 197 | instance.bar = function() { 198 | this.callSuper('bar'); 199 | barInstance(); 200 | } 201 | 202 | 203 | instance.bar(); 204 | 205 | expect(barB).to.have.been.calledOnce; 206 | expect(barInstance).to.have.been.calledOnce; 207 | 208 | 209 | }); 210 | 211 | 212 | it('`callSuper` method with multiple inheritance level should call parent constructors in order', function () { 213 | var ClassA, ClassB, ClassC, 214 | constructorA, constructorB, constructorC; 215 | 216 | constructorA = sinon.spy(); 217 | constructorB = sinon.spy(); 218 | constructorC = sinon.spy(); 219 | 220 | ClassA = cocktail.mix({ 221 | 222 | constructor: function() { 223 | constructorA(); 224 | } 225 | }); 226 | 227 | ClassB = cocktail.mix({ 228 | '@extends': ClassA, 229 | 230 | constructor: function() { 231 | this.callSuper('constructor'); 232 | constructorB(); 233 | } 234 | }); 235 | 236 | ClassC = cocktail.mix({ 237 | '@extends': ClassB, 238 | 239 | constructor: function() { 240 | this.callSuper('constructor'); 241 | constructorC(); 242 | } 243 | }); 244 | 245 | new ClassC(); 246 | 247 | expect(constructorA).to.have.been.calledOnce; 248 | expect(constructorB).to.have.been.calledOnce; 249 | expect(constructorC).to.have.been.calledOnce; 250 | 251 | }); 252 | 253 | it('makes methods and properties from base available on the given subject when subject is a class definition', function(){ 254 | ClassA = cocktail.mix({ 255 | '@extends': Base 256 | }); 257 | 258 | expect(ClassA).to.respondTo('aMethod'); 259 | expect(ClassA.prototype).to.have.property('aProperty').that.equal(1); 260 | 261 | instanceA = new ClassA(); 262 | 263 | expect(instanceA).to.be.an.instanceOf(ClassA); 264 | expect(instanceA).to.be.an.instanceOf(Base); 265 | }); 266 | 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /test/integration/cocktail-merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../lib/cocktail'), 5 | RestoreProcessors = require('../helper/RestoreProcessors'); 6 | 7 | var expect = chai.expect; 8 | 9 | cocktail.mix(cocktail, { 10 | '@talents': [RestoreProcessors] 11 | }); 12 | 13 | describe('cocktail Integration Test: merge', function(){ 14 | 15 | beforeEach(function(){ 16 | cocktail.restoreDefaultProcessors(); 17 | }); 18 | 19 | afterEach(function(){ 20 | cocktail.restoreDefaultProcessors(); 21 | }); 22 | 23 | describe('`@merge` annotation adds the properties and methods in the options to the given subject', function(){ 24 | var ClassA, 25 | ClassAA, 26 | anObject, 27 | aMethod = function aMethod(){}, 28 | aProperty = 1; 29 | 30 | beforeEach(function(){ 31 | ClassA = function(){}; 32 | ClassAA = function(){}; 33 | anObject = {}; 34 | }); 35 | 36 | it('merges the properties and methods into the subject prototype when subject is a class', function(){ 37 | cocktail.mix(ClassA, { 38 | method: aMethod, 39 | property: aProperty 40 | }); 41 | 42 | expect(ClassA).to.respondTo('method'); 43 | expect(ClassA.prototype).to.have.property('property').that.equal(aProperty); 44 | }); 45 | 46 | it('merges the properties and methods into the subject when subject is an object', function(){ 47 | cocktail.mix(anObject, { 48 | method: aMethod, 49 | property: aProperty 50 | }); 51 | 52 | expect(anObject).to.respondTo('method'); 53 | expect(anObject).to.have.property('property').that.equal(aProperty); 54 | }); 55 | 56 | it('a `@merge` annotation is added by default if it is no specified', function(){ 57 | cocktail.mix(ClassA, { 58 | method: aMethod, 59 | property: aProperty 60 | }); 61 | 62 | cocktail.mix(ClassAA, { 63 | '@merge': 'single', 64 | method: aMethod, 65 | property: aProperty 66 | }); 67 | 68 | expect(ClassA).to.respondTo('method'); 69 | expect(ClassA.prototype).to.have.property('property').that.equal(aProperty); 70 | 71 | expect(ClassAA).to.respondTo('method'); 72 | expect(ClassAA.prototype).to.have.property('property').that.equal(aProperty); 73 | 74 | }); 75 | 76 | it('overrides properties/methods defined in the subject when `@merge` is `single`', function(){ 77 | ClassA.prototype.oValue = 1; 78 | ClassA.prototype.oMethod = function o(){}; 79 | 80 | cocktail.mix(ClassA, { 81 | //'@merge': 'single', //default value 82 | oValue : aProperty, 83 | oMethod : aMethod 84 | }); 85 | 86 | expect(ClassA).to.respondTo('oMethod'); 87 | expect(ClassA.prototype).to.have.property('oValue').that.equal(aProperty); 88 | expect(ClassA.prototype.oMethod).to.be.equal(aMethod); 89 | }); 90 | 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/integration/cocktail-properties.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinonChai = require('sinon-chai'), 5 | cocktail = require('../../lib/cocktail'), 6 | RestoreProcessors = require('../helper/RestoreProcessors'); 7 | 8 | var expect = chai.expect; 9 | chai.use(sinonChai); 10 | 11 | cocktail.mix(cocktail, { 12 | '@talents': [RestoreProcessors] 13 | }); 14 | 15 | describe('cocktail Integration Test: properties', function(){ 16 | 17 | beforeEach(function(){ 18 | cocktail.restoreDefaultProcessors(); 19 | }); 20 | 21 | afterEach(function(){ 22 | cocktail.restoreDefaultProcessors(); 23 | }); 24 | 25 | describe('`@properties` annotation adds getter and setter for the specified properties to the given subject ', function(){ 26 | var ClassA, 27 | anObject, 28 | instanceA; 29 | 30 | beforeEach(function(){ 31 | ClassA = function(){}; 32 | anObject = {}; 33 | instanceA = undefined; 34 | }); 35 | 36 | it('adds a getter/setter method and set the value for each property to the subject prototype when subject is a class', function(){ 37 | cocktail.mix(ClassA, { 38 | '@properties': { 39 | value: 1, 40 | name : 'name' 41 | } 42 | }); 43 | 44 | expect(ClassA).to.respondTo('setName'); 45 | expect(ClassA).to.respondTo('getName'); 46 | expect(ClassA).to.respondTo('setValue'); 47 | expect(ClassA).to.respondTo('getValue'); 48 | expect(ClassA.prototype).to.have.property('value').that.equal(1); 49 | expect(ClassA.prototype).to.have.property('name').that.equal('name'); 50 | 51 | instanceA = new ClassA(); 52 | 53 | expect(instanceA.getValue()).to.be.equal(1); 54 | expect(instanceA.getName()).to.be.equal('name'); 55 | 56 | }); 57 | 58 | it('adds a getter/setter method and set the value for each property to the subject when subject is an object', function(){ 59 | cocktail.mix(anObject, { 60 | '@properties': { 61 | value: 1, 62 | name : 'name' 63 | } 64 | }); 65 | 66 | expect(anObject).to.respondTo('setName'); 67 | expect(anObject).to.respondTo('getName'); 68 | expect(anObject).to.respondTo('setValue'); 69 | expect(anObject).to.respondTo('getValue'); 70 | expect(anObject).to.have.property('value').that.equal(1); 71 | expect(anObject).to.have.property('name').that.equal('name'); 72 | 73 | }); 74 | 75 | it('adds a getter/setter method but does not override value if property is already defined in the given object', function(){ 76 | anObject.name = 'Defined'; 77 | 78 | cocktail.mix(anObject, { 79 | '@properties': { 80 | value: 1, 81 | name : 'name' 82 | } 83 | }); 84 | 85 | expect(anObject).to.respondTo('setName'); 86 | expect(anObject).to.respondTo('getName'); 87 | expect(anObject).to.respondTo('setValue'); 88 | expect(anObject).to.respondTo('getValue'); 89 | expect(anObject).to.have.property('value').that.equal(1); 90 | expect(anObject).to.have.property('name').that.equal('Defined'); 91 | }); 92 | 93 | 94 | it('if the property is a boolean a isXXX() method is created instead of the getter', function(){ 95 | cocktail.mix(ClassA, { 96 | '@properties': { 97 | valid: false 98 | } 99 | }); 100 | 101 | expect(ClassA).to.respondTo('setValid'); 102 | expect(ClassA).to.respondTo('isValid'); 103 | expect(ClassA).to.not.respondTo('getValid'); 104 | expect(ClassA.prototype).to.have.property('valid').that.equal(false); 105 | 106 | instanceA = new ClassA(); 107 | 108 | expect(instanceA.isValid()).to.be.equal(false); 109 | 110 | }); 111 | 112 | it('adds a getter/setter method and set the value for each property to the subject prototype when subject is a class definition', function(){ 113 | var ClassA; 114 | 115 | ClassA = cocktail.mix({ 116 | 117 | constructor: function(){ 118 | //do smth 119 | }, 120 | 121 | '@properties': { 122 | value: 1, 123 | name : 'name' 124 | } 125 | }); 126 | 127 | expect(ClassA).to.respondTo('setName'); 128 | expect(ClassA).to.respondTo('getName'); 129 | expect(ClassA).to.respondTo('setValue'); 130 | expect(ClassA).to.respondTo('getValue'); 131 | expect(ClassA.prototype).to.have.property('value').that.equal(1); 132 | expect(ClassA.prototype).to.have.property('name').that.equal('name'); 133 | 134 | instanceA = new ClassA(); 135 | 136 | expect(instanceA.getValue()).to.be.equal(1); 137 | expect(instanceA.getName()).to.be.equal('name'); 138 | 139 | }); 140 | 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/integration/cocktail-single-arg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../lib/cocktail'), 5 | RestoreProcessors = require('../helper/RestoreProcessors'); 6 | 7 | var expect = chai.expect; 8 | 9 | cocktail.mix(cocktail, { 10 | '@talents': [RestoreProcessors] 11 | }); 12 | 13 | describe('cocktail Integration Test: single argument class definition', function(){ 14 | 15 | beforeEach(function(){ 16 | cocktail.restoreDefaultProcessors(); 17 | }); 18 | 19 | afterEach(function(){ 20 | cocktail.restoreDefaultProcessors(); 21 | }); 22 | 23 | describe('Single Argument Parameter Class Definition', function(){ 24 | 25 | it('returns a class if one single argument is specified with an @extends annotation.', function(){ 26 | var MyClass = function(){}, 27 | method = function(){}, 28 | value = 'value', 29 | sut; 30 | 31 | MyClass.prototype = { 32 | method: method, 33 | value : value 34 | }; 35 | 36 | sut = cocktail.mix({ 37 | '@extends': MyClass 38 | }); 39 | 40 | expect(sut).to.be.a('function'); 41 | expect(sut).to.respondTo('method'); 42 | expect(sut.prototype.value).to.be.equal(value); 43 | }); 44 | 45 | it('returns a class if one single argument is specified with a @traits annotation.', function(){ 46 | var MyTrait = function(){}, 47 | sut; 48 | 49 | MyTrait.prototype.method = function(){}; 50 | 51 | sut = cocktail.mix({ 52 | '@traits': [MyTrait] 53 | }); 54 | 55 | expect(sut).to.be.a('function'); 56 | 57 | expect(sut).to.respondTo('method'); 58 | 59 | }); 60 | 61 | it('returns a class if one single argument is specified with a @as annotation with a "class" value.', function(){ 62 | var sut; 63 | 64 | sut = cocktail.mix({ 65 | '@as': 'class', 66 | 67 | method: function() {} 68 | }); 69 | 70 | expect(sut).to.be.a('function'); 71 | 72 | expect(sut).to.respondTo('method'); 73 | 74 | }); 75 | 76 | it('returns a module if one single argument is specified with a @as annotation with a "module" value.', function(){ 77 | var sut; 78 | 79 | sut = cocktail.mix({ 80 | '@as': 'module', 81 | 82 | method: function() {} 83 | }); 84 | 85 | expect(sut).to.be.an('object'); 86 | 87 | expect(sut).to.respondTo('method'); 88 | 89 | }); 90 | 91 | it('did not return a class if one single argument is specified with a @as annotation with a value different of "class".', function(){ 92 | var sut; 93 | 94 | sut = cocktail.mix({ 95 | '@as': 'SomethingElse', 96 | 97 | method: function() {} 98 | }); 99 | 100 | expect(sut).not.to.be.a('function'); 101 | expect(sut).to.be.a('object'); 102 | //even thou the Class is not returned, the object still responds to method 103 | expect(sut).to.respondTo('method'); 104 | //@as pseudo-annotation is not processed, so it is not removed from sut 105 | expect(sut).to.have.property('@as'); 106 | }); 107 | 108 | it('chains constructors when extending and parent has a constructor defined', function(){ 109 | var value = 1, 110 | Parent, Class, sut; 111 | 112 | Parent = cocktail.mix({ 113 | '@as': 'class', 114 | 115 | constructor: function(option){ 116 | this.created = value; 117 | this.option = option; 118 | } 119 | }); 120 | 121 | Class = cocktail.mix({ 122 | '@as': 'class', 123 | '@extends': Parent 124 | }); 125 | 126 | sut = new Class(value); 127 | 128 | expect(sut).to.have.property('created'); 129 | expect(sut.created).to.be.equal(value); 130 | expect(sut).to.have.property('option'); 131 | expect(sut.option).to.be.equal(value); 132 | }); 133 | 134 | }); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /test/integration/cocktail-static.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../lib/cocktail'), 5 | RestoreProcessors = require('../helper/RestoreProcessors'); 6 | 7 | var expect = chai.expect; 8 | 9 | cocktail.mix(cocktail, { 10 | '@talents': [RestoreProcessors] 11 | }); 12 | 13 | describe('cocktail Integration Test: static', function(){ 14 | 15 | beforeEach(function(){ 16 | cocktail.restoreDefaultProcessors(); 17 | }); 18 | 19 | afterEach(function(){ 20 | cocktail.restoreDefaultProcessors(); 21 | }); 22 | 23 | describe('`@static` annotations defines methods and properties in the class', function(){ 24 | it('adds static method to ClassA', function(){ 25 | var method = function(){}, 26 | ClassA, instance; 27 | 28 | ClassA = cocktail.mix({ 29 | '@as' : 'class', 30 | '@static': { 31 | method: method 32 | } 33 | }); 34 | 35 | expect(ClassA).to.be.a('function'); 36 | expect(ClassA.method).to.be.a('function'); 37 | 38 | instance = new ClassA(); 39 | 40 | expect(instance).to.not.respondTo('method'); 41 | 42 | }); 43 | 44 | it('adds static property to ClassA', function(){ 45 | var property = 'property', 46 | ClassA, instance; 47 | 48 | ClassA = cocktail.mix({ 49 | '@as' : 'class', 50 | '@static': { 51 | property: property 52 | } 53 | }); 54 | 55 | expect(ClassA).to.be.a('function'); 56 | expect(ClassA.property).to.be.a('string'); 57 | 58 | expect(ClassA.property).to.be.eql(property); 59 | 60 | instance = new ClassA(); 61 | expect(instance.property).to.be.an('undefined'); 62 | 63 | }); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/unit/cocktail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinon = require('sinon'), 5 | sinonChai = require('sinon-chai'), 6 | cocktail = require('../../lib/cocktail'), 7 | RestoreProcessors = require('../helper/RestoreProcessors'); 8 | 9 | var expect = chai.expect; 10 | 11 | chai.use(sinonChai); 12 | 13 | cocktail.mix(cocktail, { 14 | '@talents': [RestoreProcessors] 15 | }); 16 | 17 | describe('cocktail', function(){ 18 | var Merge = function Merge(){}, 19 | mergeProcessSpy = sinon.spy(), 20 | mergeSetParameterSpy = sinon.spy(), 21 | MergeSpy; 22 | 23 | Merge.prototype.name = 'spyMerge'; 24 | Merge.prototype.priority = 99; 25 | Merge.prototype.process = mergeProcessSpy; 26 | Merge.prototype.setParameter = mergeSetParameterSpy; 27 | 28 | MergeSpy = sinon.spy(Merge); 29 | 30 | beforeEach(function(){ 31 | cocktail.clearProcessors(); 32 | cocktail.registerProcessors({ 33 | '@merge': MergeSpy 34 | }); 35 | }); 36 | 37 | afterEach(function(){ 38 | cocktail.restoreDefaultProcessors(); 39 | }); 40 | 41 | 42 | describe('Registering processors', function(){ 43 | var Processor = function(){}; 44 | 45 | it('Adds new processor/s to registered processors list', function(){ 46 | cocktail.registerProcessors({ 47 | '@processor': Processor 48 | }); 49 | 50 | expect(cocktail.getProcessors()).to.have.property('@processor'); 51 | }); 52 | 53 | }); 54 | 55 | describe('Mix', function(){ 56 | var MyClass = function(){}, 57 | anObject = {}; 58 | 59 | describe('mix()', function(){ 60 | it('returns `undefined` if no args are passed.', function(){ 61 | expect(cocktail.mix()).to.be.an('undefined'); 62 | }); 63 | }); 64 | 65 | describe('mix({Class} MyClass)', function(){ 66 | it('returns the same MyClass if no other arguments are specified.', function(){ 67 | expect(cocktail.mix(MyClass)).to.be.equal(MyClass); 68 | }); 69 | }); 70 | 71 | describe('mix({Object} anObject)', function(){ 72 | it('returns the same anObject if no other arguments are specified.', function(){ 73 | expect(cocktail.mix(anObject)).to.be.equal(anObject); 74 | }); 75 | }); 76 | 77 | describe('mix({Object} classDefinition)', function(){ 78 | 79 | it('returns the a class if one single argument is specified with constructor.', function(){ 80 | var sut; 81 | 82 | sut = cocktail.mix({ 83 | constructor: function() {} 84 | }); 85 | 86 | expect(sut).to.be.a('function'); 87 | }); 88 | 89 | it('returns a class if one single argument is specified with an @extends annotation.', function(){ 90 | var MyBaseClass = function(){}, 91 | sut; 92 | 93 | sut = cocktail.mix({ 94 | '@extends': MyBaseClass 95 | }); 96 | 97 | expect(sut).to.be.a('function'); 98 | }); 99 | 100 | it('returns a class if one single argument is specified with a @traits annotation.', function(){ 101 | var MyTrait = function(){}, 102 | sut; 103 | 104 | MyTrait.prototype.method = function(){}; 105 | 106 | sut = cocktail.mix({ 107 | '@traits': [MyTrait] 108 | }); 109 | 110 | expect(sut).to.be.a('function'); 111 | 112 | }); 113 | 114 | it('constructor defined in the classDefinition should not be enumerable.', function(){ 115 | var sut; 116 | 117 | sut = cocktail.mix({ 118 | constructor: function() { 119 | } 120 | }); 121 | 122 | expect(sut).to.be.a('function'); 123 | 124 | expect(sut.prototype).to.not.contain.keys('constructor'); 125 | }); 126 | 127 | 128 | }); 129 | 130 | describe('mix({Class} MyClass, {Object} prototype)', function(){ 131 | var MyClass = function(){}, 132 | aMethod = function method(){}; 133 | 134 | 135 | it('calls merge processor with MyClass and proto', function(){ 136 | var proto = { 137 | method: aMethod, 138 | property : 1 139 | }; 140 | 141 | expect(cocktail.mix(MyClass, proto)).to.be.a('function'); 142 | 143 | expect(mergeSetParameterSpy).to.be.calledWith('single'); 144 | expect(mergeProcessSpy).to.be.calledWith(MyClass, proto); 145 | 146 | }); 147 | 148 | }); 149 | 150 | describe('mix({Object} anObject, {Object} prototype)', function(){ 151 | var anObject = {}, 152 | aMethod = function method(){}; 153 | 154 | it('calls merge processor with MyClass and proto', function(){ 155 | var proto = { 156 | method: aMethod, 157 | property : 1 158 | }; 159 | 160 | expect(cocktail.mix(anObject, proto)).to.be.an('object'); 161 | 162 | expect(mergeSetParameterSpy).to.be.calledWith('single'); 163 | expect(mergeProcessSpy).to.be.calledWith(anObject, proto); 164 | }); 165 | 166 | }); 167 | 168 | describe('mix({Class} MyClass, {Object} prototype {\'@merge\': \'strategy\'})', function(){ 169 | var MyClass = function(){}, 170 | strategy = 'strategy'; 171 | 172 | it('calls merge processor with MyClass and proto and uses the merge strategy defined by `@merge`', function(){ 173 | expect( 174 | cocktail.mix(MyClass, { 175 | '@merge': strategy, 176 | property: 1 177 | }) 178 | ).to.be.a('function'); 179 | 180 | expect(mergeSetParameterSpy).to.be.calledWith(strategy); 181 | expect(mergeProcessSpy).to.be.calledWith(MyClass, {property: 1}); 182 | 183 | }); 184 | }); 185 | 186 | }); 187 | 188 | describe('Use', function(){ 189 | 190 | describe('use(Class)', function(){ 191 | 192 | it('adds MyAnnotation Class as a custom processor', function(){ 193 | var MyAnnotation = function(){}, 194 | processors; 195 | 196 | MyAnnotation.prototype.name = '@myannotation'; 197 | 198 | cocktail.use(MyAnnotation); 199 | 200 | processors = cocktail.getProcessors(); 201 | 202 | expect(processors).to.have.property('@myannotation'); 203 | 204 | expect(processors['@myannotation']).to.be.equal(MyAnnotation); 205 | 206 | }); 207 | 208 | it('does not add MyAnnotation Class as a custom processor if name doesn\'t start wiht @', function(){ 209 | var MyAnnotation = function(){}, 210 | processors; 211 | 212 | MyAnnotation.prototype.name = 'myannotation'; 213 | 214 | cocktail.use(MyAnnotation); 215 | 216 | processors = cocktail.getProcessors(); 217 | 218 | expect(processors).to.not.have.property('@myannotation'); 219 | 220 | expect(processors['@myannotation']).to.be.equal(undefined); 221 | 222 | }); 223 | 224 | it('does not add MyAnnotation Class as a custom processor if name is not defined in prototype', function(){ 225 | var MyAnnotation = function myannotation(){}, 226 | processors; 227 | 228 | expect(MyAnnotation.name).to.be.equal('myannotation'); 229 | 230 | cocktail.use(MyAnnotation); 231 | 232 | processors = cocktail.getProcessors(); 233 | 234 | expect(processors).to.not.have.property('@myannotation'); 235 | 236 | expect(processors['@myannotation']).to.be.equal(undefined); 237 | 238 | }); 239 | 240 | }); 241 | 242 | 243 | describe('use(Object)', function(){ 244 | var MyAnnotation = {name: '@myannotation'}, 245 | processors; 246 | 247 | it('does not add MyAnnotation Object as a custom processor', function(){ 248 | cocktail.use(MyAnnotation); 249 | 250 | processors = cocktail.getProcessors(); 251 | 252 | expect(processors).to.not.have.property('@myannotation'); 253 | 254 | expect(processors['@myannotation']).to.be.equal(undefined); 255 | 256 | }); 257 | 258 | }); 259 | 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /test/unit/processor/NoOp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | cocktail = require('../../../lib/cocktail'), 5 | NoOp = require('../../../lib/processor/NoOp'); 6 | 7 | describe('NoOp Processor', function(){ 8 | var sut = new NoOp(); 9 | 10 | it('has retain set false', function(){ 11 | expect(sut.retain).to.equal(false); 12 | }); 13 | 14 | it('has priority set to cocktail.SEQUENCE.NO_OP', function(){ 15 | expect(sut.priority).to.equal(cocktail.SEQUENCE.NO_OP); 16 | }); 17 | 18 | it('a NoOP processor instance has no process method', function(){ 19 | expect(sut.process).to.be.an('undefined'); 20 | }); 21 | 22 | it('a NoOP processor instance setParameter does nothing', function(){ 23 | sut.setParameter('Whatever'); 24 | 25 | expect(sut.getParameter()).to.equal(undefined); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Annotation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinonChai = require('sinon-chai'), 5 | cocktail = require('../../../../lib/cocktail'), 6 | Annotation = require('../../../../lib/processor/annotation/Annotation.js'); 7 | 8 | var expect = chai.expect; 9 | chai.use(sinonChai); 10 | 11 | describe('Annotation Processor @annotation', function(){ 12 | var sut; 13 | 14 | sut = new Annotation(); 15 | 16 | it('has retain set false', function(){ 17 | expect(sut.retain).to.equal(false); 18 | }); 19 | 20 | it('has priority set to cocktail.SEQUENCE.ANNOTATION', function(){ 21 | expect(sut.priority).to.equal(cocktail.SEQUENCE.ANNOTATION); 22 | }); 23 | 24 | describe('Parameter for @annotation annotation', function(){ 25 | 26 | it('accepts {String} as parameter', function(){ 27 | var name = 'single'; 28 | 29 | sut.setParameter(name); 30 | 31 | expect(sut.getParameter()).to.be.equal(name); 32 | }); 33 | }); 34 | 35 | describe('Annotation process', function(){ 36 | var name = 'custom'; 37 | 38 | describe('registers the subject as a processor in cocktail', function(){ 39 | 40 | it('adds a new processor in the form of `@`+parameter', function(){ 41 | var Subject = function(){}; 42 | 43 | sut.setParameter(name); 44 | sut.process(Subject, Subject.prototype); 45 | 46 | expect(Subject.prototype).to.have.property('name'); 47 | }); 48 | }); 49 | 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../../../lib/cocktail'), 5 | Exports = require('../../../../lib/processor/annotation/Exports.js'); 6 | 7 | var expect = chai.expect; 8 | 9 | describe('Annotation Processor @exports', function(){ 10 | var sut = new Exports(); 11 | 12 | it('has retain set false', function(){ 13 | expect(sut.retain).to.equal(false); 14 | }); 15 | 16 | it('has priority set to cocktail.SEQUENCE.EXPORTS', function(){ 17 | expect(sut.priority).to.equal(cocktail.SEQUENCE.EXPORTS); 18 | }); 19 | 20 | describe('Parameter for @exports annotation', function(){ 21 | 22 | it('accepts {Object} as parameter', function(){ 23 | var sut = new Exports(), 24 | param = {}; 25 | 26 | sut.setParameter(param); 27 | 28 | expect(sut.getParameter()).to.be.equal(param); 29 | }); 30 | }); 31 | 32 | describe('Exports process', function(){ 33 | 34 | describe('Exports the subject as `exports` property in the given parameter', function(){ 35 | var sut = new Exports(), 36 | BaseClass = function(){}, 37 | module = {}; 38 | 39 | 40 | it('make the subject part of the given parameter as `exports` property', function(){ 41 | 42 | sut.setParameter(module); 43 | 44 | sut.process(BaseClass); 45 | 46 | expect(module.exports).to.not.be.an('undefined'); 47 | expect(module.exports).to.be.equal(BaseClass); 48 | 49 | }); 50 | 51 | }); 52 | 53 | describe('Do not exports the subject as `exports` property if the given parameter is not a module object', function(){ 54 | var sut = new Exports(), 55 | BaseClass = function(){}, 56 | module; 57 | 58 | 59 | it('does not exports the subject if the parameter is false', function(){ 60 | module = false; 61 | 62 | sut.setParameter(module); 63 | 64 | sut.process(BaseClass); 65 | 66 | expect(module).to.be.equal(false); 67 | 68 | }); 69 | 70 | it('does not exports the subject if the parameter is true', function(){ 71 | module = true; 72 | 73 | sut.setParameter(module); 74 | 75 | sut.process(BaseClass); 76 | 77 | expect(module).to.be.equal(true); 78 | 79 | }); 80 | 81 | it('does not exports the subject if the parameter is null', function(){ 82 | module = null; 83 | 84 | sut.setParameter(module); 85 | 86 | sut.process(BaseClass); 87 | 88 | expect(module).to.be.equal(null); 89 | 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Extends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | sinon = require('sinon'), 5 | sinonChai = require('sinon-chai'), 6 | cocktail = require('../../../../lib/cocktail'), 7 | Extends = require('../../../../lib/processor/annotation/Extends.js'); 8 | 9 | var expect = chai.expect; 10 | 11 | chai.use(sinonChai); 12 | 13 | describe('Annotation Processor @extends', function(){ 14 | var sut = new Extends(); 15 | 16 | it('has retain set false', function(){ 17 | expect(sut.retain).to.equal(false); 18 | }); 19 | 20 | it('has priority set to cocktail.SEQUENCE.EXTENDS', function(){ 21 | expect(sut.priority).to.equal(cocktail.SEQUENCE.EXTENDS); 22 | }); 23 | 24 | describe('Parameter for @extends annotation', function(){ 25 | 26 | it('accepts {function|Class} as parameter', function(){ 27 | var sut = new Extends(), 28 | Base = function(){}; 29 | 30 | sut.setParameter(Base); 31 | 32 | expect(sut.getParameter()).to.be.equal(Base); 33 | }); 34 | 35 | it('does not accept {Object} as parameter', function(){ 36 | var sut = new Extends(), 37 | object = {}; 38 | 39 | expect(function(){ 40 | sut.setParameter(object); 41 | }).to.throw(Error, /@extends/); 42 | expect(sut.getParameter()).to.be.an('undefined'); 43 | }); 44 | }); 45 | 46 | describe('Extends process', function(){ 47 | 48 | describe('Makes the subject to inherit from the given Parent passed as a parameter', function(){ 49 | var sut = new Extends(), 50 | BaseClass = function(){}, 51 | MyClass = function(){}; 52 | 53 | BaseClass.prototype.foo = function foo(){}; 54 | 55 | sut.setParameter(BaseClass); 56 | sut.process(MyClass); 57 | 58 | it('MyClass will respond to methods defined in BaseClass', function(){ 59 | expect(MyClass).to.respondTo('foo'); 60 | }); 61 | 62 | it('An instance of MyClass must be an instance of BaseClass too.', function(){ 63 | var instance = new MyClass(); 64 | 65 | expect(instance).to.be.instanceOf(MyClass); 66 | expect(instance).to.be.instanceOf(BaseClass); 67 | }); 68 | }); 69 | 70 | describe('Creates a method to call super class methods', function(){ 71 | var sut = new Extends(), 72 | BaseClass = function(){}, 73 | MyClass = function(){}, 74 | foo, myInstance; 75 | 76 | foo = sinon.spy(function(val){ 77 | return val; 78 | }); 79 | 80 | BaseClass.prototype.foo = foo; 81 | 82 | sut.setParameter(BaseClass); 83 | sut.process(MyClass); 84 | 85 | it('MyClass should have a callSuper method', function(){ 86 | expect(MyClass).to.respondTo('callSuper'); 87 | }); 88 | 89 | MyClass.prototype.foo = function(args){ 90 | return this.callSuper('foo', args); 91 | }; 92 | 93 | MyClass.prototype.noSuper = function(args){ 94 | return this.callSuper('noSuper', args); 95 | }; 96 | myInstance = new MyClass(); 97 | 98 | it('Parent method is called when using this.callSuper(`methodName`)', function(){ 99 | myInstance.foo(); 100 | 101 | expect(foo).to.have.been.calledOn(myInstance); 102 | }); 103 | 104 | it('Parent method is called with the specified parameters using this.callSuper(`methodName`, `params`)', function(){ 105 | myInstance.foo('123'); 106 | 107 | expect(foo).to.have.been.calledOn(myInstance); 108 | expect(foo).to.have.been.calledWith('123'); 109 | }); 110 | 111 | it('Parent method is called with the specified parameters using this.callSuper(`methodName`, `params`) and returns value', function(){ 112 | var ret = myInstance.foo('123'); 113 | 114 | expect(foo).to.have.been.calledOn(myInstance); 115 | expect(foo).to.have.been.calledWith('123'); 116 | expect(foo).to.have.returned('123'); 117 | expect(ret).to.be.equal('123'); 118 | }); 119 | 120 | it('should throw exception if parent class has no method', function(){ 121 | expect(function(){ 122 | myInstance.noSuper(); 123 | }).to.throw(Error); 124 | }); 125 | 126 | }); 127 | }); 128 | 129 | }); 130 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | cocktail = require('../../../../lib/cocktail'), 5 | Merge = require('../../../../lib/processor/annotation/Merge.js'); 6 | 7 | describe('Annotation Processor @merge', function(){ 8 | var sut = new Merge(); 9 | 10 | it('has retain set false', function(){ 11 | expect(sut.retain).to.equal(false); 12 | }); 13 | 14 | it('has priority set to cocktail.SEQUENCE.MERGE', function(){ 15 | expect(sut.priority).to.equal(cocktail.SEQUENCE.MERGE); 16 | }); 17 | 18 | describe('Parameter for @merge annotation', function(){ 19 | 20 | it('accepts {String} as parameter', function(){ 21 | var sut = new Merge(), 22 | strategy = 'single'; 23 | 24 | sut.setParameter(strategy); 25 | 26 | expect(sut.getParameter()).to.be.equal(strategy); 27 | }); 28 | }); 29 | 30 | describe('Merge process', function(){ 31 | 32 | describe('`single` merge strategy', function(){ 33 | var strategy = 'single'; 34 | 35 | it('adds properties and/or methods defined in proto into subject object', function(){ 36 | var subject = {}, 37 | proto = { 38 | property: 1, 39 | method: function(){} 40 | }; 41 | 42 | sut.setParameter(strategy); 43 | sut.process(subject, proto); 44 | 45 | expect(subject).to.have.property('property').that.equal(1); 46 | expect(subject).to.respondTo('method'); 47 | }); 48 | 49 | it('adds properties and/or methods defined in proto into Subject class', function(){ 50 | var Subject = function(){}, 51 | proto = { 52 | property: 1, 53 | method: function(){} 54 | }; 55 | 56 | sut.setParameter(strategy); 57 | sut.process(Subject, proto); 58 | 59 | expect(Subject.prototype).to.have.property('property').that.equal(1); 60 | expect(Subject).to.respondTo('method'); 61 | }); 62 | 63 | it('overrides properties and/or methods defined in subject object if they have the same name', function(){ 64 | var aMethod = function(){}, 65 | subject = { 66 | property: 0, 67 | override: function(){} 68 | }, 69 | proto = { 70 | property: 1, 71 | method: function(){}, 72 | override: aMethod 73 | }; 74 | 75 | sut.setParameter(strategy); 76 | sut.process(subject, proto); 77 | 78 | expect(subject).to.have.property('property').that.equal(1); 79 | expect(subject).to.respondTo('method'); 80 | expect(subject).to.respondTo('override'); 81 | expect(subject.override).to.be.equal(aMethod); 82 | }); 83 | 84 | it('overrides properties and/or methods defined in Subject class if they have the same name', function(){ 85 | var aMethod = function(){}, 86 | Subject = function(){}, 87 | proto = { 88 | property: 1, 89 | method: function(){}, 90 | override: aMethod 91 | }; 92 | 93 | Subject.prototype.property = 0; 94 | Subject.prototype.override = function(){}; 95 | 96 | sut.setParameter(strategy); 97 | sut.process(Subject, proto); 98 | 99 | expect(Subject.prototype).to.have.property('property').that.equal(1); 100 | expect(Subject).to.respondTo('method'); 101 | expect(Subject).to.respondTo('override'); 102 | expect(Subject.prototype.override).to.be.equal(aMethod); 103 | }); 104 | 105 | 106 | }); 107 | 108 | 109 | describe('`their` merge strategy', function(){ 110 | var strategy = 'their'; 111 | 112 | it('adds properties and/or methods defined in proto into subject object', function(){ 113 | var subject = {}, 114 | proto = { 115 | property: 1, 116 | method: function(){} 117 | }; 118 | 119 | sut.setParameter(strategy); 120 | sut.process(subject, proto); 121 | 122 | expect(subject).to.have.property('property').that.equal(1); 123 | expect(subject).to.respondTo('method'); 124 | }); 125 | 126 | it('adds properties and/or methods defined in proto into Subject class', function(){ 127 | var Subject = function(){}, 128 | proto = { 129 | property: 1, 130 | method: function(){} 131 | }; 132 | 133 | sut.setParameter(strategy); 134 | sut.process(Subject, proto); 135 | 136 | expect(Subject.prototype).to.have.property('property').that.equal(1); 137 | expect(Subject).to.respondTo('method'); 138 | }); 139 | 140 | it('does NOT override properties and/or methods defined in subject object if they have the same name', function(){ 141 | var aMethod = function(){}, 142 | subject = { 143 | property: 0, 144 | override: aMethod 145 | }, 146 | proto = { 147 | property: 1, 148 | method: function(){}, 149 | override: function(){} 150 | }; 151 | 152 | sut.setParameter(strategy); 153 | sut.process(subject, proto); 154 | 155 | expect(subject).to.have.property('property').that.equal(0); 156 | expect(subject).to.respondTo('method'); 157 | expect(subject).to.respondTo('override'); 158 | expect(subject.override).to.be.equal(aMethod); 159 | }); 160 | 161 | it('does NOT override properties and/or methods defined in Subject class if they have the same name', function(){ 162 | var aMethod = function(){}, 163 | Subject = function(){}, 164 | proto = { 165 | property: 1, 166 | method: function(){}, 167 | override: function(){} 168 | }; 169 | 170 | Subject.prototype.property = 0; 171 | Subject.prototype.override = aMethod; 172 | 173 | sut.setParameter(strategy); 174 | sut.process(Subject, proto); 175 | 176 | expect(Subject.prototype).to.have.property('property').that.equal(0); 177 | expect(Subject).to.respondTo('method'); 178 | expect(Subject).to.respondTo('override'); 179 | expect(Subject.prototype.override).to.be.equal(aMethod); 180 | }); 181 | 182 | }); 183 | 184 | 185 | describe('`deep-mine` merge strategy', function(){ 186 | var strategy = 'deep-mine'; 187 | 188 | it('adds properties and/or methods defined in proto into subject object', function(){ 189 | var subject = {}, 190 | proto = { 191 | property: 1, 192 | method: function(){} 193 | }; 194 | 195 | sut.setParameter(strategy); 196 | sut.process(subject, proto); 197 | 198 | expect(subject).to.have.property('property').that.equal(1); 199 | expect(subject).to.respondTo('method'); 200 | }); 201 | 202 | it('adds properties and/or methods defined in proto into Subject class', function(){ 203 | var Subject = function(){}, 204 | proto = { 205 | property: 1, 206 | method: function(){} 207 | }; 208 | 209 | sut.setParameter(strategy); 210 | sut.process(Subject, proto); 211 | 212 | expect(Subject.prototype).to.have.property('property').that.equal(1); 213 | expect(Subject).to.respondTo('method'); 214 | }); 215 | 216 | it('overrides properties defined in subject object if they have the same name, if the target property is an object it merges the content using strategy mine', function(){ 217 | var subject = { 218 | property: { 219 | sub1 : 0, 220 | sub2: 1 221 | } 222 | }, 223 | proto = { 224 | property: { 225 | sub1: 1 226 | } 227 | }; 228 | 229 | sut.setParameter(strategy); 230 | sut.process(subject, proto); 231 | 232 | expect(subject.property).to.have.property('sub1').that.equal(1); 233 | expect(subject.property).to.have.property('sub2').that.equal(1); 234 | }); 235 | 236 | it('overrides properties defined in subject object if they have the same name, if the target property is an array it concatenates the content', function(){ 237 | var subject = { 238 | arr: [1, 2, 3] 239 | }, 240 | proto = { 241 | arr: [4, 5, 6] 242 | }; 243 | 244 | sut.setParameter(strategy); 245 | sut.process(subject, proto); 246 | expect(subject.arr).to.eql([1, 2, 3, 4, 5, 6]); 247 | }); 248 | }); 249 | 250 | describe('`deep-their` merge strategy', function(){ 251 | var strategy = 'deep-their'; 252 | 253 | it('adds properties and/or methods defined in proto into subject object', function(){ 254 | var subject = {}, 255 | proto = { 256 | property: 1, 257 | method: function(){} 258 | }; 259 | 260 | sut.setParameter(strategy); 261 | sut.process(subject, proto); 262 | 263 | expect(subject).to.have.property('property').that.equal(1); 264 | expect(subject).to.respondTo('method'); 265 | }); 266 | 267 | it('adds properties and/or methods defined in proto into Subject class', function(){ 268 | var Subject = function(){}, 269 | proto = { 270 | property: 1, 271 | method: function(){} 272 | }; 273 | 274 | sut.setParameter(strategy); 275 | sut.process(Subject, proto); 276 | 277 | expect(Subject.prototype).to.have.property('property').that.equal(1); 278 | expect(Subject).to.respondTo('method'); 279 | }); 280 | 281 | it('overrides properties defined in subject object if they have the same name, if the target property is an object it merges the content using strategy their', function(){ 282 | var subject = { 283 | property: { 284 | sub1 : 0 285 | } 286 | }, 287 | proto = { 288 | property: { 289 | sub1: 1, 290 | sub2: 1 291 | } 292 | }; 293 | 294 | sut.setParameter(strategy); 295 | sut.process(subject, proto); 296 | 297 | expect(subject.property).to.have.property('sub1').that.equal(0); 298 | expect(subject.property).to.have.property('sub2').that.equal(1); 299 | }); 300 | 301 | it('overrides properties defined in subject object if they have the same name, if the target property is an array it concatenates the content', function(){ 302 | var subject = { 303 | arr: [1, 2, 3] 304 | }, 305 | proto = { 306 | arr: [4, 5, 6] 307 | }; 308 | 309 | sut.setParameter(strategy); 310 | sut.process(subject, proto); 311 | expect(subject.arr).to.eql([1, 2, 3, 4, 5, 6]); 312 | }); 313 | }); 314 | 315 | describe('`properties` merge strategy', function(){ 316 | var strategy = 'properties'; 317 | 318 | it('adds only properties defined in proto into subject object', function(){ 319 | var subject = {}, 320 | proto = { 321 | property: 1, 322 | method: function(){} 323 | }; 324 | 325 | sut.setParameter(strategy); 326 | sut.process(subject, proto); 327 | 328 | expect(subject).to.have.property('property').that.equal(1); 329 | expect(subject).to.not.respondTo('method'); 330 | }); 331 | 332 | it('adds only properties defined in proto into Subject class', function(){ 333 | var Subject = function(){}, 334 | proto = { 335 | property: 1, 336 | method: function(){} 337 | }; 338 | 339 | sut.setParameter(strategy); 340 | sut.process(Subject, proto); 341 | 342 | expect(Subject.prototype).to.have.property('property').that.equal(1); 343 | expect(Subject).to.not.respondTo('method'); 344 | }); 345 | 346 | // it('overrides properties defined in subject object if they have the same name, if the target property is an object it merges the content using strategy their', function(){ 347 | // var subject = { 348 | // property: { 349 | // sub1 : 0 350 | // } 351 | // }, 352 | // proto = { 353 | // property: { 354 | // sub1: 1, 355 | // sub2: 1 356 | // } 357 | // }; 358 | 359 | // sut.setParameter(strategy); 360 | // sut.process(subject, proto); 361 | 362 | // expect(subject.property).to.have.property('sub1').that.equal(0); 363 | // expect(subject.property).to.have.property('sub2').that.equal(1); 364 | // }); 365 | 366 | // it('overrides properties defined in subject object if they have the same name, if the target property is an array it concatenates the content', function(){ 367 | // var subject = { 368 | // arr: [1,2,3] 369 | // }, 370 | // proto = { 371 | // arr: [4,5,6] 372 | // }; 373 | 374 | // sut.setParameter(strategy); 375 | // sut.process(subject, proto); 376 | // expect(subject.arr).to.eql([1,2,3,4,5,6]); 377 | // }); 378 | }); 379 | 380 | }); 381 | }); 382 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Properties.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | cocktail = require('../../../../lib/cocktail'), 5 | Properties = require('../../../../lib/processor/annotation/Properties.js'); 6 | 7 | describe('Annotation Processor @properties', function(){ 8 | var sut = new Properties(); 9 | 10 | it('has retain set false', function(){ 11 | expect(sut.retain).to.equal(false); 12 | }); 13 | 14 | it('has priority set to cocktail.SEQUENCE.PROPERTIES', function(){ 15 | expect(sut.priority).to.equal(cocktail.SEQUENCE.PROPERTIES); 16 | }); 17 | 18 | describe('Parameter for @properties annotation', function(){ 19 | 20 | it('accepts {} as parameter', function(){ 21 | var sut = new Properties(), 22 | properties = {}; 23 | 24 | sut.setParameter(properties); 25 | 26 | expect(sut.getParameter()).to.be.equal(properties); 27 | }); 28 | 29 | it('throws an error if parameter is set to an Array', function(){ 30 | var sut = new Properties(); 31 | 32 | expect(function(){ 33 | sut.setParameter(['some']); 34 | }).to.throw(Error, /@properties parameter should be an Object/); 35 | expect(sut.getParameter()).to.be.an('undefined'); 36 | }); 37 | 38 | it('throws an error if parameter is set to a String', function(){ 39 | var sut = new Properties(); 40 | 41 | expect(function(){ 42 | sut.setParameter('some'); 43 | }).to.throw(Error, /@properties parameter should be an Object/); 44 | expect(sut.getParameter()).to.be.an('undefined'); 45 | }); 46 | 47 | }); 48 | 49 | describe('Properties process', function(){ 50 | 51 | describe('Creates getters and setters for the specified property into the given Class', function(){ 52 | var sut = new Properties(), 53 | MyClass = function(){}; 54 | 55 | sut.setParameter({ 56 | 'undef': undefined, 57 | 'nullVal': null, 58 | 'name': 'My Name', 59 | 'testBool': true, 60 | 'falsyBool': false, 61 | 'number': 1, 62 | 'falsyNumber' : 0 63 | }); 64 | sut.process(MyClass); 65 | 66 | it('creates the property in the subject and sets the given value', function(){ 67 | expect(MyClass.prototype.undef).to.be.equal(undefined); 68 | expect(MyClass.prototype.nullVal).to.be.equal(null); 69 | expect(MyClass.prototype.name).to.be.equal('My Name'); 70 | expect(MyClass.prototype.testBool).to.be.equal(true); 71 | expect(MyClass.prototype.falsyBool).to.be.equal(false); 72 | expect(MyClass.prototype.number).to.be.equal(1); 73 | expect(MyClass.prototype.falsyNumber).to.be.equal(0); 74 | 75 | }); 76 | 77 | it('creates getter and setter with setXXX getXXX for String, Numbers, Array, `null`, and `undefined` properties', function(){ 78 | expect(MyClass).to.respondTo('setUndef'); 79 | expect(MyClass).to.respondTo('getUndef'); 80 | 81 | expect(MyClass).to.respondTo('setNullVal'); 82 | expect(MyClass).to.respondTo('getNullVal'); 83 | 84 | expect(MyClass).to.respondTo('setName'); 85 | expect(MyClass).to.respondTo('getName'); 86 | 87 | expect(MyClass).to.respondTo('setNumber'); 88 | expect(MyClass).to.respondTo('getNumber'); 89 | 90 | expect(MyClass).to.respondTo('setFalsyNumber'); 91 | expect(MyClass).to.respondTo('getFalsyNumber'); 92 | 93 | }); 94 | 95 | it('creates getter as isXXX for boolean properties', function(){ 96 | expect(MyClass).to.respondTo('setTestBool'); 97 | expect(MyClass).to.respondTo('isTestBool'); 98 | 99 | expect(MyClass).to.respondTo('setFalsyBool'); 100 | expect(MyClass).to.respondTo('isFalsyBool'); 101 | 102 | }); 103 | 104 | it('its setters modify the property and it is returned by its getter', function(){ 105 | var instance = new MyClass(), 106 | stringVal = 'VALUE'; 107 | 108 | instance.setName(stringVal); 109 | 110 | expect(instance.name).to.be.equal(stringVal); 111 | expect(instance.getName()).to.be.equal(stringVal); 112 | }); 113 | }); 114 | 115 | describe('Creates getters and setters for the specified property into the given Object', function(){ 116 | var sut = new Properties(), 117 | myObject = {}; 118 | 119 | sut.setParameter({ 120 | 'undef': undefined, 121 | 'nullVal': null, 122 | 'name': 'My Name', 123 | 'testBool': true, 124 | 'falsyBool': false, 125 | 'number': 1, 126 | 'falsyNumber' : 0 127 | }); 128 | sut.process(myObject); 129 | 130 | it('creates the property in the subject and sets the given value', function(){ 131 | expect(myObject.undef).to.be.equal(undefined); 132 | expect(myObject.nullVal).to.be.equal(null); 133 | expect(myObject.name).to.be.equal('My Name'); 134 | expect(myObject.testBool).to.be.equal(true); 135 | expect(myObject.falsyBool).to.be.equal(false); 136 | expect(myObject.number).to.be.equal(1); 137 | expect(myObject.falsyNumber).to.be.equal(0); 138 | }); 139 | 140 | it('creates getter and setter with setXXX getXXX for String, Numbers, Array, `null`, and `undefined` properties', function(){ 141 | expect(myObject).to.respondTo('setUndef'); 142 | expect(myObject).to.respondTo('getUndef'); 143 | 144 | expect(myObject).to.respondTo('setNullVal'); 145 | expect(myObject).to.respondTo('getNullVal'); 146 | 147 | expect(myObject).to.respondTo('setName'); 148 | expect(myObject).to.respondTo('getName'); 149 | 150 | expect(myObject).to.respondTo('setNumber'); 151 | expect(myObject).to.respondTo('getNumber'); 152 | 153 | expect(myObject).to.respondTo('setFalsyNumber'); 154 | expect(myObject).to.respondTo('getFalsyNumber'); 155 | 156 | }); 157 | 158 | it('creates getter as isXXX for boolean properties', function(){ 159 | expect(myObject).to.respondTo('setTestBool'); 160 | expect(myObject).to.respondTo('isTestBool'); 161 | 162 | expect(myObject).to.respondTo('setFalsyBool'); 163 | expect(myObject).to.respondTo('isFalsyBool'); 164 | 165 | }); 166 | 167 | it('its setters modify the property and it is returned by its getter', function(){ 168 | var instance = myObject, 169 | stringVal = 'VALUE'; 170 | 171 | instance.setName(stringVal); 172 | 173 | expect(instance.name).to.be.equal(stringVal); 174 | expect(instance.getName()).to.be.equal(stringVal); 175 | }); 176 | }); 177 | 178 | describe('Does nothing if parameter is not a plain and non empty object', function(){ 179 | var sut = new Properties(), 180 | MyClass = function(){}; 181 | 182 | 183 | it('keeps the prototype untouched if no property is defined in the param', function(){ 184 | sut.setParameter({}); 185 | sut.process(MyClass); 186 | expect(MyClass.prototype).to.be.empty; 187 | }); 188 | 189 | it('keeps the prototype untouched if param is not a plain object', function(){ 190 | var CustomClass = function() {}; 191 | 192 | CustomClass.prototype.value = 1; 193 | 194 | sut.setParameter(new CustomClass()); 195 | sut.process(MyClass); 196 | expect(MyClass.prototype).to.be.empty; 197 | }); 198 | }); 199 | 200 | describe('Does not override value when processing an Object and property is already defined in the given object', function(){ 201 | var sut = new Properties(), 202 | value = 'DEFINED', 203 | myObj = {alreadyDefined: value}; 204 | 205 | sut.setParameter({alreadyDefined: 'default'}); 206 | sut.process(myObj); 207 | 208 | it('keeps the object value if it is already defined', function(){ 209 | expect(myObj.alreadyDefined).to.be.equal(value); 210 | }); 211 | 212 | it('creates getter and setter for the alreadyDefined property', function(){ 213 | expect(myObj).to.respondTo('getAlreadyDefined'); 214 | expect(myObj).to.respondTo('setAlreadyDefined'); 215 | }); 216 | }); 217 | }); 218 | 219 | }); 220 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Requires.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | cocktail = require('../../../../lib/cocktail'), 5 | Requires = require('../../../../lib/processor/annotation/Requires.js'); 6 | 7 | describe('Annotation Processor @requires', function(){ 8 | var sut = new Requires(); 9 | 10 | 11 | it('has retain set false', function(){ 12 | expect(sut.retain).to.equal(false); 13 | }); 14 | 15 | it('has priority set to cocktail.SEQUENCE.REQUIRES', function(){ 16 | expect(sut.priority).to.equal(cocktail.SEQUENCE.REQUIRES); 17 | }); 18 | 19 | describe('Parameter for @properties annotation', function(){ 20 | 21 | it('accepts a String[] as parameter', function(){ 22 | var sut = new Requires(), 23 | requires = ['aRequired']; 24 | 25 | sut.setParameter(requires); 26 | 27 | expect(sut.getParameter()).to.contain('aRequired'); 28 | }); 29 | 30 | it('accepts a String as parameter', function(){ 31 | var sut = new Requires(), 32 | requires = 'aRequired'; 33 | 34 | sut.setParameter(requires); 35 | 36 | expect(sut.getParameter()).to.contain('aRequired'); 37 | }); 38 | 39 | }); 40 | 41 | describe('Requires process', function(){ 42 | 43 | describe('Creates a required function for each method specified in parameters', function(){ 44 | 45 | it('creates the method as part of the given subject as a required function', function(){ 46 | var sut = new Requires(), 47 | MyTrait = function(){}; 48 | 49 | sut.setParameter([ 50 | 'requiredMethod' 51 | ]); 52 | 53 | sut.process(MyTrait); 54 | 55 | expect(MyTrait).to.respondTo('requiredMethod'); 56 | expect(MyTrait.prototype.requiredMethod).to.be.equal(Requires.requiredMethod); 57 | }); 58 | 59 | it('does not create the method if it has been already defined', function(){ 60 | var sut = new Requires(), 61 | MyTrait = function(){}, 62 | requiredMethod = function(){ 63 | return 'required'; 64 | }; 65 | 66 | MyTrait.prototype.requiredMethod = requiredMethod; 67 | 68 | sut.setParameter([ 69 | 'requiredMethod' 70 | ]); 71 | 72 | sut.process(MyTrait); 73 | 74 | expect(MyTrait).to.respondTo('requiredMethod'); 75 | expect(MyTrait.prototype.requiredMethod).to.be.equal(requiredMethod); 76 | expect(MyTrait.prototype.requiredMethod).to.not.be.equal(Requires.requiredMethod); 77 | }); 78 | 79 | 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Static.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | cocktail = require('../../../../lib/cocktail'), 5 | Static = require('../../../../lib/processor/annotation/Static.js'); 6 | 7 | describe('Annotation Processor @static', function(){ 8 | var sut = new Static(); 9 | 10 | it('has retain set false', function(){ 11 | expect(sut.retain).to.equal(false); 12 | }); 13 | 14 | it('has priority set to cocktail.SEQUENCE.POST_MERGE', function(){ 15 | expect(sut.priority).to.equal(cocktail.SEQUENCE.POST_MERGE); 16 | }); 17 | 18 | describe('Parameters for @static annotation', function(){ 19 | 20 | it('accepts a literal object as parameter', function(){ 21 | var sut = new Static(), 22 | param = {}; 23 | 24 | sut.setParameter(param); 25 | 26 | expect(sut.getParameter()).to.be.equal(param); 27 | }); 28 | 29 | it('throws an error if parameter is set to an Array', function(){ 30 | var sut = new Static(); 31 | 32 | expect(function(){ 33 | sut.setParameter(['some']); 34 | }).to.throw(Error, /@static parameter should be an Object/); 35 | expect(sut.getParameter()).to.be.an('undefined'); 36 | }); 37 | 38 | it('throws an error if parameter is set to a String', function(){ 39 | var sut = new Static(); 40 | 41 | expect(function(){ 42 | sut.setParameter('some'); 43 | }).to.throw(Error, /@static parameter should be an Object/); 44 | expect(sut.getParameter()).to.be.an('undefined'); 45 | }); 46 | 47 | }); 48 | 49 | describe('Static process', function(){ 50 | 51 | it('adds a function as a static method on ClassA', function(){ 52 | var sut = new Static(), 53 | method = function() {}, 54 | MyClass = function(){}; 55 | 56 | sut.setParameter({ 57 | methodAsStatic: method 58 | }); 59 | 60 | sut.process(MyClass); 61 | 62 | expect(MyClass.methodAsStatic).to.be.equal(method); 63 | expect(MyClass.prototype.methodAsStatic).to.be.equal(undefined); 64 | 65 | }); 66 | 67 | it('adds a value as a static property on ClassA', function(){ 68 | var sut = new Static(), 69 | property = 'someValue', 70 | MyClass = function(){}; 71 | 72 | sut.setParameter({ 73 | propertyAsStatic: property 74 | }); 75 | 76 | sut.process(MyClass); 77 | 78 | expect(MyClass.propertyAsStatic).to.be.equal(property); 79 | expect(MyClass.prototype.propertyAsStatic).to.be.equal(undefined); 80 | 81 | }); 82 | 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Talents.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../../../lib/cocktail'), 5 | // Requires = require('../../../../lib/processor/annotation/Requires.js'), 6 | Talents = require('../../../../lib/processor/annotation/Talents.js'); 7 | 8 | var expect = chai.expect; 9 | 10 | describe('Annotation Processor @talents', function(){ 11 | var sut = new Talents(); 12 | 13 | it('has retain set false', function(){ 14 | expect(sut.retain).to.equal(false); 15 | }); 16 | 17 | it('has priority set to cocktail.SEQUENCE.TRAITS', function(){ 18 | expect(sut.priority).to.equal(cocktail.SEQUENCE.TRAITS); 19 | }); 20 | 21 | describe('Parameter for @talents annotation', function(){ 22 | 23 | it('accepts an array of Talents as parameter', function(){ 24 | var sut = new Talents(), 25 | traitDef = {}; 26 | 27 | sut.setParameter([traitDef]); 28 | 29 | expect(sut.getParameter()).to.contain(traitDef); 30 | }); 31 | }); 32 | 33 | describe('Talents process', function(){ 34 | 35 | describe('Passing a Talent class reference `@talents:[TalentA]`', function(){ 36 | var sut = new Talents(), 37 | TalentA = function(){}, 38 | myObj = {}, 39 | aMethod = function method(){}; 40 | 41 | TalentA.prototype.aMethod = aMethod; 42 | myObj.foo = 1; 43 | 44 | sut.setParameter([TalentA]); 45 | 46 | sut.process(myObj); 47 | 48 | it('makes the TalentA methods part of the given myObj', function(){ 49 | expect(myObj).to.respondTo('aMethod'); 50 | expect(myObj.aMethod).to.be.equal(aMethod); 51 | }); 52 | }); 53 | 54 | describe('Passing a Talent object reference `@talents:[TalentA]`', function(){ 55 | var sut = new Talents(), 56 | TalentA = {}, 57 | myObj = {}, 58 | aMethod = function method(){}; 59 | 60 | TalentA.aMethod = aMethod; 61 | myObj.foo = 1; 62 | 63 | sut.setParameter([TalentA]); 64 | 65 | sut.process(myObj); 66 | 67 | it('makes the TalentA methods part of the given myObj', function(){ 68 | expect(myObj).to.respondTo('aMethod'); 69 | expect(myObj.aMethod).to.be.equal(aMethod); 70 | }); 71 | }); 72 | 73 | describe('Passing a Talent class using options object `@talents: [{talent: TalentA}]`', function(){ 74 | var sut = new Talents(), 75 | TalentA = function(){}, 76 | myObj = {}, 77 | aMethod = function method(){}; 78 | 79 | TalentA.prototype.aMethod = aMethod; 80 | myObj.foo = 1; 81 | 82 | sut.setParameter([{talent: TalentA}]); 83 | 84 | sut.process(myObj); 85 | 86 | it('makes the TalentA methods part of the given myObj', function(){ 87 | expect(myObj).to.respondTo('aMethod'); 88 | expect(myObj.aMethod).to.be.equal(aMethod); 89 | }); 90 | }); 91 | 92 | 93 | describe('Passing a Talent object using options object `@talents: [{talent: TalentA}]`', function(){ 94 | var sut = new Talents(), 95 | TalentA = {}, 96 | myObj = {}, 97 | aMethod = function method(){}; 98 | 99 | TalentA.aMethod = aMethod; 100 | myObj.foo = 1; 101 | 102 | sut.setParameter([{talent: TalentA}]); 103 | 104 | sut.process(myObj); 105 | 106 | it('makes the TalentA methods part of the given myObj', function(){ 107 | expect(myObj).to.respondTo('aMethod'); 108 | expect(myObj.aMethod).to.be.equal(aMethod); 109 | }); 110 | }); 111 | 112 | describe('Passing a Talent reference `@talents:[TalentA]` to a Class', function(){ 113 | var sut = new Talents(), 114 | TalentA = function(){}, 115 | MyClass = function(){}, 116 | aMethod = function method(){}; 117 | 118 | TalentA.prototype.aMethod = aMethod; 119 | MyClass.prototype.foo = 1; 120 | 121 | sut.setParameter([TalentA]); 122 | 123 | sut.process(MyClass); 124 | 125 | it('makes the TalentA methods part of the given MyClass as static method', function(){ 126 | expect(MyClass).to.not.respondTo('aMethod'); 127 | expect(MyClass.aMethod).to.be.equal(aMethod); 128 | }); 129 | }); 130 | 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/unit/processor/annotation/Traits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | cocktail = require('../../../../lib/cocktail'), 5 | Requires = require('../../../../lib/processor/annotation/Requires.js'), 6 | Traits = require('../../../../lib/processor/annotation/Traits.js'); 7 | 8 | var expect = chai.expect; 9 | 10 | describe('Annotation Processor @traits', function(){ 11 | var sut = new Traits(); 12 | 13 | it('has retain set false', function(){ 14 | expect(sut.retain).to.equal(false); 15 | }); 16 | 17 | it('has priority set to cocktail.SEQUENCE.TRAITS', function(){ 18 | expect(sut.priority).to.equal(cocktail.SEQUENCE.TRAITS); 19 | }); 20 | 21 | describe('Parameter for @traits annotation', function(){ 22 | 23 | it('accepts an array of Traits as parameter', function(){ 24 | var sut = new Traits(), 25 | traitDef = {}; 26 | 27 | sut.setParameter([traitDef]); 28 | 29 | expect(sut.getParameter()).to.contain(traitDef); 30 | }); 31 | }); 32 | 33 | describe('Traits process', function(){ 34 | 35 | describe('Passing a Trait class reference `@traits:[TraitA]`', function(){ 36 | var sut = new Traits(), 37 | TraitA = function(){}, 38 | MyClass = function(){}, 39 | aMethod = function method(){}; 40 | 41 | TraitA.prototype.aMethod = aMethod; 42 | MyClass.prototype.foo = 1; 43 | 44 | sut.setParameter([TraitA]); 45 | 46 | sut.process(MyClass); 47 | 48 | it('makes the TraitA methods part of the given MyClass', function(){ 49 | expect(MyClass).to.respondTo('aMethod'); 50 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 51 | }); 52 | }); 53 | 54 | describe('Passing a Trait object reference `@traits:[TraitA]`', function(){ 55 | var sut = new Traits(), 56 | TraitA = {}, 57 | MyClass = function(){}, 58 | aMethod = function method(){}; 59 | 60 | TraitA.aMethod = aMethod; 61 | MyClass.prototype.foo = 1; 62 | 63 | sut.setParameter([TraitA]); 64 | 65 | sut.process(MyClass); 66 | 67 | it('makes the TraitA methods part of the given MyClass', function(){ 68 | expect(MyClass).to.respondTo('aMethod'); 69 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 70 | }); 71 | }); 72 | 73 | describe('Passing a ES6-type class as Trait reference `@traits:[TraitA]`', function(){ 74 | var sut = new Traits(), 75 | TraitA = function(){}, 76 | MyClass = function(){}, 77 | aMethod = function method(){}, 78 | propDesc = { 79 | enumerable: false, 80 | value: aMethod 81 | }; 82 | 83 | Object.defineProperty( 84 | TraitA.prototype, 85 | 'aMethod', 86 | propDesc 87 | ); 88 | sut.setParameter([TraitA]); 89 | 90 | sut.process(MyClass); 91 | 92 | it('makes the TraitA methods part of the given MyClass', function(){ 93 | expect(MyClass).to.respondTo('aMethod'); 94 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 95 | }); 96 | 97 | it('makes MyClass method to keep same property descriptor', function(){ 98 | expect(Object.getOwnPropertyDescriptor(MyClass.prototype, 'aMethod') === propDesc); 99 | }); 100 | }); 101 | 102 | describe('Passing a Trait class using options object `@traits: [{trait: TraitA}]`', function(){ 103 | var sut = new Traits(), 104 | TraitA = function(){}, 105 | MyClass = function(){}, 106 | aMethod = function method(){}; 107 | 108 | TraitA.prototype.aMethod = aMethod; 109 | MyClass.prototype.foo = 1; 110 | 111 | sut.setParameter([{trait: TraitA}]); 112 | 113 | sut.process(MyClass); 114 | 115 | it('makes the TraitA methods part of the given MyClass', function(){ 116 | expect(MyClass).to.respondTo('aMethod'); 117 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 118 | }); 119 | }); 120 | 121 | describe('Passing a Trait object using options object `@traits: [{trait: TraitA}]`', function(){ 122 | var sut = new Traits(), 123 | TraitA = {}, 124 | MyClass = function(){}, 125 | aMethod = function method(){}; 126 | 127 | TraitA.aMethod = aMethod; 128 | MyClass.prototype.foo = 1; 129 | 130 | sut.setParameter([{trait: TraitA}]); 131 | 132 | sut.process(MyClass); 133 | 134 | it('makes the TraitA methods part of the given MyClass', function(){ 135 | expect(MyClass).to.respondTo('aMethod'); 136 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 137 | }); 138 | }); 139 | 140 | describe('A Trait must not contain state', function(){ 141 | var sut = new Traits(), 142 | TraitA = function(){}, 143 | MyClass = function(){}, 144 | aMethod = function method(){}; 145 | 146 | TraitA.prototype.aMethod = aMethod; 147 | TraitA.prototype.state = '1'; 148 | 149 | sut.setParameter([TraitA]); 150 | 151 | it('throws exception if there is any state in the Trait definition', function(){ 152 | expect(function(){ 153 | sut.process(MyClass); 154 | }).to.throw(Error, /Trait MUST NOT contain any state/); 155 | }); 156 | }); 157 | 158 | describe('Traits are applied not matter the order they are listed', function(){ 159 | var sut = new Traits(), 160 | TraitA = function(){}, 161 | TraitB = function(){}, 162 | MyClass = function(){}, 163 | AnotherClass = function(){}, 164 | aMethod = function method(){}, 165 | anotherMethod = function anotherMethod(){}; 166 | 167 | TraitA.prototype.aMethod = aMethod; 168 | 169 | TraitB.prototype.anotherMethod = anotherMethod; 170 | 171 | 172 | it('creates the same result no matter the order of traits', function(){ 173 | sut.setParameter([TraitA, TraitB]); 174 | sut.process(MyClass); 175 | 176 | sut.setParameter([TraitB, TraitA]); 177 | sut.process(AnotherClass); 178 | 179 | expect(MyClass.prototype).to.be.eql(AnotherClass.prototype); 180 | 181 | }); 182 | 183 | }); 184 | 185 | describe('Traits are applied not matter the order they are created', function(){ 186 | var sut = new Traits(), 187 | TraitA = function(){}, 188 | TraitB = function(){}, 189 | MyClass = function(){}, 190 | AnotherClass = function(){}, 191 | aMethod = function method(){}, 192 | anotherMethod = function anotherMethod(){}; 193 | 194 | TraitA.prototype.aMethod = aMethod; 195 | 196 | //TraitB contains TraitA 197 | TraitB.prototype.aMethod = aMethod; 198 | TraitB.prototype.anotherMethod = anotherMethod; 199 | 200 | 201 | it('creates the same result no matter the order of traits', function(){ 202 | sut.setParameter([TraitA, TraitB]); 203 | sut.process(MyClass); 204 | 205 | sut.setParameter([TraitB, TraitA]); 206 | sut.process(AnotherClass); 207 | 208 | expect(MyClass.prototype).to.be.eql(AnotherClass.prototype); 209 | 210 | }); 211 | 212 | }); 213 | 214 | describe('Excluding method from TraitA `@traits: [{trait: TraitA, excludes:[\'anotherMethod\']}]`', function(){ 215 | var sut = new Traits(), 216 | TraitA = function(){}, 217 | MyClass = function(){}, 218 | aMethod = function method(){}, 219 | anotherMethod = function anotherMethod(){}; 220 | 221 | TraitA.prototype.aMethod = aMethod; 222 | TraitA.prototype.anotherMethod = anotherMethod; 223 | 224 | MyClass.prototype.foo = 1; 225 | 226 | sut.setParameter([{trait: TraitA, excludes:['anotherMethod']}]); 227 | 228 | sut.process(MyClass); 229 | 230 | it('excludes anotherMethod from TraitA so it is not part of MyClass', function(){ 231 | expect(MyClass).to.respondTo('aMethod'); 232 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 233 | expect(MyClass).to.not.respondTo('anotherMethod'); 234 | }); 235 | }); 236 | 237 | describe('Aliasing methods from trait `@traits: [{trait: TraitA, alias:{\'anotherMethod\': \'myAnother\'}}]`', function(){ 238 | var sut = new Traits(), 239 | TraitA = function(){}, 240 | MyClass = function(){}, 241 | aMethod = function method(){}, 242 | anotherMethod = function anotherMethod(){}; 243 | 244 | TraitA.prototype.aMethod = aMethod; 245 | TraitA.prototype.anotherMethod = anotherMethod; 246 | 247 | MyClass.prototype.foo = 1; 248 | 249 | sut.setParameter([ 250 | { 251 | trait: TraitA, 252 | alias:{'anotherMethod': 'myAnother'} 253 | } 254 | ]); 255 | 256 | sut.process(MyClass); 257 | 258 | it('creates alias myAnother in MyClass for TraitA method anotherMethod', function(){ 259 | expect(MyClass).to.respondTo('aMethod'); 260 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 261 | expect(MyClass).to.not.respondTo('anotherMethod'); 262 | expect(MyClass).to.respondTo('myAnother'); 263 | expect(MyClass.prototype.myAnother).to.be.equal(anotherMethod); 264 | 265 | }); 266 | }); 267 | 268 | describe('Non Conflicts between TraitA and MyClass', function(){ 269 | var sut = new Traits(), 270 | TraitA = function(){}, 271 | MyClass = function(){}, 272 | sameMethod = function(){}, 273 | aMethod = function method(){}, 274 | anotherMethod = function anotherMethod(){}; 275 | 276 | TraitA.prototype.sameMethod = sameMethod; 277 | TraitA.prototype.aMethod = Requires.requiredMethod; 278 | TraitA.prototype.aReqMethod = anotherMethod; 279 | 280 | MyClass.prototype.sameMethod = sameMethod; 281 | MyClass.prototype.aReqMethod = Requires.requiredMethod; 282 | MyClass.prototype.aMethod = aMethod; 283 | 284 | sut.setParameter([TraitA]); 285 | 286 | it('does not generate a conflict if both method are the same.', function(){ 287 | expect(function(){ 288 | sut.process(MyClass); 289 | }).not.to.throw(Error, /aMethod is defined in trait and Class/); 290 | expect(MyClass).to.respondTo('sameMethod'); 291 | expect(MyClass.prototype.sameMethod).to.be.equal(sameMethod); 292 | }); 293 | 294 | it('does not generate a conflict if the method is already defined by the class but it is a required one from trait.', function(){ 295 | expect(function(){ 296 | sut.process(MyClass); 297 | }).not.to.throw(Error, /aMethod is defined in trait and Class/); 298 | expect(MyClass).to.respondTo('aMethod'); 299 | expect(MyClass.prototype.aMethod).to.not.be.equal(Requires.requiredMethod); 300 | expect(MyClass.prototype.aMethod).to.be.equal(aMethod); 301 | }); 302 | 303 | it('does not generate a conflict if the method is already defined by the class and it is a required one from another trait.', function(){ 304 | expect(function(){ 305 | sut.process(MyClass); 306 | }).not.to.throw(Error, /aMethod is defined in trait and Class/); 307 | expect(MyClass).to.respondTo('aReqMethod'); 308 | expect(MyClass.prototype.aReqMethod).to.not.be.equal(Requires.requiredMethod); 309 | expect(MyClass.prototype.aReqMethod).to.be.equal(anotherMethod); 310 | }); 311 | 312 | }); 313 | 314 | describe('Non Conflicts between TraitA and MyClass with alias', function(){ 315 | var sut = new Traits(), 316 | TraitA = function(){}, 317 | MyClass = function(){}, 318 | sameMethod = function(){}, 319 | anotherMethod = function anotherMethod(){}; 320 | 321 | TraitA.prototype.sameMethod = sameMethod; 322 | TraitA.prototype.aMethod = Requires.requiredMethod; 323 | TraitA.prototype.aReqMethod = anotherMethod; 324 | 325 | 326 | MyClass.prototype.sameMethod = function() {}; 327 | sut.setParameter([{trait: TraitA, alias: {sameMethod: 'myAnother'}}]); 328 | 329 | it('does not generate conflict if the method is aliased in the host class', function(){ 330 | expect(function(){ 331 | sut.process(MyClass); 332 | }).not.to.throw(Error, /aMethod is defined in trait and Class/); 333 | expect(MyClass).to.respondTo('sameMethod'); 334 | expect(MyClass).to.respondTo('myAnother'); 335 | }); 336 | 337 | }); 338 | 339 | describe('Non Conflicts between TraitA and MyClass with excluded methods', function(){ 340 | var sut = new Traits(), 341 | TraitA = function(){}, 342 | MyClass = function(){}, 343 | sameMethod = function(){}, 344 | anotherMethod = function anotherMethod(){}; 345 | 346 | TraitA.prototype.sameMethod = sameMethod; 347 | TraitA.prototype.aMethod = Requires.requiredMethod; 348 | TraitA.prototype.aReqMethod = anotherMethod; 349 | 350 | 351 | MyClass.prototype.sameMethod = function() {}; 352 | sut.setParameter([{trait: TraitA, excludes: ['sameMethod']}]); 353 | 354 | it('does not generate conflict if the method is aliased in the host class', function(){ 355 | expect(function(){ 356 | sut.process(MyClass); 357 | }).not.to.throw(Error, /aMethod is defined in trait and Class/); 358 | expect(MyClass).to.respondTo('sameMethod'); 359 | }); 360 | 361 | }); 362 | 363 | describe('Conflicts between TraitA and MyClass', function(){ 364 | var sut = new Traits(), 365 | TraitA = function(){}, 366 | MyClass = function(){}, 367 | aMethod = function method(){}, 368 | anotherMethod = function anotherMethod(){}; 369 | 370 | TraitA.prototype.aMethod = aMethod; 371 | TraitA.prototype.anotherMethod = anotherMethod; 372 | 373 | MyClass.prototype.aMethod = function myMethod(){}; 374 | 375 | sut.setParameter([TraitA]); 376 | 377 | it('generates a conflict if a method defined in TraitA is already defined by MyClass.', function(){ 378 | expect(function(){ 379 | sut.process(MyClass); 380 | }).to.throw(Error, /aMethod is defined in trait and Class/); 381 | }); 382 | 383 | }); 384 | 385 | }); 386 | }); 387 | -------------------------------------------------------------------------------- /test/unit/processor/sequence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect, 4 | sequence = require('../../../lib/processor/sequence'); 5 | 6 | describe('sequence', function(){ 7 | 8 | it('NO_OP should be -1', function(){ 9 | expect(sequence.NO_OP).to.equal(-1); 10 | }); 11 | 12 | it('EXTENDS, PROPERTIES, REQUIRES, MERGE, TRAITS, ANNOTATION and EXPORTS priority should be defined and bigger than 0', function(){ 13 | expect(sequence.EXTENDS).to.be.above(0); 14 | expect(sequence.PROPERTIES).to.be.above(0); 15 | expect(sequence.MERGE).to.be.above(0); 16 | expect(sequence.REQUIRES).to.be.above(0); 17 | expect(sequence.TRAITS).to.be.above(0); 18 | expect(sequence.ANNOTATION).to.be.above(0); 19 | expect(sequence.EXPORTS).to.be.above(0); 20 | }); 21 | 22 | it('EXTENDS has priority over PROPERTIES, REQUIRES, MERGE, TRAITS, ANNOTATION and EXPORTS', function(){ 23 | expect(sequence.PROPERTIES).to.be.above(sequence.EXTENDS); 24 | expect(sequence.REQUIRES).to.be.above(sequence.EXTENDS); 25 | expect(sequence.MERGE).to.be.above(sequence.EXTENDS); 26 | expect(sequence.TRAITS).to.be.above(sequence.EXTENDS); 27 | expect(sequence.ANNOTATION).to.be.above(sequence.EXTENDS); 28 | expect(sequence.EXPORTS).to.be.above(sequence.EXTENDS); 29 | }); 30 | 31 | it('PROPERTIES has priority over REQUIRES, MERGE, TRAITS, ANNOTATION and EXPORTS', function(){ 32 | expect(sequence.REQUIRES).to.be.above(sequence.PROPERTIES); 33 | expect(sequence.MERGE).to.be.above(sequence.PROPERTIES); 34 | expect(sequence.TRAITS).to.be.above(sequence.PROPERTIES); 35 | expect(sequence.ANNOTATION).to.be.above(sequence.PROPERTIES); 36 | expect(sequence.EXPORTS).to.be.above(sequence.PROPERTIES); 37 | }); 38 | 39 | it('REQUIRES has priority over MERGE, TRAITS, ANNOTATION and EXPORTS', function(){ 40 | expect(sequence.MERGE).to.be.above(sequence.REQUIRES); 41 | expect(sequence.TRAITS).to.be.above(sequence.REQUIRES); 42 | expect(sequence.ANNOTATION).to.be.above(sequence.REQUIRES); 43 | expect(sequence.EXPORTS).to.be.above(sequence.REQUIRES); 44 | }); 45 | 46 | it('MERGE has priority over TRAITS, ANNOTATION and EXPORTS', function(){ 47 | expect(sequence.TRAITS).to.be.above(sequence.MERGE); 48 | expect(sequence.ANNOTATION).to.be.above(sequence.MERGE); 49 | expect(sequence.EXPORTS).to.be.above(sequence.MERGE); 50 | 51 | }); 52 | 53 | it('TRAITS has priority over ANNOTATION and EXPORTS', function(){ 54 | expect(sequence.ANNOTATION).to.be.above(sequence.TRAITS); 55 | expect(sequence.EXPORTS).to.be.above(sequence.TRAITS); 56 | }); 57 | 58 | it('ANNOTATION has priority over EXPORTS', function(){ 59 | expect(sequence.EXPORTS).to.be.above(sequence.ANNOTATION); 60 | }); 61 | 62 | }); 63 | --------------------------------------------------------------------------------