├── .babelrc ├── .eslintrc ├── .gitignore ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .versions ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── circle.yml ├── docs └── images │ └── space-brand-mood.jpg ├── package.js ├── source ├── application.coffee ├── configuration.js ├── error.js ├── helpers.coffee ├── injector.coffee ├── injector_annotations.coffee ├── lib │ └── underscore_deep_extend_mixin.js ├── logger.js ├── module.coffee ├── namespace.coffee ├── object.coffee ├── struct.coffee └── testing │ └── bdd-api.coffee ├── test.sh └── tests ├── integration ├── application_with_modules.spec.js ├── lifecycle_hooks.tests.js ├── module.regressions.js ├── requiring-modules.tests.js └── standalone_application.integration.coffee └── unit ├── application.unit.coffee ├── error.tests.js ├── helpers.unit.coffee ├── injector.unit.coffee ├── injector_annotations.unit.js ├── logger.tests.js ├── module.unit.coffee ├── object.unit.coffee └── struct.unit.coffee /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-decorators-legacy"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | /** 2 | * 0 - turn the rule off 3 | * 1 - turn the rule on as a warning (doesn't affect exit code) 4 | * 2 - turn the rule on as an error (exit code will be 1) 5 | * 6 | * Meteor Style Guide: https://github.com/meteor/meteor/wiki/Meteor-Style-Guide 7 | * 8 | */ 9 | 10 | { 11 | "parser": "babel-eslint", 12 | "env": { 13 | "browser": true, 14 | "node": true 15 | }, 16 | "ecmaFeatures": { 17 | "arrowFunctions": true, 18 | "blockBindings": true, 19 | "classes": true, 20 | "defaultParams": true, 21 | "destructuring": true, 22 | "forOf": true, 23 | "generators": false, 24 | "modules": true, 25 | "objectLiteralComputedProperties": true, 26 | "objectLiteralDuplicateProperties": false, 27 | "objectLiteralShorthandMethods": true, 28 | "objectLiteralShorthandProperties": true, 29 | "spread": true, 30 | "superInFunctions": true, 31 | "templateStrings": true, 32 | "jsx": true 33 | }, 34 | "rules": { 35 | /** 36 | * Strict mode 37 | */ 38 | // babel inserts "use strict"; for us 39 | // http://eslint.org/docs/rules/strict 40 | "strict": 0, 41 | 42 | /** 43 | * ES6 44 | */ 45 | "no-var": 1, // http://eslint.org/docs/rules/no-var 46 | 47 | /** 48 | * Variables 49 | */ 50 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 51 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 52 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 53 | "vars": "local", 54 | "args": "after-used" 55 | }], 56 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define 57 | 58 | /** 59 | * Possible errors 60 | */ 61 | "comma-dangle": [1, "never"], // http://eslint.org/docs/rules/comma-dangle 62 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 63 | "no-console": 1, // http://eslint.org/docs/rules/no-console 64 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 65 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 66 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 67 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 68 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 69 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 70 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 71 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 72 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 73 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 74 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 75 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 76 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 77 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 78 | "quote-props": [2, "as-needed", { "keywords": true, "unnecessary": false }], // http://eslint.org/docs/rules/quote-props (previously known as no-reserved-keys) 79 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 80 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 81 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 82 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 83 | 84 | /** 85 | * Best practices 86 | */ 87 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 88 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 89 | "default-case": 2, // http://eslint.org/docs/rules/default-case 90 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 91 | "allowKeywords": true 92 | }], 93 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 94 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 95 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 96 | //"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 97 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 98 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 99 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 100 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 101 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 102 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 103 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 104 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 105 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 106 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 107 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 108 | "no-new": 2, // http://eslint.org/docs/rules/no-new 109 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 110 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 111 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 112 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 113 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 114 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 115 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 116 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 117 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 118 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 119 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 120 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 121 | "no-with": 2, // http://eslint.org/docs/rules/no-with 122 | "radix": 2, // http://eslint.org/docs/rules/radix 123 | "vars-on-top": 1, // http://eslint.org/docs/rules/vars-on-top 124 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 125 | "yoda": 2, // http://eslint.org/docs/rules/yoda 126 | "max-len": [1, 200, 2], // http://eslint.org/docs/rules/max-len 127 | 128 | /** 129 | * Style 130 | */ 131 | "indent": [2, 2, {"VariableDeclarator": 2}], // http://eslint.org/docs/rules/indent 132 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 133 | "1tbs", { 134 | "allowSingleLine": true 135 | }], 136 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 137 | "properties": "never" 138 | }], 139 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 140 | "before": false, 141 | "after": true 142 | }], 143 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 144 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 145 | "func-names": 0, // http://eslint.org/docs/rules/func-names 146 | "func-style": [2, "expression"], // http://eslint.org/docs/rules/func-style 147 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 148 | "beforeColon": false, 149 | "afterColon": true 150 | }], 151 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 152 | "newIsCap": true, 153 | "capIsNew": false 154 | }], 155 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 156 | "max": 2 157 | }], 158 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 159 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 160 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor 161 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 162 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 163 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 164 | "one-var": [1, "never"], // http://eslint.org/docs/rules/one-var 165 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 166 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 167 | "before": false, 168 | "after": true 169 | }], 170 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 171 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 172 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 173 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 174 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 175 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment (previously known as spaced-line-comment) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .DS_Store 3 | .idea 4 | -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "getenv": { 4 | "version": "0.5.0" 5 | }, 6 | "winston": { 7 | "version": "2.1.0", 8 | "dependencies": { 9 | "async": { 10 | "version": "1.0.0" 11 | }, 12 | "colors": { 13 | "version": "1.0.3" 14 | }, 15 | "cycle": { 16 | "version": "1.0.3" 17 | }, 18 | "eyes": { 19 | "version": "0.1.8" 20 | }, 21 | "isstream": { 22 | "version": "0.1.2" 23 | }, 24 | "pkginfo": { 25 | "version": "0.3.1" 26 | }, 27 | "stack-trace": { 28 | "version": "0.0.9" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.2 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-tools@1.0.4 8 | boilerplate-generator@1.0.4 9 | caching-compiler@1.0.0 10 | caching-html-compiler@1.0.2 11 | callback-hook@1.0.4 12 | check@1.1.0 13 | coffeescript@1.0.11 14 | ddp@1.2.2 15 | ddp-client@1.2.1 16 | ddp-common@1.2.2 17 | ddp-rate-limiter@1.0.0 18 | ddp-server@1.2.2 19 | deps@1.0.9 20 | diff-sequence@1.0.1 21 | ecmascript@0.1.6 22 | ecmascript-runtime@0.2.6 23 | ejson@1.0.7 24 | email@1.0.8 25 | geojson-utils@1.0.4 26 | grigio:babel@0.1.3 27 | html-tools@1.0.5 28 | htmljs@1.0.5 29 | id-map@1.0.4 30 | jquery@1.11.4 31 | local-test:space:base@4.1.3 32 | localstorage@1.0.5 33 | logging@1.0.8 34 | meteor@1.1.10 35 | minifiers@1.1.7 36 | minimongo@1.0.10 37 | mongo@1.1.3 38 | mongo-id@1.0.1 39 | npm-mongo@1.4.39_1 40 | observe-sequence@1.0.7 41 | ordered-dict@1.0.4 42 | practicalmeteor:chai@2.1.0_1 43 | practicalmeteor:loglevel@1.2.0_2 44 | practicalmeteor:munit@2.1.5 45 | practicalmeteor:sinon@1.14.1_2 46 | promise@0.5.1 47 | random@1.0.5 48 | rate-limit@1.0.0 49 | reactive-dict@1.1.3 50 | reactive-var@1.0.6 51 | retry@1.0.4 52 | routepolicy@1.0.6 53 | service-configuration@1.0.5 54 | session@1.1.1 55 | space:base@4.1.3 56 | space:testing@3.0.1 57 | spacebars@1.0.7 58 | spacebars-compiler@1.0.7 59 | templating@1.1.5 60 | templating-tools@1.0.0 61 | test-helpers@1.0.5 62 | tinytest@1.0.6 63 | tracker@1.0.9 64 | ui@1.0.8 65 | underscore@1.0.4 66 | webapp@1.2.3 67 | webapp-hashing@1.0.5 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | ## 4.1.4 4 | - Removes deprecated `grigio:babel@0.1.3` meteor dependency. It was causing errors related to long or failed builds (like: `Cannot enlarge memory arrays`) 5 | 6 | ## 4.1.3 7 | - Fixes the BDD test helper on `Space.Module` and `Space.Application` to actually accept a second param that can be a existing app instance. 8 | 9 | ## 4.1.2 10 | - Fixes superClass method for classes that are not directly extending Space.Object 11 | 12 | ## 4.1.1 13 | - Improves the way mixins are applied to classes and ensures that: 14 | - Sub classes always inherit all mixins applied to parent classes, even when this happens "later on" 15 | - All sub classes inherit the static mixin properties, even after extending the base class 16 | 17 | - Adds the following helpers are added to `Space.Object` for convenience: 18 | - **static** 19 | - `MyClass.hasSuperClass()` 20 | - `MyClass.superClass([key])` - either returns the super class constructor or static property/method 21 | - `MyClass.subClasses()` - returns flat array of all sub classes 22 | - **prototype** 23 | - `myInstance.hasSuperClass()` 24 | - `myInstance.superClass([key])` - same as static version but returns prototype props/methods 25 | 26 | ## 4.1.0 27 | - Adds `hasMixin()` instance method to Space.Object to check if the class has applied or inherited a specific mixin 28 | 29 | ### 4.0.2 30 | - `Space.Struct` were previously not calling their super constructor which caused a bug with the new `onConstruction` hooks 31 | 32 | ### 4.0.1 33 | 34 | - Improves `Space.Object.extend()` implementations 35 | - Fixes bug in `Space.Error` 36 | - Adds tests for mixin callbacks 37 | 38 | ## 4.0.0 :tada: 39 | **This is a big major release with many new features and improvements:** 40 | 41 | - Adds extensible `Space.Error` class that has the same api as `Space.Object` but can 42 | be used for better custom error classes. 43 | ```javascript 44 | // When throwing this: "MyError: The default message for this error" 45 | let MyError = Space.Error.extend('MyError', { 46 | message: 'The default message for this error' 47 | }); 48 | // When throwing this: "MyParamError: Custom message with " 49 | let MyError = Space.Error.extend('MyParamError', { 50 | Constructor: (param) { 51 | Space.Error.call(this, `Custom message with ${param}`); 52 | } 53 | }); 54 | ``` 55 | 56 | - Improves mixins for `Space.Object` by making `onDependenciesReady` hooks 57 | available. All mixins can now define their own `onDependenciesReady` methods 58 | which all get called by the host class correctly. This way mixins can do setup 59 | work on host classes. 60 | ```javascript 61 | My.CustomMixin = { 62 | onDependenciesReady() { // Do some setup work } 63 | }; 64 | My.CustomClass.mixin(My.CustomMixin); 65 | // Of course normally you dont have to call `onDependenciesReady` yourself 66 | new My.CustomClass().onDependenciesReady() // Mixin hook is called! 67 | ``` 68 | 69 | - You can now add mixins to any subclass of `Space.Object` like this: 70 | ```javascript 71 | Space.Object.extend('CustomClass', { mixin: [FirstMixin, SecondMixin] }); 72 | ``` 73 | 74 | - Mixin methods now override methods on the host class! 75 | 76 | - Many bugfixes and improvements related to `Space.Module` lifecycle hooks. 77 | Previous hooks like `onStart` and `onConfigure` have been replaced with a complete 78 | lifecycle split into three main phases: `initialize`, `start`, `reset`. Each 79 | with `on`, `before` and `after` hooks like `onInitialize` / `afterStart` etc. 80 | 81 | - Refactored all core api properties like `Dependencies`, `RequiredModules`, 82 | `Singletons` etc. to lowercase pendants: e.g `dependencies`, `requiredModules` 83 | 84 | - Added `Space.Logger`, an extensible logger based on [winston](https://www.npmjs.com/package/winston) 85 | available in your application as `dependencies: { log: 'log' }` 86 | 87 | - You can now do static setup work on classes like this: 88 | ```javascript 89 | Space.Object.extend('CustomClass', { 90 | onExtending() { 91 | // is the class itself here! 92 | } 93 | }); 94 | ``` 95 | 96 | - Adds static `Space.Object.isSubclassOf` method that is available for all 97 | sub classes automatically. So you can ask `MyFirstClass.isSubclassOf(OtherClass)` 98 | 99 | - Improved error handling and messages of `Space.Injector` (DI System) 100 | 101 | - `Space.Struct` can now be serialized `toData` and `Space.Struct.fromData` 102 | which has some benefits over EJSON: all fields are plain and accessible for 103 | queries! 104 | 105 | - New recommended way to define classes with full class path for improved 106 | debugging and automatic type registration (EJSON / toData): 107 | ```javascript 108 | // Instead of: 109 | Space.Object.extend(My.namespace, 'MyCustomClass'); 110 | // Do this now: 111 | Space.Object.extend('My.namespace.MyCustomClass'); 112 | ``` 113 | 114 | - Added proper MIT license 115 | 116 | - Added BDD test helper `Space.Module.registerBddApi` 117 | 118 | *Thanks to all the new people on the team who made this awesome release possible:* 119 | 120 | - [Rhys Bartels-Waller](https://github.com/rhyslbw) 121 | - [Darko Mijić](https://github.com/darko-mijic) 122 | - [Adam Desivi](https://github.com/qejk) 123 | - [Jonas Aschenbrenner](https://github.com/Sanjo) 124 | 125 | :clap: 126 | 127 | ## 3.2.1 128 | - Bug fixes relating to package configuration 129 | 130 | ## 3.2.0 131 | **Module/Application state and lifecycle improvements** 132 | - Formalizes state to better manage module lifecycle 133 | - States include: Constructed -> Initialized -> Stopped -> Running 134 | - Accessor method `app.is(expectedState) // eg 'running'` 135 | - Calling .reset() on a running Application or Module now calls .stop() 136 | on it first, then start() again after it's been reset. 137 | 138 | **Configuration API Support** 139 | - `Space.getenv` helper wraps [getenv](https://www.npmjs.com/package/getenv) to provide support for ENV typecasting, particularly for merging with runtime config 140 | 141 | **Other Changes** 142 | - Default Meteor mappings have been moved down to Module from Application 143 | - `MongoInternals` is now mapped on the server 144 | 145 | **Bugfixes** 146 | - Minor injector fixes, with better error handling. 147 | 148 | ### 3.1.1 149 | - Fixes bug with recently updated injector code 150 | 151 | ### 3.1.0 152 | - Fixed bug with module lifecycle hooks 153 | - Improved `Space.Object` mixin api to make it possible to mixin static class 154 | properties via the special `Static` property and to do static setup when the 155 | mixin is applied by providing a `onMixinApplied` method in the mixin definition. 156 | Both properties are not added to the prototype of course. The `onMixinApplied` 157 | method is called with the host class as `this` context. 158 | 159 | ### 3.0.0 160 | Several breaking changes and API improvements have been made: 161 | - `ReactiveVar` is now mapped statically instead of `instancesOf`, so you 162 | can use it like the "normal" 163 | - The module/app lifecycle has been harmonized into a simple system: 164 | There are 4 lifecycle phases: `initialize`, `start`, `reset` and `stop`. 165 | For each phase there are `before`, `on`, and `after` hooks that are called 166 | in the logical order -> deepest nested module first. The `initialize` method 167 | is the only one that should not be called from outside but is automatically 168 | called when constructing a module/app. 169 | - The configuration API slightly changed again: The `configure` method is 170 | only defined on `Space.Application` and allows to override the default config of 171 | the app / modules. 172 | - The previous `configure` hooks does not exist anymore. You should use `onInitialize` 173 | instead. 174 | 175 | ### 2.5.1 176 | - Thanks @sanjo for a patch that fixes dependency injection if two components 177 | depend on each other. 178 | 179 | ### 2.5.0 180 | Several improvements and bug fixes have been made: 181 | - Keep the required min version of Meteor down to 1.0 182 | - Added `afterApplicationStart` hook for modules and apps that is called 183 | after everything has been configured and started. 184 | - Improved configuration api to allow overriding of arbitrarily nested 185 | configuration values. This is super awesome for overriding configs of 186 | modules. 187 | - Added `Space.Object#type` api for adding debug info about class types. 188 | - Added `stop` hook for modules and apps that is similar to `reset` but 189 | should be used for really drastic cleanup (like stopping observers etc.) 190 | 191 | ### 2.4.2 192 | Fixes some bugs with the new Meteor dependency tracker and the default 193 | dependency injections in Space applications. 194 | 195 | ### 2.4.1 196 | Introduces better way to configure modules and applications. Now you can 197 | define a default `Configuration` property on the prototype and override 198 | specific values of it when creating the application instance like this: 199 | ```javascript 200 | new TestApp({ 201 | Configuration: { 202 | propertyToOverride: 'customValue', 203 | } 204 | }); 205 | ``` 206 | All configurations are merged into a single `Configuration` object available 207 | via the application injector like this: `injector.get('Configuration')`. 208 | 209 | ### 2.4.0 210 | Introduces dynamic overriding of injected dependencies in the running system. 211 | Now you can call `injector.override('Test').to('newValue')` and all objects 212 | that had the previous value of `Test` injected will be updated with the new 213 | value dynamically. This makes it possible to swap out complete sub-systems 214 | while the application is running. Of course it also comes with the potential 215 | of memory leaks, because now the injection mappings have to hold references 216 | to each and every object that dependencies are injected to. That's where the 217 | new `Space.Injector::release` API comes to play. If you have an object that 218 | should be garbage collected but has dependencies injected you need to release 219 | it from the Injector to let the mappings remove the references. In reality you 220 | rarely have to manage this yourself because other packages like `space:ui` will 221 | handle this transparently in the background. 222 | 223 | ### 2.3.1 224 | Only updated the Github repository links after moving it to the new 225 | https://github.com/meteor-space organization. 226 | 227 | ### 2.3.0 228 | - Adds `Space.namespace('MyNamespace')` which simplifies working with Meteor 229 | package scopes. Up until now Space could not resolve exported variables from 230 | packages in your app because Meteor is hiding them from the `space:base` package. 231 | 232 | ### 2.2.0 233 | - Improves `Space.Object.extend` capabilities to work smoothly with Javascript 234 | and provide multiple ways to extend core classes. 235 | - Improves general debugging experience by throwing errors when values can 236 | not be resolved with `Space.resolvePath` 237 | - `Space.resolvePath` now also takes into account published space modules, which 238 | solves the problem of Meteor package scoping. 239 | - adds `Space.Module.define` and `Space.Application.define` which makes it easier 240 | to flesh out modules and apps in Javascript (without having to call `publish`) 241 | 242 | ### 2.1.0 243 | Introduces lazy auto-mapping of singletons and static values if they are 244 | requested by other parts via the `Dependencies` property. For the first request 245 | it looks up the dependency on the global context and maps functions as singletons 246 | and other values as static. Be aware that some part of the system has to require 247 | your singleton before it ever exists. In some cases this is not what you want 248 | (e.g event handling) 249 | 250 | ### 2.0.1 251 | @Sanjo fixed an issue with weak-dependencies injection in `Space.Application`. 252 | 253 | ### 2.0.0 254 | #### Breaking Changes: 255 | 256 | - Unified the API for starting `Space.Module` and `Space.Application`. This 257 | means you have to call `yourApp.start()` instead of `yourApp.run()` now. 258 | - Renamed the `run` hook for modules and applications to `startup`, so instead 259 | of defining `run: function() { … }` you need `startup: function() { … }` inside your modules and apps now. 260 | 261 | ### 1.4.2 262 | Make it possible to declare `Singletons: []` on any module with paths to classes 263 | that should be mapped and created as singletons automatically. 264 | 265 | ### 1.4.1 266 | Fixes some package dependency issues 267 | 268 | ### 1.4.0 269 | Adds basic mixin capabilities to Space.Object that allows to extend 270 | the prototype of a given class without direct inheritance 271 | 272 | ### 1.3.2 273 | Fixes bug with running dependent modules in correct order. 274 | 275 | ### 1.3.1 276 | Throw error when trying to map `undefined` or `null` value. 277 | 278 | ### 1.3.0 279 | Adds helpers specs and fixes edge case bug with resolve path helper 280 | 281 | ### 1.2.9 282 | Introduces general purpose helpers on the Space namespace 283 | 284 | ### 1.2.8 285 | Adds global value lookup for injection mapping support: e.g: 286 | `@injector.map('my.awesome.Class').asSingleton()` 287 | 288 | ### 1.2.7 289 | Support old js engines that without Object.defineProperty support. 290 | 291 | ### 1.2.6 292 | Fixes bug where `onDependenciesReady` was only called once per prototype 293 | 294 | ### 1.2.5 295 | Fixes bug where `onDependenciesReady` was called more than once 296 | 297 | ### 1.2.4 298 | Fixes bug where injected values were overwritten 299 | 300 | ### 1.2.4 301 | Fixes bug where injected values were overwritten 302 | 303 | ### 1.2.3 304 | Fixes regression bug where injector didn't inject into values 305 | 306 | ### 1.2.2 307 | Renames `Space.Class` to `Space.Object` 308 | 309 | ### 1.2.1 310 | #### Features: 311 | * Adds support for static constructor functions while extending classes 312 | * Completes API compatibility to dependance injector 313 | 314 | #### Bugfixes: 315 | * Providers are now added to the `Mapping` prototype, not to every instance. 316 | 317 | ### 1.2.0 318 | #### Features: 319 | * Adds `Space.Object` for better Javascript inheritance support 320 | * Replaces Dependance with brand new (but compatible) `Space.Injector` 321 | 322 | ### 1.1.0 323 | Added mappings to core Meteor packages 324 | 325 | ### 1.0.0 326 | Publish first version to Meteor package system 327 | 328 | ### 0.1.0 329 | Initial release of Space 330 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) <2016> 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 10 | of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 13 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Space Brand Mood](docs/images/space-brand-mood.jpg?raw=true) 2 | 3 | # SPACE [![Circle CI](https://circleci.com/gh/meteor-space/base.svg?style=svg)](https://circleci.com/gh/meteor-space/base) [![Join the chat at https://gitter.im/meteor-space/general](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/meteor-space/general?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | 6 | Modular application architecture for Meteor, with following goals: 7 | 8 | 1. Explicit dependencies in your code 9 | 2. Full control over configuration and initialization 10 | 3. Testability 11 | 12 | ## Why? 13 | As your Meteor app grows, you keep adding packages and dependencies 14 | to it and sprinkle your configuration and initialization logic into 15 | `Meteor.startup` blocks all over the code base. If you don't use *some* 16 | structure, you will end up throwing your laptop against the wall. 17 | 18 | ## 1. Explicit Dependencies 19 | Space comes with a lightweight dependency injection system. It tries 20 | to keep your code as clean as possible and doesn't force you to wrap your 21 | functions with library calls. 22 | 23 | If an object needs other code during runtime, it simply declares its 24 | **dependencies**: 25 | 26 | ```javascript 27 | var dependentObject = { 28 | dependencies: { 29 | lib: 'OtherCode' 30 | }, 31 | sayHello: function() { 32 | this.lib.sayHello(); 33 | } 34 | }; 35 | ``` 36 | Now `dependentObject` declares very explicitly that it needs `OtherCode` 37 | which it will access via `this.lib` later on. But where does `OtherCode` 38 | come from? 39 | 40 | This is where the `Space.Injector` helps out: 41 | 42 | ```javascript 43 | var library = { sayHello: function() { console.log('hello!'); } }; 44 | var injector = new Space.Injector(); 45 | 46 | // maps the string identifier 'OtherCode' to the library object 47 | injector.map('OtherCode').to(library); 48 | // injects all dependencies into the dependent object 49 | injector.injectInto(dependentObject); 50 | 51 | dependentObject.sayHello(); // logs: 'hello!' 52 | ``` 53 | Of course, this also works with Javascript constructors and prototypes: 54 | 55 | ```javascript 56 | var MyClass = function() {}; 57 | MyClass.prototype.dependencies = { lib: 'OtherCode' }; 58 | MyClass.prototype.sayHello = function() { this.lib.sayHello() }; 59 | 60 | var instance = new MyClass(); 61 | injector.injectInto(instance); 62 | instance.sayHello(); // logs: 'hello!' 63 | ``` 64 | 65 | This was just first glimpse into dependency injection, there many 66 | other ways to map your code and you can add your own too: 67 | 68 | **[Learn more about Space.Injector](https://github.com/CodeAdventure/meteor-space/wiki/Space.Injector)** 69 | 70 | ### Sidebar: Namespaces and Classes 71 | 72 | In the examples above we used plain Javascript, but Space comes bundled 73 | with a simple but powerful inheritance system with namespacing and classes: 74 | 75 | ```javascript 76 | var myApp = Space.namespace('myApp'); 77 | 78 | Space.Object.extend('myApp.MyBaseClass', { 79 | dependencies: { lib: 'OtherCode' }, 80 | sayHello: function() { this.lib.sayHello(); } 81 | }); 82 | 83 | MyBaseClass.extend('myApp.MySubClass', { 84 | name: '', 85 | sayHello: function() { 86 | myApp.MyBaseClass.prototype.sayHello.call(this); 87 | console.log('I am ' + this.name); 88 | } 89 | }); 90 | 91 | var instance = myApp.MySubClass.create({ name: 'Dominik' }); 92 | injector.injectInto(instance); 93 | instance.sayHello(); // logs: 'hello!' and 'I am Dominik' 94 | ``` 95 | 96 | This was just the very basic example, there are many other features 97 | that help you build awesome classes with Space: 98 | 99 | **[Learn more about Space.Object](https://github.com/CodeAdventure/meteor-space/wiki/Space.Object)** 100 | 101 | ## 2. Control over configuration and Initialization 102 | 103 | Ok, now you declared your dependencies and learned how to inject them. 104 | The next questions is: "Where should the mapping of string identifiers 105 | to actual implementations happen?". 106 | 107 | ### Applications 108 | 109 | Applications are the command center of your code. Here you configure 110 | and initialize all the different pieces: 111 | 112 | ```javascript 113 | var App = Space.Application.extend({ 114 | // This is automatically called on creation 115 | configure: function () { 116 | // every app has its own injector by default 117 | this.injector.map('ExternalLib').to(SomeLibrary); 118 | this.injector.map('MyDependentClass').toSingleton(MyClass); 119 | }, 120 | startup: function() { 121 | // Create the singleton instance of my class 122 | this.injector.create('MyDependentClass'); 123 | } 124 | }); 125 | 126 | app = new App() 127 | app.start(); // You decide when your app starts 128 | ``` 129 | 130 | because singletons are needed so often, there is even a much shorter way 131 | to express the above: 132 | 133 | ```javascript 134 | var app = Space.Application.create({ 135 | // Let the framework map and create the singleton instances for you 136 | singletons: ['MyDependentClass', 'MyOtherSingleton'] 137 | }); 138 | app.start(); // You decide when your app starts 139 | ``` 140 | 141 | ### Modules 142 | 143 | When your application grows, it will become tedious to setup everything 144 | in your main application. It's time to split up your code into modules! 145 | 146 | Modules work exactly like applications, in fact `Space.Application` 147 | inherits from `Space.Module`. However they don't create an injector 148 | for themselves, but use the one provided by the (single) application. 149 | This way, all modules within your app share the same injector. 150 | 151 | Modules declare which other modules they require and what runtime 152 | dependencies they have, by putting the special properties 153 | `requiredModules` and `dependencies` on their prototypes: 154 | 155 | ```javascript 156 | var MyModule = Space.Module.define('MyModule', { 157 | // Declare which other Space modules are require 158 | requiredModules: [ 159 | 'OtherModule', 160 | 'YetAnotherModule' 161 | ], 162 | // Declare injected runtime dependencies 163 | dependencies: { 164 | someService: 'OtherModule.SomeService', 165 | anotherService: 'YetAnotherModule.AnotherService' 166 | }, 167 | // This method is called by the Space framework after all 168 | // required modules are initialized and the dependencies 169 | // are resolved and injected into the instance of this module. 170 | onInitialize: function() { 171 | // Add mappings to the shared dependency injection system 172 | this.injector.map('MyModule.TestValue').to('test'); 173 | // Use required dependencies 174 | this.someService.doSomeMagic() 175 | this.anotherService.beAwesome() 176 | } 177 | }); 178 | ``` 179 | 180 | ### Creating Applications based on Modules 181 | 182 | ```javascript 183 | Space.Application.create({ 184 | // Applications also declare which modules they need: 185 | requiredModules: ['MyModule'], 186 | // And their runtime dependencies from required modules: 187 | dependencies: { 188 | testValue: 'MyModule.TestValue' 189 | }, 190 | // This is called when all required modules are configured. 191 | afterInitialize: function() { 192 | console.log(this.testValue); // logs 'test' (see module above) 193 | } 194 | }) 195 | ``` 196 | 197 | ### Configuring Modules and Applications 198 | 199 | You can define default configurations for each module and application and 200 | override any part of it when creating an application instance like here: 201 | 202 | ```javascript 203 | Space.Module.define('FirstModule', { 204 | configuration: { 205 | firstToChange: 'first', 206 | firstToKeep: 'first' 207 | } 208 | }); 209 | 210 | Space.Module.define('SecondModule', { 211 | requiredModules: ['FirstModule'], 212 | configuration: { 213 | secondToChange: 'second', 214 | secondToKeep: 'second' 215 | } 216 | }); 217 | 218 | TestApp = Space.Application.extend({ 219 | requiredModules: ['SecondModule'], 220 | configuration: { 221 | appConfigToChange: 'app', 222 | appConfigToKeep: 'app' 223 | } 224 | }); 225 | 226 | var app = new TestApp({ 227 | configuration: { 228 | firstToChange: 'firstChanged', 229 | secondToChange: 'secondChanged', 230 | appConfigToChange: 'appChanged' 231 | } 232 | }); 233 | 234 | expect(app.injector.get('configuration')).to.deep.equal({ 235 | firstToChange: 'firstChanged', 236 | firstToKeep: 'first', 237 | secondToChange: 'secondChanged', 238 | secondToKeep: 'second', 239 | appConfigToChange: 'appChanged', 240 | appConfigToKeep: 'app' 241 | }); 242 | ``` 243 | 244 | ## 3. Testability 245 | 246 | You may ask why you should deal with dependency injection if you can access your 247 | dependencies directly like this: 248 | 249 | ```javascript 250 | var Customer = function(id) { 251 | this.id = id; 252 | }; 253 | 254 | Customer.prototype.getPurchases = function() { 255 | return Purchases.find({ customerId: this.id }).fetch(); 256 | } 257 | ``` 258 | 259 | This works well, until you write your first unit tests. The problem is that this class 260 | directly references `Purchases` and there is only one way you can test this (sanely): 261 | 262 | By temporarily replacing `Purchases` globally with some mock/stub 263 | 264 | ```javascript 265 | describe('Customer.getPurchases', function() { 266 | 267 | beforeEach(function() { 268 | // Save a backup of the global Purchases collection 269 | this.savedPurchasesCollection = Purchases; 270 | // Replace the collection with an anonymous one 271 | Purchases = new Mongo.Collection(null); 272 | this.customerId = 'xyz'; 273 | }) 274 | 275 | afterEach(function() { 276 | // Restore the global purchases collection 277 | Purchases = this.savedPurchasesCollection; 278 | }) 279 | 280 | it('queries the purchases collection and returns fetched results', function() { 281 | // Prepare 282 | var testPurchase = { _id: '123', customerId: this.customerId }; 283 | Purchases.insert(testPurchase); 284 | // Test 285 | var customer = new Customer(this.customerId); 286 | var purchases = customer.getPurchases(); 287 | expect(purchases).to.deep.equal([testPurchase]); 288 | }) 289 | }) 290 | ``` 291 | 292 | In this example it does not look too bad but this pattern quickly becomes tedious 293 | if you have more than 1-2 dependencies you want to replace during your tests. 294 | 295 | Here is how you can write a test like this when using space: 296 | 297 | ```javascript 298 | var Customer = Space.Object.extend({ 299 | // Annotate your dependencies 300 | dependencies: { purchases: 'Purchases' }, 301 | id: null, 302 | getPurchases: function() { 303 | this.purchases.find({ customerId: this.id }).fetch(); 304 | } 305 | }); 306 | 307 | describe('Customer.getPurchases', function() { 308 | 309 | beforeEach(function() { 310 | this.customerId = 'xyz'; 311 | // Inject dependencies directly on creation 312 | this.customer = new Customer({ 313 | id: this.customerId, 314 | purchases: new Mongo.Collection(null) // dependency 315 | }); 316 | }) 317 | 318 | it 'queries the purchases collection and returns fetched results', function() { 319 | // Prepare 320 | testPurchase = { _id: '123', customerId: this.customerId }; 321 | this.customer.purchases.insert(testPurchase); 322 | // Test 323 | var purchases = customer.getPurchases(); 324 | expect(purchases).to.deep.equal([testPurchase]); 325 | }) 326 | }) 327 | ``` 328 | 329 | Since the `dependencies` property is just a simple prototype annotation that has 330 | no meaning outside the Space framework, you can just inject the dependencies 331 | yourself during the tests. This pattern works great, because your code remains 332 | completely framework agnostic (you could replace Space by any other DI framework 333 | or do it yourself). The positive side effect is that you explicitely declare your 334 | dependencies now. This helps you keep an eye on the complexity and coupling. If 335 | you realize that a class has more than 5 dependencies, it might be a good i 336 | ndicator that it is doing too much. 337 | 338 | ## Further Examples 339 | Look through the tests of this package to see all 340 | features that `space:base` provides for you. 341 | 342 | ## Migration Guide 343 | 344 | ### 3.2.1 → 4.0.0 345 | 346 | The 4.0.0 release has brought many small breaking changes and improvements. 347 | 348 | #### Lowercase API properties 349 | 350 | All api properties, *significant* to the framework are now like "normal" properties: 351 | ```javascript 352 | Space.Module.define('My.CustomModule', { 353 | requiredModules: ['My.OtherModule'], // instead of RequiredModules 354 | singletons: ['My.OtherModule'], // instead of Singletons 355 | }) 356 | Space.Object.extend('My.CustomClass', { 357 | dependencies: { /* … */ } // instead of Dependencies 358 | }) 359 | ``` 360 | 361 | #### Module Lifecycle Changes 362 | 363 | Previous hooks like `onStart` and `onConfigure` have been replaced with a complete 364 | lifecycle split into three main phases: `initialize`, `start`, `reset`. Each 365 | with `on`, `before` and `after` hooks like `onInitialize` / `afterStart` etc. 366 | 367 | ```javascript 368 | Space.Module.define('My.CustomModule', { 369 | onInitialize() {} // instead of "onConfigure" 370 | onStart() {} // same as previous "onStart" 371 | onReset() {} // did not exist before -> hook to reset collections etc. 372 | }) 373 | ``` 374 | 375 | #### New Class Registry 376 | 377 | There is a new recommended way to define classes with full class path for improved 378 | debugging and automatic type registration (EJSON / toData): 379 | ```javascript 380 | // Instead of: 381 | Space.Object.extend(My.namespace, 'MyCustomClass'); 382 | // Do this now: 383 | Space.Object.extend('My.namespace.MyCustomClass'); 384 | ``` 385 | 386 | ## Install 387 | `meteor add space:base` 388 | 389 | ## Run the tests 390 | `./test.sh` 391 | 392 | ## Release History 393 | You find all release changes in the [changelog](https://github.com/meteor-space/base/blob/master/CHANGELOG.md) 394 | 395 | ## License 396 | Licensed under the MIT license. 397 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.33 4 | pre: 5 | - curl https://install.meteor.com | /bin/sh 6 | dependencies: 7 | pre: 8 | - npm install -g spacejam 9 | test: 10 | override: 11 | - spacejam test-packages ./ 12 | -------------------------------------------------------------------------------- /docs/images/space-brand-mood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor-space/base/57f065de22b3dd406c3c0686c329808a6ac47768/docs/images/space-brand-mood.jpg -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: 'Modular Application Architecture for Meteor.', 3 | name: 'space:base', 4 | version: '4.1.4', 5 | git: 'https://github.com/meteor-space/base.git', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Npm.depends({ 10 | "getenv": "0.5.0", 11 | "winston": "2.1.0", 12 | "babel-plugin-transform-decorators-legacy": "1.3.4" 13 | }); 14 | 15 | Package.onUse(function(api) { 16 | 17 | api.versionsFrom('1.2.0.1'); 18 | 19 | api.use([ 20 | 'coffeescript', 21 | 'check', 22 | 'underscore', 23 | 'ecmascript' 24 | ]); 25 | 26 | api.use([ 27 | 'ejson', 28 | 'ddp', 29 | 'random', 30 | 'mongo', 31 | 'tracker', 32 | 'templating', 33 | 'session', 34 | 'blaze', 35 | 'email', 36 | 'accounts-base', 37 | 'reactive-var' 38 | ], {weak: true}); 39 | 40 | api.addFiles([ 41 | 'source/lib/underscore_deep_extend_mixin.js', 42 | 'source/namespace.coffee', 43 | 'source/helpers.coffee', 44 | 'source/configuration.js', 45 | 'source/object.coffee', 46 | 'source/logger.js', 47 | 'source/struct.coffee', 48 | 'source/error.js', 49 | 'source/injector.coffee', 50 | 'source/injector_annotations.coffee', 51 | 'source/module.coffee', 52 | 'source/application.coffee' 53 | ]); 54 | 55 | // Test helpers 56 | api.addFiles([ 57 | 'source/testing/bdd-api.coffee' 58 | ]); 59 | 60 | }); 61 | 62 | Package.onTest(function(api) { 63 | 64 | api.use([ 65 | 'meteor', 66 | 'coffeescript', 67 | 'check', 68 | 'ecmascript', 69 | 'space:base', 70 | 71 | // weak-dependencies 72 | 'ddp', 73 | 'random', 74 | 'underscore', 75 | 'mongo', 76 | 'tracker', 77 | 'templating', 78 | 'ejson', 79 | 'accounts-base', 80 | 'email', 81 | 'session', 82 | 'reactive-var', 83 | 'practicalmeteor:munit@2.1.5', 84 | 'space:testing@3.0.1' 85 | ]); 86 | 87 | api.addFiles([ 88 | 89 | // unit tests 90 | 'tests/unit/object.unit.coffee', 91 | 'tests/unit/module.unit.coffee', 92 | 'tests/unit/struct.unit.coffee', 93 | 'tests/unit/application.unit.coffee', 94 | 'tests/unit/injector.unit.coffee', 95 | 'tests/unit/injector_annotations.unit.js', 96 | 'tests/unit/helpers.unit.coffee', 97 | 'tests/unit/error.tests.js', 98 | 'tests/unit/logger.tests.js', 99 | 100 | // integration tests 101 | 'tests/integration/application_with_modules.spec.js', 102 | 'tests/integration/standalone_application.integration.coffee', 103 | 'tests/integration/lifecycle_hooks.tests.js', 104 | 'tests/integration/requiring-modules.tests.js', 105 | 'tests/integration/module.regressions.js' 106 | ]); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /source/application.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.Application extends Space.Module 3 | 4 | configuration: { 5 | appId: null 6 | } 7 | 8 | @define: (appName, prototype) -> 9 | prototype.toString = -> appName # For better debugging 10 | return @extend appName, prototype 11 | 12 | constructor: (options={}) -> 13 | super 14 | @modules = {} 15 | @configuration = options.configuration || {} 16 | @constructor.publishedAs = @constructor.name 17 | @initialize this, options.injector ? new Space.Injector() 18 | 19 | # Make it possible to override configuration (at any nested level) 20 | configure: (options) -> _.deepExtend @configuration, options 21 | -------------------------------------------------------------------------------- /source/configuration.js: -------------------------------------------------------------------------------- 1 | 2 | if (Meteor.isServer) { 3 | 4 | let getenv = Npm.require('getenv'); 5 | // Wrapper 6 | Space.getenv = getenv; 7 | 8 | Space.configuration = Space.getenv.multi({ 9 | log: { 10 | enabled: ['SPACE_LOG_ENABLED', false, 'bool'], 11 | minLevel: ['SPACE_LOG_MIN_LEVEL', 'info', 'string'] 12 | } 13 | }); 14 | 15 | // Pass down to the client 16 | _.deepExtend(Meteor.settings, { 17 | public: { 18 | log: { 19 | enabled: Space.configuration.log.enabled, 20 | minLevel: Space.configuration.log.minLevel 21 | } 22 | } 23 | }); 24 | 25 | __meteor_runtime_config__.PUBLIC_SETTINGS = Meteor.settings.public; 26 | 27 | } 28 | 29 | if (Meteor.isClient) { 30 | 31 | let log = Meteor.settings.public.log; 32 | 33 | // Guard and defaults when not loaded on server 34 | Space.configuration = { 35 | log: { 36 | enabled: log && log.enabled || false, 37 | minLevel: log && log.minLevel || 'info' 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /source/error.js: -------------------------------------------------------------------------------- 1 | let IntermediateInheritor = function() {}; 2 | IntermediateInheritor.prototype = Error.prototype; 3 | 4 | Space.Error = function(params) { 5 | this._invokeConstructionCallbacks.apply(this, arguments); 6 | let data = null; 7 | if (_.isString(params)) { 8 | data = { message: params }; 9 | } else if (_.isObject(params)) { 10 | data = params; 11 | } else { 12 | data = {}; 13 | } 14 | Space.Struct.call(this, this.extractErrorProperties(data)); 15 | return this; 16 | }; 17 | 18 | Space.Error.prototype = new IntermediateInheritor(); 19 | 20 | _.extend( 21 | Space.Error.prototype, // target 22 | Space.Struct.prototype, 23 | _.omit(Space.Object.prototype, 'toString'), 24 | { 25 | message: '', 26 | fields() { 27 | let fields = Space.Struct.prototype.fields.call(this); 28 | _.extend(fields, { 29 | name: String, 30 | message: String, 31 | stack: Match.Optional(String), 32 | code: Match.Optional(Match.Integer) 33 | }); 34 | return fields; 35 | }, 36 | extractErrorProperties(data) { 37 | let message = data.message ? data.message : this.message; 38 | let error = Error.call(this, message); 39 | data.name = error.name = this.constructor.name; 40 | data.message = error.message; 41 | if (error.stack !== undefined) data.stack = error.stack; 42 | return data; 43 | } 44 | } 45 | ); 46 | 47 | _.extend(Space.Error, _.omit(Space.Object, 'toString'), { 48 | __keepToStringMethod__: true // Do not override #toString method 49 | }); 50 | -------------------------------------------------------------------------------- /source/helpers.coffee: -------------------------------------------------------------------------------- 1 | 2 | global = this 3 | 4 | # Resolves a (possibly nested) path to a global object 5 | # Returns the object or null (if not found) 6 | Space.resolvePath = (path) -> 7 | if !path? then throw new Error "Cannot resolve invalid path <#{path}>" 8 | if path == '' then return global 9 | 10 | # If there is a direct reference just return it 11 | if Space.registry[path]? then return Space.registry[path] 12 | if Space.Module?.published[path]? then return Space.Module.published[path] 13 | parts = path.split '.' 14 | result = global # Start with global namespace 15 | for key in parts # Move down the object chain 16 | result = result?[key] ? null 17 | # Take published space modules into account 18 | # to solve the Meteor package scoping problem 19 | if !result? then throw new Error "Could not resolve path '#{path}'" 20 | return result 21 | 22 | Space.namespace = (id) -> Space.registry[id] = new Space.Namespace(id) 23 | 24 | Space.capitalizeString = (string) -> 25 | string.charAt(0).toUpperCase() + string.slice(1) 26 | -------------------------------------------------------------------------------- /source/injector.coffee: -------------------------------------------------------------------------------- 1 | 2 | Space.Error.extend(Space, 'InjectionError') 3 | 4 | class Space.Injector 5 | 6 | ERRORS: { 7 | cannotMapUndefinedId: -> 'Cannot map or .' 8 | mappingExists: (id) -> "<#{id}> would be overwritten. Use for that." 9 | noMappingFound: (id) -> "no mapping found for <#{id}>" 10 | cannotGetValueForUndefined: -> "Cannot get injection mapping for ." 11 | } 12 | 13 | constructor: (providers) -> 14 | @_mappings = {} 15 | @_providers = providers ? Injector.DEFAULT_PROVIDERS 16 | 17 | toString: -> 'Instance ' 18 | 19 | map: (id, override) -> 20 | if not id? 21 | throw new Space.InjectionError(@ERRORS.cannotMapUndefinedId()) 22 | mapping = @_mappings[id] 23 | # Avoid accidential override of existing mapping 24 | if mapping? and !override 25 | throw new Space.InjectionError(@ERRORS.mappingExists(id)) 26 | else if mapping? and override 27 | mapping.markForOverride() 28 | return mapping 29 | else 30 | @_mappings[id] = new Mapping id, @_providers 31 | return @_mappings[id] 32 | 33 | override: (id) -> @map id, true 34 | 35 | remove: (id) -> delete @_mappings[id] 36 | 37 | get: (id, dependentObject=null) -> 38 | if !id? 39 | throw new Space.InjectionError(@ERRORS.cannotGetValueForUndefined()) 40 | if not @_mappings[id]? 41 | throw new Space.InjectionError(@ERRORS.noMappingFound(id)) 42 | dependency = @_mappings[id].provide(dependentObject) 43 | @injectInto dependency 44 | return dependency 45 | 46 | create: (id) -> @get id 47 | 48 | injectInto: (value) -> 49 | unless _.isObject(value) and !value.__dependenciesInjected__ then return 50 | if Object.defineProperty? 51 | # Flag this object as injected 52 | Object.defineProperty value, '__dependenciesInjected__', 53 | enumerable: false 54 | configurable: false 55 | writable: false 56 | value: true 57 | else 58 | # support old engines without Object.defineProperty 59 | value.__dependenciesInjected__ = true 60 | # Get flat map of dependencies (possibly inherited) 61 | dependencies = @_mapDependencies value 62 | # Inject into dependencies to create the object graph 63 | for key, id of dependencies 64 | try 65 | value[key] ?= @get(id, value) 66 | catch error 67 | error.message += " for {#{key}: '#{id}'} in <#{value}>. Did you forget 68 | to map it in your application?" 69 | throw error 70 | # Notify when dependencies are ready 71 | if value.onDependenciesReady? then value.onDependenciesReady() 72 | 73 | addProvider: (name, provider) -> @_providers[name] = provider 74 | 75 | getMappingFor: (id) -> @_mappings[id] 76 | 77 | getIdForValue: (value) -> 78 | for id, mapping of @_mappings 79 | return id if mapping.getProvider().getValue() is value 80 | 81 | release: (dependent) -> 82 | for id, mapping of @_mappings 83 | mapping.release(dependent) if mapping.hasDependent(dependent) 84 | 85 | _mapDependencies: (value, deps={}) -> 86 | Class = value.constructor ? null 87 | SuperClass = Class.__super__ ? null 88 | # Recurse down the prototype chain 89 | if SuperClass? then @_mapDependencies SuperClass.constructor::, deps 90 | # Add dependencies of current value 91 | deps[key] = id for key, id of value.dependencies 92 | return deps 93 | 94 | _resolveValue: (path) -> Space.resolvePath path 95 | 96 | # ========= PRIVATE CLASSES ========== # 97 | 98 | class Mapping 99 | 100 | _id: null 101 | _provider: null 102 | _dependents: null 103 | _overrideInDependents: false 104 | 105 | constructor: (@_id, providers) -> 106 | @_dependents = [] 107 | @[key] = @_setup(provider) for key, provider of providers 108 | 109 | toString: -> 'Instance ' 110 | 111 | provide: (dependent) -> 112 | # Register depented objects for this mapping so that their 113 | # dependencies can overwritten later on. 114 | @_dependents.push(dependent)if dependent? and not @hasDependent(dependent) 115 | @_provider.provide() 116 | 117 | markForOverride: -> @_overrideInDependents = true 118 | 119 | hasDependent: (dependent) -> @getIndexOfDependee(dependent) > -1 120 | 121 | getIndexOfDependee: (dependent) -> @_dependents.indexOf(dependent) 122 | 123 | release: (dependent) -> @_dependents.splice(@getIndexOfDependee(dependent), 1) 124 | 125 | getId: -> @_id 126 | 127 | getProvider: -> @_provider 128 | 129 | _setup: (provider) -> 130 | return (value) => # We are inside an API call like injector.map('this').to('that') 131 | # Set the provider of this mapping to what the API user chose 132 | try 133 | @_provider = new provider @_id, value 134 | catch error 135 | error.message += " could not be found! Maybe you forgot to 136 | include a file in package.js?" 137 | throw error 138 | # Override the dependency in all dependent objects if this mapping is flagged 139 | if @_overrideInDependents 140 | # Get the value from the provider 141 | value = @_provider.provide() 142 | # Loop over the dependents 143 | for dependent in @_dependents 144 | # Loop over their dependencies and override the one this mapping 145 | # is managing if it exists (it should) 146 | dependencies = dependent.dependencies ? {} 147 | for key, id of dependencies 148 | if id is @_id 149 | dependent[key] = value 150 | dependent.onDependencyChanged?(key, value) 151 | 152 | # Reset the flag to override dependencies 153 | @_overrideInDependents = false 154 | 155 | # ========= DEFAULT PROVIDERS ======== # 156 | 157 | class Provider 158 | 159 | _id: null 160 | _value: null 161 | 162 | constructor: (@_id, @_value) -> 163 | 164 | getValue: -> @_value 165 | 166 | class ValueProvider extends Provider 167 | 168 | constructor: -> 169 | super 170 | if not @_value? 171 | if (typeof @_id is 'string') 172 | @_value = Space.resolvePath(@_id) 173 | else 174 | @_value = @_id 175 | 176 | toString: -> 'Instance ' 177 | 178 | provide: -> @_value 179 | 180 | class InstanceProvider extends Provider 181 | 182 | toString: -> 'Instance ' 183 | 184 | provide: -> new @_value() 185 | 186 | class SingletonProvider extends Provider 187 | 188 | _singleton: null 189 | 190 | constructor: -> 191 | super 192 | if not @_value? then @_value = @_id 193 | if typeof(@_value) is 'string' then @_value = Space.resolvePath(@_value) 194 | 195 | toString: -> 'Instance ' 196 | 197 | provide: -> 198 | if not @_singleton? then @_singleton = new @_value() 199 | return @_singleton 200 | 201 | Space.Injector.DEFAULT_PROVIDERS = 202 | 203 | to: ValueProvider 204 | toStaticValue: ValueProvider 205 | asStaticValue: ValueProvider 206 | toClass: InstanceProvider 207 | toInstancesOf: InstanceProvider 208 | asSingleton: SingletonProvider 209 | toSingleton: SingletonProvider 210 | -------------------------------------------------------------------------------- /source/injector_annotations.coffee: -------------------------------------------------------------------------------- 1 | @Space.Dependency = (propertyName, dependencyId) -> 2 | if (typeof dependencyId == 'undefined') 3 | dependencyId = propertyName 4 | (target) -> 5 | if target.prototype.dependencies and not target.prototype.hasOwnProperty('Dependencies') 6 | target.prototype.dependencies = _.clone target.prototype.dependencies 7 | target.prototype.dependencies ?= {} 8 | target.prototype.dependencies[propertyName] = dependencyId 9 | return target 10 | 11 | @Space.RequireModule = (moduleId) -> 12 | (target) -> 13 | if target.prototype.requiredModules and not target.prototype.hasOwnProperty('RequiredModules') 14 | target.prototype.requiredModules = _.clone target.prototype.requiredModules 15 | target.prototype.requiredModules ?= [] 16 | target.prototype.requiredModules.push moduleId 17 | return target 18 | -------------------------------------------------------------------------------- /source/lib/underscore_deep_extend_mixin.js: -------------------------------------------------------------------------------- 1 | // Deep object extend for underscore 2 | // As found on http://stackoverflow.com/a/29563346 3 | 4 | let deepObjectExtend = function(target, source) { 5 | for (let prop in source) { 6 | if (source.hasOwnProperty(prop)) { 7 | if (target[prop] && typeof source[prop] === 'object') { 8 | deepObjectExtend(target[prop], source[prop]); 9 | } else { 10 | target[prop] = source[prop]; 11 | } 12 | } 13 | } 14 | return target; 15 | }; 16 | 17 | _.mixin({ 'deepExtend': deepObjectExtend }); 18 | -------------------------------------------------------------------------------- /source/logger.js: -------------------------------------------------------------------------------- 1 | let config = Space.configuration; 2 | 3 | if (Meteor.isServer) { 4 | winston = Npm.require('winston'); 5 | } 6 | 7 | Space.Object.extend(Space, 'Logger', { 8 | 9 | _logger: null, 10 | _minLevel: 6, 11 | _state: 'stopped', 12 | 13 | _levels: { 14 | 'error': 3, 15 | 'warning': 4, 16 | 'warn': 4, 17 | 'info': 6, 18 | 'debug': 7 19 | }, 20 | 21 | Constructor() { 22 | if (Meteor.isServer) { 23 | this._logger = new winston.Logger({ 24 | transports: [ 25 | new winston.transports.Console({ 26 | colorize: true, 27 | prettyPrint: true 28 | }) 29 | ] 30 | }); 31 | this._logger.setLevels(winston.config.syslog.levels); 32 | } 33 | if (Meteor.isClient) { 34 | this._logger = console; 35 | } 36 | }, 37 | 38 | setMinLevel(name) { 39 | let newCode = this._levelCode(name); 40 | if (this._minLevel !== newCode) { 41 | this._minLevel = newCode; 42 | if (Meteor.isServer) { 43 | this._logger.transports.console.level = name; 44 | } 45 | } 46 | }, 47 | 48 | start() { 49 | if (this._is('stopped')) { 50 | this._state = 'running'; 51 | } 52 | }, 53 | 54 | stop() { 55 | if (this._is('running')) { 56 | this._state = 'stopped'; 57 | } 58 | }, 59 | 60 | debug(message) { 61 | check(message, String); 62 | this._log('debug', arguments); 63 | }, 64 | 65 | info(message) { 66 | check(message, String); 67 | this._log('info', arguments); 68 | }, 69 | 70 | warning(message) { 71 | check(message, String); 72 | if (Meteor.isClient) 73 | this._log('warn', arguments); 74 | if (Meteor.isServer) 75 | this._log('warning', arguments); 76 | }, 77 | 78 | error(message) { 79 | check(message, String); 80 | this._log('error', arguments); 81 | }, 82 | 83 | _levelCode(name) { 84 | return this._levels[name]; 85 | }, 86 | 87 | _is(expectedState) { 88 | if (this._state === expectedState) return true; 89 | }, 90 | 91 | _log(level, message) { 92 | if(this._is('running') && this._levelCode(level) <= this._minLevel) { 93 | this._logger[level].apply(this._logger, message); 94 | } 95 | } 96 | 97 | }); 98 | 99 | Space.log = new Space.Logger(); 100 | 101 | if (config.log.enabled) { 102 | Space.log.setMinLevel(config.log.minLevel); 103 | Space.log.start(); 104 | } 105 | -------------------------------------------------------------------------------- /source/module.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.Module extends Space.Object 3 | 4 | ERRORS: { 5 | injectorMissing: 'Instance of Space.Injector needed to initialize module.' 6 | } 7 | 8 | configuration: {} 9 | requiredModules: null 10 | # An array of paths to classes that you want to become 11 | # singletons in your application e.g: ['Space.messaging.EventBus'] 12 | # these are automatically mapped and created on `app.run()` 13 | singletons: [] 14 | injector: null 15 | _state: 'constructed' 16 | 17 | constructor: -> 18 | super 19 | @requiredModules ?= [] 20 | 21 | initialize: (@app, @injector, isSubModule=false) -> 22 | return if not @is('constructed') # only initialize once 23 | if not @injector? then throw new Error @ERRORS.injectorMissing 24 | @_state = 'configuring' 25 | Space.log.debug("#{@constructor.publishedAs}: initialize") 26 | # Setup basic mappings required by all modules if this the top-level module 27 | unless isSubModule 28 | @injector.map('Injector').to @injector 29 | @_mapSpaceServices() 30 | @_mapMeteorApis() 31 | 32 | # Setup required modules 33 | for moduleId in @requiredModules 34 | # Create a new module instance if not already registered with the app 35 | unless @app.modules[moduleId]? 36 | moduleClass = Space.Module.require(moduleId, this.constructor.name) 37 | @app.modules[moduleId] = new moduleClass() 38 | # Initialize required module 39 | module = @app.modules[moduleId] 40 | module.initialize(@app, @injector, true) 41 | 42 | # Merge in own configuration to give the chance for overwriting. 43 | if isSubModule 44 | _.deepExtend(@app.configuration, @configuration) 45 | @configuration = @app.configuration 46 | else 47 | # The app can override all other modules 48 | _.deepExtend(@configuration, @constructor.prototype.configuration) 49 | 50 | # Provide lifecycle hook before any initialization has been done 51 | @beforeInitialize?() 52 | # Give every module access Npm 53 | if Meteor.isServer then @npm = Npm 54 | # Top-level module 55 | if not isSubModule 56 | @injector.map('configuration').to(@configuration) 57 | @_runOnInitializeHooks() 58 | @_autoMapSingletons() 59 | @_autoCreateSingletons() 60 | @_runAfterInitializeHooks() 61 | 62 | start: -> 63 | if @is('running') then return 64 | @_runLifeCycleAction 'start' 65 | @_state = 'running' 66 | 67 | reset: -> 68 | return if Meteor.isServer and process.env.NODE_ENV is 'production' 69 | return if @_isResetting 70 | restartRequired = @is('running') 71 | @_isResetting = true 72 | if restartRequired then @stop() 73 | @_runLifeCycleAction 'reset' 74 | if restartRequired then @start() 75 | # There is no other way to avoid reset being called multiple times 76 | # if multiple modules require the same sub-module. 77 | Meteor.defer => @_isResetting = false 78 | 79 | stop: -> 80 | if @is('stopped') then return 81 | @_runLifeCycleAction 'stop', => 82 | @_state = 'stopped' 83 | 84 | is: (expectedState) -> expectedState is @_state 85 | 86 | # ========== STATIC MODULE MANAGEMENT ============ # 87 | 88 | @define: (moduleName, prototype={}) -> 89 | prototype.toString = -> moduleName # For better debugging 90 | @publish Space.Module.extend(moduleName, prototype), moduleName 91 | 92 | # All published modules register themselves here 93 | @published = {} 94 | 95 | # Publishes a module into the space environment to make it 96 | # visible and requireable for other modules and the application 97 | @publish: (module, identifier) -> 98 | module.publishedAs = module.name = identifier 99 | if Space.Module.published[identifier]? 100 | throw new Error "Two modules tried to be published as <#{identifier}>" 101 | else 102 | Space.Module.published[identifier] = module 103 | 104 | # Retrieve a module by identifier 105 | @require: (requiredModule, requestingModule) -> 106 | module = Space.Module.published[requiredModule] 107 | if not module? 108 | throw new Error "Could not find module <#{requiredModule}> 109 | required by <#{requestingModule}>" 110 | else 111 | return module 112 | 113 | # Invokes the lifecycle action on all required modules, then on itself, 114 | # calling the instance hooks before, on, and after 115 | _runLifeCycleAction: (action, func) -> 116 | @_invokeActionOnRequiredModules action 117 | Space.log.debug("#{@constructor.publishedAs}: #{action}") 118 | this["before#{Space.capitalizeString(action)}"]?() 119 | func?() 120 | this["on#{Space.capitalizeString(action)}"]?() 121 | this["after#{Space.capitalizeString(action)}"]?() 122 | 123 | # Provide lifecycle hook after this module was configured and injected 124 | _runOnInitializeHooks: -> 125 | @_invokeActionOnRequiredModules '_runOnInitializeHooks' 126 | # Never run this hook twice 127 | if @is('configuring') 128 | Space.log.debug("#{@constructor.publishedAs}: onInitialize") 129 | @_state = 'initializing' 130 | # Inject required dependencies into this module 131 | @injector.injectInto this 132 | # Call custom lifecycle hook if existant 133 | @onInitialize?() 134 | 135 | _autoMapSingletons: -> 136 | @_invokeActionOnRequiredModules '_autoMapSingletons' 137 | if @is('initializing') 138 | Space.log.debug("#{@constructor.publishedAs}: _autoMapSingletons") 139 | @_state = 'auto-mapping-singletons' 140 | # Map classes that are declared as singletons 141 | @injector.map(singleton).asSingleton() for singleton in @singletons 142 | 143 | _autoCreateSingletons: -> 144 | @_invokeActionOnRequiredModules '_autoCreateSingletons' 145 | if @is('auto-mapping-singletons') 146 | Space.log.debug("#{@constructor.publishedAs}: _autoCreateSingletons") 147 | @_state = 'auto-creating-singletons' 148 | # Create singleton classes 149 | @injector.create(singleton) for singleton in @singletons 150 | 151 | # After all modules in the tree have been configured etc. invoke last hook 152 | _runAfterInitializeHooks: -> 153 | @_invokeActionOnRequiredModules '_runAfterInitializeHooks' 154 | # Never run this hook twice 155 | if @is('auto-creating-singletons') 156 | Space.log.debug("#{@constructor.publishedAs}: afterInitialize") 157 | @_state = 'initialized' 158 | # Call custom lifecycle hook if existant 159 | @afterInitialize?() 160 | 161 | _invokeActionOnRequiredModules: (action) -> 162 | @app.modules[moduleId][action]?() for moduleId in @requiredModules 163 | 164 | _wrapLifecycleHook: (hook, wrapper) -> 165 | this[hook] ?= -> 166 | this[hook] = _.wrap(this[hook], wrapper) 167 | 168 | _mapSpaceServices: -> 169 | @injector.map('log').to Space.log 170 | 171 | _mapMeteorApis: -> 172 | Space.log.debug("#{@constructor.publishedAs}: _mapMeteorApis") 173 | # Map Meteor standard packages 174 | @injector.map('Meteor').to Meteor 175 | if Package.ejson? 176 | @injector.map('EJSON').to Package.ejson.EJSON 177 | if Package.ddp? 178 | @injector.map('DDP').to Package.ddp.DDP 179 | if Package.random? 180 | @injector.map('Random').to Package.random.Random 181 | @injector.map('underscore').to Package.underscore._ 182 | if Package.mongo? 183 | @injector.map('Mongo').to Package.mongo.Mongo 184 | if Meteor.isServer 185 | @injector.map('MongoInternals').to Package.mongo.MongoInternals 186 | 187 | if Meteor.isClient 188 | if Package.tracker? 189 | @injector.map('Tracker').to Package.tracker.Tracker 190 | if Package.templating? 191 | @injector.map('Template').to Package.templating.Template 192 | if Package.session? 193 | @injector.map('Session').to Package.session.Session 194 | if Package.blaze? 195 | @injector.map('Blaze').to Package.blaze.Blaze 196 | 197 | if Meteor.isServer 198 | @injector.map('check').to check 199 | @injector.map('Match').to Match 200 | @injector.map('process').to process 201 | @injector.map('Future').to Npm.require 'fibers/future' 202 | 203 | if Package.email? 204 | @injector.map('Email').to Package.email.Email 205 | 206 | if Package['accounts-base']? 207 | @injector.map('Accounts').to Package['accounts-base'].Accounts 208 | 209 | if Package['reactive-var']? 210 | @injector.map('ReactiveVar').to Package['reactive-var'].ReactiveVar 211 | -------------------------------------------------------------------------------- /source/namespace.coffee: -------------------------------------------------------------------------------- 1 | class Namespace 2 | constructor: (@_path) -> 3 | getPath: -> this._path 4 | toString: -> @_path 5 | 6 | # Define global namespace for the space framework 7 | @Space = new Namespace 'Space' 8 | @Space.Namespace = Namespace 9 | @Space.registry = {} -------------------------------------------------------------------------------- /source/object.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.Object 3 | 4 | # ============= PUBLIC PROTOTYPE ============== # 5 | 6 | # Assign given properties to the instance 7 | constructor: (properties) -> 8 | @_invokeConstructionCallbacks.apply(this, arguments) 9 | # Copy properties to instance by default 10 | @[key] = value for key, value of properties 11 | 12 | onDependenciesReady: -> 13 | # Let mixins initialize themselves when dependencies are ready 14 | for mixin in @constructor._getAppliedMixins() 15 | mixin.onDependenciesReady?.call(this) 16 | 17 | toString: -> @constructor.toString() 18 | 19 | hasSuperClass: -> @constructor.__super__? 20 | 21 | # Returns either the super class constructor (if no param given) or 22 | # the prototype property or method with [key] 23 | superClass: (key) -> 24 | sup = @constructor.__super__.constructor 25 | if key? then sup.prototype[key] else sup 26 | 27 | # Returns true if the passed in mixin has been applied to this or a super class 28 | hasMixin: (mixin) -> _.contains(@constructor._getAppliedMixins(), mixin) 29 | 30 | # This method needs to stay separate from the constructor so that 31 | # Space.Error can use it too! 32 | _invokeConstructionCallbacks: -> 33 | # Let mixins initialize themselves on construction 34 | for mixin in @constructor._getAppliedMixins() 35 | mixin.onConstruction?.apply(this, arguments) 36 | 37 | # ============= PUBLIC STATIC ============== # 38 | 39 | # Extends this class and return a child class with inherited prototype 40 | # and static properties. 41 | # 42 | # There are various ways you can call this method: 43 | # 44 | # 1. Space.Object.extend() 45 | # -------------------------------------------- 46 | # Creates an anonymous child class without extra prototype properties. 47 | # Basically the same as `class extend Space.Object` in coffeescript 48 | # 49 | # 2. Space.Object.extend(className) 50 | # -------------------------------------------- 51 | # Creates a named child class without extra prototype properties. 52 | # Basically the same as `class ClassName extend Space.Object` in coffeescript 53 | # 54 | # 3. Space.Object.extend(classPath) 55 | # -------------------------------------------- 56 | # Creates a child class with fully qualified class path like "my.custom.Class" 57 | # assigned and registered internally so that Space.resolvePath can find it. 58 | # This also assigns the class path as type, which can be used for serialization 59 | # 60 | # 4. Space.Object.extend({ prop: 'first', … }) 61 | # -------------------------------------------- 62 | # Creates an anonymous child class with extra prototype properties. 63 | # Same as: 64 | # class extend Space.Object 65 | # prop: 'first' 66 | # 67 | # 5. Space.Object.extend(namespace, className) 68 | # -------------------------------------------- 69 | # Creates a named class which inherits from Space.Object and assigns 70 | # it to the given namespace object. 71 | # 72 | # 6. Space.Object.extend(className, prototype) 73 | # -------------------------------------------- 74 | # Creates a named class which inherits from Space.Object and extra prototype 75 | # properties which are assigned to the new class 76 | # 77 | # 7. Space.Object.extend(classPath, prototype) 78 | # -------------------------------------------- 79 | # Creates a registered class which inherits from Space.Object and extra prototype 80 | # properties which are assigned to the new class 81 | # 82 | # 8. Space.Object.extend(namespace, className, prototype) 83 | # -------------------------------------------- 84 | # Creates a named class which inherits from Space.Object, has extra prototype 85 | # properties and is assigned to the given namespace. 86 | @extend: (args...) -> 87 | 88 | # Defaults 89 | namespace = {} 90 | classPath = null 91 | className = '_Class' # Same as coffeescript 92 | extension = {} 93 | 94 | # Only one param: (extension) OR (className) OR (classPath) -> 95 | if args.length is 1 96 | if _.isObject(args[0]) then extension = args[0] 97 | if _.isString(args[0]) 98 | # (className) OR (classPath) 99 | if args[0].indexOf('.') != -1 100 | # classPath 101 | classPath = args[0] 102 | className = classPath.substr(classPath.lastIndexOf('.') + 1) 103 | else 104 | # className 105 | className = classPath = args[0] 106 | 107 | # Two params must be: (namespace, className) OR (className, extension) -> 108 | if args.length is 2 109 | if _.isObject(args[0]) and _.isString(args[1]) 110 | namespace = args[0] 111 | className = args[1] 112 | extension = {} 113 | else if _.isString(args[0]) and _.isObject(args[1]) 114 | # (className) OR (classPath) 115 | namespace = {} 116 | extension = args[1] 117 | if args[0].indexOf('.') != -1 118 | # classPath 119 | classPath = args[0] 120 | className = classPath.substr(classPath.lastIndexOf('.') + 1) 121 | else 122 | # className 123 | className = classPath = args[0] 124 | 125 | # All three params: (namespace, className, extension) -> 126 | if args.length is 3 127 | namespace = args[0] 128 | className = args[1] 129 | extension = args[2] 130 | 131 | check namespace, Match.OneOf(Match.ObjectIncluding({}), Space.Namespace, Function) 132 | check classPath, Match.OneOf(String, null) 133 | check className, String 134 | check extension, Match.ObjectIncluding({}) 135 | 136 | # Assign the optional custom constructor for this class 137 | Parent = this 138 | Constructor = extension.Constructor ? -> Parent.apply(this, arguments) 139 | 140 | # Create a named constructor for this class so that debugging 141 | # consoles are displaying the class name nicely. 142 | Child = new Function('initialize', 'return function ' + className + '() { 143 | initialize.apply(this, arguments); 144 | }')(Constructor) 145 | 146 | # Add subclass to parent class 147 | Parent._subClasses.push(Child) 148 | 149 | # Copy the static properties of this class over to the extended 150 | Child[key] = this[key] for key of this 151 | Child._subClasses = [] 152 | 153 | # Copy over static class properties defined on the extension 154 | if extension.statics? 155 | _.extend Child, extension.statics 156 | delete extension.statics 157 | 158 | # Extract mixins before they get added to prototype 159 | mixins = extension.mixin 160 | delete extension.mixin 161 | 162 | # Extract onExtending callback and avoid adding it to prototype 163 | onExtendingCallback = extension.onExtending 164 | delete extension.onExtending 165 | 166 | # Javascript prototypal inheritance "magic" 167 | Ctor = -> 168 | @constructor = Child 169 | return 170 | Ctor.prototype = Parent.prototype 171 | Child.prototype = new Ctor() 172 | Child.__super__ = Parent.prototype 173 | 174 | # Apply mixins 175 | if mixins? then Child.mixin(mixins) 176 | 177 | # Merge the extension into the class prototype 178 | @_mergeIntoPrototype Child.prototype, extension 179 | 180 | # Add the class to the namespace 181 | if namespace? 182 | namespace[className] = Child 183 | if namespace instanceof Space.Namespace 184 | classPath = "#{namespace.getPath()}.#{className}" 185 | 186 | # Add type information to the class 187 | Child.type classPath if classPath? 188 | 189 | # Invoke the onExtending callback after everything has been setup 190 | onExtendingCallback?.call(Child) 191 | 192 | return Child 193 | 194 | @toString: -> @classPath 195 | 196 | @type: (@classPath) -> 197 | # Register this class with its class path 198 | Space.registry[@classPath] = this 199 | try 200 | # Add the class to the resolved namespace 201 | path = @classPath.substr 0, @classPath.lastIndexOf('.') 202 | namespace = Space.resolvePath path 203 | className = @classPath.substr(@classPath.lastIndexOf('.') + 1) 204 | namespace[className] = this 205 | 206 | # Create and instance of the class that this method is called on 207 | # e.g.: Space.Object.create() would return an instance of Space.Object 208 | @create: -> 209 | # Use a wrapper class to hand the constructor arguments 210 | # to the context class that #create was called on 211 | args = arguments 212 | Context = this 213 | wrapper = -> Context.apply this, args 214 | wrapper extends Context 215 | new wrapper() 216 | 217 | # Mixin properties and methods to the class prototype and merge 218 | # properties that are plain objects to support the mixin of configs etc. 219 | @mixin: (mixins) -> 220 | if _.isArray(mixins) 221 | @_applyMixin(mixin) for mixin in mixins 222 | else 223 | @_applyMixin(mixins) 224 | 225 | # Returns true if this class has a super class 226 | @hasSuperClass: -> @__super__? 227 | 228 | @isSubclassOf: (sup) -> 229 | isSubclass = this.prototype instanceof sup 230 | isSameClass = this is sup 231 | return isSubclass || isSameClass 232 | 233 | # Returns either the super class constructor (if no param given) or 234 | # the static property or method with [key] 235 | @superClass: (key) -> 236 | return undefined if !@__super__? 237 | sup = @__super__.constructor 238 | if key? then sup[key] else sup 239 | 240 | # Returns a flat, uniq array of all sub classes 241 | @subClasses: -> 242 | subs = [].concat(@_subClasses) 243 | subs = subs.concat(subClass.subClasses()) for subClass in subs 244 | return _.uniq(subs) 245 | 246 | # Returns true if the passed in mixin has been applied to this or a super class 247 | @hasMixin: (mixin) -> _.contains(@_getAppliedMixins(), mixin) 248 | 249 | # ============= PRIVATE STATIC ============== # 250 | 251 | @_subClasses: [] 252 | 253 | @_applyMixin: (mixin) -> 254 | # Add the original mixin to the registry so we can ask if a specific 255 | # mixin has been added to a host class / instance 256 | # Each class has its own mixins array 257 | hasMixins = @_appliedMixins? 258 | areInherited = hasMixins and @superClass('_appliedMixins') is @_appliedMixins 259 | if !hasMixins or areInherited then @_appliedMixins = [] 260 | 261 | # Keep the mixins array clean from duplicates 262 | @_appliedMixins.push(mixin) if !_.contains(@_appliedMixins, mixin) 263 | 264 | # Create a clone so that we can remove properties without affecting the global mixin 265 | mixinCopy = _.clone mixin 266 | 267 | # Remove hooks from mixin, so that they are not added to host class 268 | delete mixinCopy.onDependenciesReady 269 | delete mixinCopy.onConstruction 270 | 271 | # Mixin static properties into the host class 272 | if mixinCopy.statics? 273 | statics = mixinCopy.statics 274 | _.extend(this, statics) 275 | _.extend(sub, statics) for sub in @subClasses() 276 | delete mixinCopy.statics 277 | 278 | # Give mixins the chance to do static setup when applied to the host class 279 | mixinCopy.onMixinApplied?.call this 280 | delete mixinCopy.onMixinApplied 281 | 282 | # Copy over the mixin to the prototype and merge objects 283 | @_mergeIntoPrototype @prototype, mixinCopy 284 | 285 | @_getAppliedMixins: -> 286 | mixins = [] 287 | mixins = mixins.concat(@superClass()._getAppliedMixins()) if @hasSuperClass() 288 | mixins = mixins.concat(@_appliedMixins) if @_appliedMixins? 289 | return _.uniq(mixins) 290 | 291 | @_mergeIntoPrototype: (prototype, extension) -> 292 | # Helper function to check for object literals only 293 | isPlainObject = (value) -> 294 | _.isObject(value) and !_.isArray(value) and !_.isFunction(value) 295 | for key, value of extension 296 | hasProperty = prototype.hasOwnProperty(key) 297 | if hasProperty and isPlainObject(value) and isPlainObject(prototype[key]) 298 | # Deep extend plain objects 299 | _.deepExtend(prototype[key], _.clone(value)) 300 | else 301 | value = _.clone(value) if isPlainObject(value) 302 | # Set non-existing props and override existing methods 303 | prototype[key] = value -------------------------------------------------------------------------------- /source/struct.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.Struct extends Space.Object 3 | 4 | @fields: {} 5 | 6 | constructor: (data={}) -> 7 | @_checkFields(data) 8 | super 9 | 10 | fields: -> _.clone(@constructor.fields) ? {} 11 | 12 | toPlainObject: -> 13 | copy = {} 14 | copy[key] = @[key] for key of @fields() when @[key] != undefined 15 | return copy 16 | 17 | # Use the fields configuration to check given data during runtime 18 | _checkFields: (data) -> check data, @fields() -------------------------------------------------------------------------------- /source/testing/bdd-api.coffee: -------------------------------------------------------------------------------- 1 | registeredBddApis = [] 2 | 3 | Space.Module.registerBddApi = (api) -> registeredBddApis.push api 4 | 5 | Space.Module.test = Space.Application.test = (systemUnderTest, app=null) -> 6 | throw new Error 'Cannot test ' unless systemUnderTest? 7 | testApi = null 8 | isModule = isSubclassOf(this, Space.Module) 9 | isApplication = isSubclassOf(this, Space.Application) 10 | 11 | # BDD API relies on dependency injection provided by Application 12 | if !app? 13 | if isApplication 14 | app = new this() 15 | else 16 | app = new (Space.Application.define('TestApp', { 17 | configuration: { appId: 'testApp' }, 18 | requiredModules: [this.publishedAs] 19 | })) 20 | 21 | for api in registeredBddApis 22 | returnValue = api(app, systemUnderTest) 23 | testApi = returnValue if returnValue? 24 | 25 | if not testApi? then throw new Error "No testing API found for #{systemUnderTest}" 26 | return testApi -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$PORT" ]; then 4 | meteor test-packages ./ --port $PORT 5 | else 6 | meteor test-packages ./ 7 | fi 8 | -------------------------------------------------------------------------------- /tests/integration/application_with_modules.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Building applications based on modules', function() { 3 | 4 | beforeEach(function() { 5 | Space.Module.published = {}; // reset published space modules 6 | }); 7 | 8 | it('loads required module correctly', function() { 9 | 10 | let testValue = {}; 11 | let testResult = null; 12 | 13 | Space.Module.define('FirstModule', { 14 | onInitialize: function() { 15 | this.injector.map('testValue').to(testValue); 16 | } 17 | }); 18 | 19 | Space.Application.create({ 20 | requiredModules: ['FirstModule'], 21 | dependencies: { testValue: 'testValue' }, 22 | onInitialize: function() { testResult = this.testValue; } 23 | }); 24 | 25 | expect(testResult).to.equal(testValue); 26 | }); 27 | 28 | it('configures module before running', function() { 29 | 30 | let moduleValue = 'module configuration'; 31 | let appValue = 'application configuration'; 32 | let testResult = null; 33 | 34 | Space.Module.define('FirstModule', { 35 | onInitialize: function() { 36 | this.injector.map('moduleValue').to(moduleValue); 37 | }, 38 | onStart: function() { 39 | testResult = this.injector.get('moduleValue'); 40 | } 41 | }); 42 | 43 | let app = Space.Application.create({ 44 | requiredModules: ['FirstModule'], 45 | dependencies: { moduleValue: 'moduleValue' }, 46 | onInitialize: function() { 47 | this.injector.override('moduleValue').toStaticValue(appValue); 48 | } 49 | }); 50 | 51 | app.start(); 52 | expect(testResult).to.equal(appValue); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/integration/lifecycle_hooks.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.base - Application lifecycle hooks", function() { 2 | 3 | // TEST HELPERS 4 | 5 | let addHookSpy = function(hooks, hookName) { 6 | hooks[hookName] = function() {}; 7 | sinon.spy(hooks, hookName); 8 | }; 9 | 10 | let createLifeCycleHookSpies = function() { 11 | hooks = {}; 12 | hookNames = [ 13 | 'beforeInitialize', 'onInitialize', 'afterInitialize', 'beforeStart', 'onStart', 14 | 'afterStart', 'beforeReset', 'onReset', 'afterReset', 'beforeStop', 'onStop', 'afterStop' 15 | ]; 16 | for (let i = 0; i < hookNames.length; i++) { 17 | addHookSpy(hooks, hookNames[i]); 18 | } 19 | return hooks; 20 | }; 21 | 22 | let testOrderOfLifecycleHook = function(context, before, on, after) { 23 | modules = ['firstHooks', 'secondHooks', 'appHooks']; 24 | hooks = [before, on, after]; 25 | for (let i = 0; i < 3; i++) { 26 | for (let j = 0; j < 3; j++) { 27 | expect(context[modules[i]][hooks[j]]).to.have.been.calledOnce; 28 | if (i < 2) { 29 | expect(context[modules[i]][hooks[j]]).to.have.been.calledBefore( 30 | context[modules[i + 1]][hooks[j]] 31 | ); 32 | } 33 | } 34 | } 35 | }; 36 | 37 | let expectHooksNotToBeCalledYet = function(context, before, on, after) { 38 | modules = ['firstHooks', 'secondHooks', 'appHooks']; 39 | hooks = [before, on, after]; 40 | for (let i = 0; i < 3; i++) { 41 | for (let j = 0; j < 3; j++) { 42 | expect(context[modules[i]][hooks[j]]).not.to.have.been.called; 43 | } 44 | } 45 | }; 46 | 47 | beforeEach(function() { 48 | // reset published space modules 49 | Space.Module.published = {}; 50 | // Setup lifecycle hooks with sinon spys 51 | this.firstHooks = createLifeCycleHookSpies(); 52 | this.secondHooks = createLifeCycleHookSpies(); 53 | this.appHooks = createLifeCycleHookSpies(); 54 | // Create a app setup with two modules and use the spied apon hooks 55 | Space.Module.define('First', this.firstHooks); 56 | Space.Module.define('Second', _.extend(this.secondHooks, { requiredModules: ['First'] })); 57 | this.app = Space.Application.create(_.extend(this.appHooks, { requiredModules: ['Second'] })); 58 | }); 59 | 60 | it("runs the initialize hooks in correct order", function() { 61 | testOrderOfLifecycleHook(this, 'beforeInitialize', 'onInitialize', 'afterInitialize'); 62 | }); 63 | 64 | it("runs the start hooks in correct order", function() { 65 | expectHooksNotToBeCalledYet(this, 'beforeStart', 'onStart', 'afterStart'); 66 | this.app.start(); 67 | testOrderOfLifecycleHook(this, 'beforeStart', 'onStart', 'afterStart'); 68 | }); 69 | 70 | it("runs the stop hooks in correct order", function() { 71 | expectHooksNotToBeCalledYet(this, 'beforeStop', 'onStop', 'afterStop'); 72 | this.app.start(); 73 | this.app.stop(); 74 | testOrderOfLifecycleHook(this, 'beforeStop', 'onStop', 'afterStop'); 75 | }); 76 | 77 | it("runs the reset hooks in correct order when app is running", function() { 78 | expectHooksNotToBeCalledYet(this, 'beforeReset', 'onReset', 'afterReset'); 79 | this.app.start(); 80 | this.app.reset(); 81 | testOrderOfLifecycleHook(this, 'beforeStop', 'onStop', 'afterStop', 82 | 'beforeReset', 'onReset', 'afterReset', 'beforeStart', 83 | 'onStart', 'afterStart'); 84 | }); 85 | 86 | it("runs the reset hooks in correct order when app is stopped", function() { 87 | expectHooksNotToBeCalledYet(this, 'beforeReset', 'onReset', 'afterReset'); 88 | this.app.reset(); 89 | testOrderOfLifecycleHook(this, 'beforeReset', 'onReset', 'afterReset'); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/integration/module.regressions.js: -------------------------------------------------------------------------------- 1 | describe("Space.Module - regressions", function() { 2 | 3 | it("ensures autoboot singletons have access to injector mappings made in module onInitialize", function() { 4 | 5 | Test = Space.namespace('Test'); 6 | SomeLib = { libMethod: function() {} }; 7 | let singletonReadySpy = sinon.spy(); 8 | let myInjector = new Space.Injector(); 9 | 10 | Space.Object.extend(Test, 'MySingleton', { 11 | dependencies: { someLib: 'SomeLib' }, 12 | onDependenciesReady: singletonReadySpy 13 | }); 14 | 15 | Test.MyModule = Space.Module.extend(Test, 'MyModule', { 16 | singletons: ['Test.MySingleton'], 17 | onInitialize() { this.injector.map('SomeLib').to(SomeLib); } 18 | }); 19 | 20 | let module = new Test.MyModule(); 21 | module.initialize(module, myInjector); 22 | 23 | expect(singletonReadySpy).to.have.been.called; 24 | 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /tests/integration/requiring-modules.tests.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Space.base - Requiring modules in other modules and apps", function() { 3 | 4 | it("multiple modules should be able to require the same base module", function() { 5 | 6 | Space.Module.define('BaseModule', { 7 | // Regression test -> this was invoked twice at some point 8 | afterInitialize: function() { 9 | this.injector.map('x').to('y'); 10 | } 11 | }); 12 | 13 | Space.Module.define('DependentModule1', { requiredModules: ['BaseModule'] }); 14 | Space.Module.define('DependentModule2', { requiredModules: ['BaseModule'] }); 15 | 16 | const MyApp = Space.Application.define('MyApp', { 17 | requiredModules: ['DependentModule1', 'DependentModule2'] 18 | }); 19 | 20 | let appInit = function() { return new MyApp(); }; 21 | expect(appInit).to.not.throw(Error); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/standalone_application.integration.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Meteor integration in applications', -> 3 | 4 | it 'maps Meteor core packages into the Space environment', -> 5 | 6 | class SharedApp extends Space.Application 7 | 8 | dependencies: 9 | meteor: 'Meteor' 10 | ejson: 'EJSON' 11 | ddp: 'DDP' 12 | accounts: 'Accounts' 13 | random: 'Random' 14 | underscore: 'underscore' 15 | reactiveVar: 'ReactiveVar' 16 | mongo: 'Mongo' 17 | 18 | onInitialize: -> 19 | expect(@meteor).to.be.defined 20 | expect(@meteor).to.equal Meteor 21 | 22 | expect(@ejson).to.be.defined 23 | expect(@ejson).to.equal EJSON 24 | 25 | expect(@ddp).to.be.defined 26 | expect(@ddp).to.equal DDP 27 | 28 | expect(@accounts).to.be.defined 29 | expect(@accounts).to.equal Package['accounts-base'].Accounts 30 | 31 | expect(@random).to.be.defined 32 | expect(@random).to.equal Random 33 | 34 | expect(@underscore).to.be.defined 35 | expect(@underscore).to.equal Package.underscore._ 36 | 37 | expect(@reactiveVar).to.equal Package['reactive-var'].ReactiveVar 38 | 39 | expect(@mongo).to.be.defined 40 | expect(@mongo).to.equal Mongo 41 | 42 | new SharedApp() 43 | 44 | # CLIENT ONLY 45 | 46 | if Meteor.isClient 47 | 48 | class ClientApp extends Space.Application 49 | 50 | dependencies: 51 | tracker: 'Tracker' 52 | templates: 'Template' 53 | session: 'Session' 54 | blaze: 'Blaze' 55 | 56 | onInitialize: -> 57 | 58 | expect(@tracker).to.be.defined 59 | expect(@tracker).to.equal Tracker 60 | 61 | expect(@templates).to.be.defined 62 | expect(@templates).to.equal Template 63 | 64 | expect(@session).to.be.defined 65 | expect(@session).to.equal Session 66 | 67 | expect(@blaze).to.be.defined 68 | expect(@blaze).to.equal Blaze 69 | 70 | new ClientApp() 71 | 72 | # SERVER ONLY 73 | 74 | if Meteor.isServer 75 | 76 | class ServerApp extends Space.Application 77 | 78 | dependencies: 79 | email: 'Email' 80 | process: 'process' 81 | Future: 'Future' 82 | mongoInternals: 'MongoInternals' 83 | 84 | onInitialize: -> 85 | expect(@email).to.be.defined 86 | expect(@email).to.equal Package['email'].Email 87 | expect(@process).to.be.defined 88 | expect(@process).to.equal process 89 | expect(@Future).to.be.defined 90 | expect(@Future).to.equal Npm.require 'fibers/future' 91 | expect(@mongoInternals).to.be.defined 92 | expect(@mongoInternals).to.equal MongoInternals 93 | 94 | new ServerApp() 95 | 96 | it 'boots core Space Services', -> 97 | 98 | class SharedApp extends Space.Application 99 | 100 | dependencies: 101 | log: 'log' 102 | 103 | onInitialize: -> 104 | expect(@log).to.be.defined 105 | expect(@log).to.be.instanceOf Space.Logger 106 | 107 | new SharedApp() 108 | -------------------------------------------------------------------------------- /tests/unit/application.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Space.Application', -> 3 | 4 | beforeEach -> 5 | # Reset published space modules 6 | Space.Module.published = {} 7 | 8 | it 'extends Space.Module', -> 9 | expect(Space.Application).to.extend Space.Module 10 | 11 | describe 'construction', -> 12 | 13 | it 'initializes modules map as empty object', -> 14 | expect(new Space.Application().modules).to.eql {} 15 | 16 | it 'creates a new injector instance if none was given', -> 17 | expect(new Space.Application().injector).to.be.instanceof Space.Injector 18 | 19 | it 'uses the provided injector when given', -> 20 | injector = new Space.Injector() 21 | application = new Space.Application injector: injector 22 | expect(application.injector).to.equal injector 23 | 24 | it 'can also be created via static create method', -> 25 | injector = new Space.Injector() 26 | application = Space.Application.create injector: injector 27 | expect(application.injector).to.equal injector 28 | expect(Space.Application.create().injector).to.be.instanceof Space.Injector 29 | 30 | it 'maps injector instance with itself', -> 31 | injector = new Space.Injector() 32 | injectionMapping = 33 | to: sinon.spy() 34 | toInstancesOf: sinon.spy() 35 | injector.map = sinon.stub().returns injectionMapping 36 | application = new Space.Application injector: injector 37 | 38 | expect(injector.map).to.have.been.calledWithExactly 'Injector' 39 | expect(injectionMapping.to).to.have.been.calledWithExactly injector 40 | 41 | it 'initializes the application', -> 42 | initializeSpy = sinon.spy Space.Application.prototype, 'initialize' 43 | application = new Space.Application() 44 | expect(initializeSpy).to.have.been.calledOnce 45 | initializeSpy.restore() 46 | 47 | it 'can be passed a configuration', -> 48 | 49 | @application = new Space.Application({ 50 | configuration: { 51 | environment: 'testing' 52 | } 53 | }) 54 | expect(@application.configuration.environment).to.equal('testing') 55 | 56 | it 'merges configurations of all modules and user options', -> 57 | class GrandchildModule extends Space.Module 58 | @publish this, 'GrandchildModule' 59 | configuration: { 60 | subModuleValue: 'grandChild' 61 | grandchild: { 62 | toChange: 'grandchildChangeMe' 63 | toKeep: 'grandchildKeepMe' 64 | } 65 | } 66 | 67 | class ChildModule extends Space.Module 68 | @publish this, 'ChildModule' 69 | requiredModules: ['GrandchildModule'] 70 | configuration: { 71 | subModuleValue: 'child' 72 | child: { 73 | toChange: 'childChangeMe' 74 | toKeep: 'childKeepMe' 75 | } 76 | } 77 | class TestApp extends Space.Application 78 | requiredModules: ['ChildModule'] 79 | configuration: { 80 | toChange: 'appChangeMe' 81 | subModuleValue: 'overriddenByApp' 82 | } 83 | app = new TestApp() 84 | app.configure { 85 | toChange: 'appNewValue' 86 | child: { 87 | toChange: 'childNewValue' 88 | } 89 | grandchild: { 90 | toChange: 'grandchildNewValue' 91 | } 92 | } 93 | expect(app.injector.get 'configuration').toMatch { 94 | toChange: 'appNewValue' 95 | subModuleValue: 'overriddenByApp' 96 | child: { 97 | toChange: 'childNewValue' 98 | toKeep: 'childKeepMe' 99 | } 100 | grandchild: { 101 | toChange: 'grandchildNewValue' 102 | toKeep: 'grandchildKeepMe' 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/unit/error.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.Error", function() { 2 | 3 | let MyError = Space.Error.extend('MyError', { 4 | message: 'The default message for this error' 5 | }); 6 | 7 | it("is an instance of error", function() { 8 | expect(new MyError()).to.be.instanceof(Error); 9 | }); 10 | 11 | it("has same behavior as Space.Struct", function() { 12 | let data = { message: 'test', code: 123 }; 13 | let error = new MyError(data); 14 | expect(error).to.be.instanceof(Error); 15 | expect(error).to.be.instanceof(MyError); 16 | expect(error.name).to.equal('MyError'); 17 | expect(error.message).to.equal(data.message); 18 | expect(error.code).to.equal(data.code); 19 | }); 20 | 21 | it("is easy to add additional fields", function() { 22 | MyError.fields = { customField: String }; 23 | let data = { message: 'test', code: 123, customField: 'test' }; 24 | let error = new MyError(data); 25 | expect(error.customField).to.equal('test'); 26 | MyError.fields = {}; 27 | }); 28 | 29 | it("throws the prototype message by default", function() { 30 | let throwWithDefaultMessage = function() { 31 | throw new MyError(); 32 | }; 33 | expect(throwWithDefaultMessage).to.throw(MyError.prototype.message); 34 | }); 35 | 36 | it("takes an optional message during construction", function() { 37 | let myMessage = 'this is a custom message'; 38 | let throwWithCustomMessage = function() { 39 | throw new MyError(myMessage); 40 | }; 41 | expect(throwWithCustomMessage).to.throw(myMessage); 42 | }); 43 | 44 | it("includes a stack trace", function() { 45 | error = new MyError(); 46 | expect(error.stack).to.be.a.string; 47 | }); 48 | 49 | describe("applying mixins", function() { 50 | 51 | it("supports mixin callbacks", function() { 52 | let MyMixin = { 53 | onConstruction: sinon.spy(), 54 | onDependenciesReady: sinon.spy() 55 | }; 56 | let MyMixinError = Space.Error.extend('MyMixinError', { mixin: MyMixin }); 57 | let param = 'test'; 58 | let error = new MyMixinError(param); 59 | expect(MyMixin.onConstruction).to.have.been.calledOn(error); 60 | expect(MyMixin.onConstruction).to.have.been.calledWithExactly(param); 61 | }); 62 | 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /tests/unit/helpers.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | global = this 3 | 4 | describe 'Space.resolvePath', -> 5 | 6 | it 'returns a deeply nested object', -> 7 | expect(Space.resolvePath 'Space.Application').to.equal Space.Application 8 | 9 | it 'returns the global context if path is empty', -> 10 | expect(Space.resolvePath '').to.equal global 11 | -------------------------------------------------------------------------------- /tests/unit/injector.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | Injector = Space.Injector 3 | global = this 4 | 5 | describe 'Space.Injector', -> 6 | 7 | beforeEach -> @injector = new Injector() 8 | 9 | # ============ MAPPINGS ============ # 10 | 11 | describe 'working with mappings', -> 12 | 13 | it 'injects into requested dependency', -> 14 | myObject = dependencies: test: 'test' 15 | testValue = {} 16 | @injector.map('test').to testValue 17 | @injector.map('myObject').to myObject 18 | 19 | expect(@injector.get('myObject').test).to.equal testValue 20 | 21 | it 'throws error if mapping doesnt exist', -> 22 | id = 'blablub' 23 | expect(=> @injector.get(id)).to.throw( 24 | @injector.ERRORS.noMappingFound(id).message 25 | ) 26 | 27 | it 'throws error if mapping would be overriden', -> 28 | @injector.map('test').to 'test' 29 | override = => @injector.map('test').to 'other' 30 | expect(override).to.throw Error 31 | 32 | it 'can remove existing mappings', -> 33 | @injector.map('test').to 'test' 34 | @injector.remove 'test' 35 | expect(=> @injector.get 'test').to.throw 36 | 37 | it 'provides an alias for getting values', -> 38 | @injector.map('test').to 'test' 39 | expect(@injector.create 'test').to.equal 'test' 40 | 41 | it 'uses the toString method if its not a string id', -> 42 | class TestClass extends Space.Object 43 | @toString: -> 'TestClass' 44 | 45 | @injector.map(TestClass).asSingleton() 46 | expect(@injector.get('TestClass')).to.be.instanceof TestClass 47 | 48 | it 'throws error if you try to map undefined', -> 49 | expect(=> @injector.map(undefined)).to.throw @injector.ERRORS.cannotMapUndefinedId() 50 | expect(=> @injector.map(null)).to.throw @injector.ERRORS.cannotMapUndefinedId() 51 | 52 | describe 'overriding mappings', -> 53 | 54 | it 'allows to override mappings', -> 55 | @injector.map('test').to 'test' 56 | @injector.override('test').to 'other' 57 | expect(@injector.get('test')).to.equal 'other' 58 | 59 | it 'dynamically updates all dependent objects with the new dependency', -> 60 | myObject = dependencies: test: 'test' 61 | firstValue = { first: true } 62 | secondValue = { second: true } 63 | @injector.map('test').to firstValue 64 | @injector.injectInto myObject 65 | expect(myObject.test).to.equal firstValue 66 | @injector.override('test').to secondValue 67 | expect(myObject.test).to.equal secondValue 68 | 69 | it 'allows to de-register a dependent object from the mappings', -> 70 | myObject = { 71 | dependencies: 72 | first: 'First' 73 | second: 'Second' 74 | } 75 | firstValue = { first: true } 76 | secondValue = { second: true } 77 | @injector.map('First').to firstValue 78 | @injector.map('Second').to secondValue 79 | 80 | @injector.injectInto myObject 81 | firstMapping = @injector.getMappingFor 'First' 82 | secondMapping = @injector.getMappingFor 'Second' 83 | expect(firstMapping.hasDependent(myObject)).to.be.true 84 | expect(secondMapping.hasDependent(myObject)).to.be.true 85 | # Release the reference to the dependent 86 | @injector.release(myObject) 87 | expect(firstMapping.hasDependent(myObject)).to.be.false 88 | expect(secondMapping.hasDependent(myObject)).to.be.false 89 | 90 | it 'tells the dependent object when a dependency changed', -> 91 | dependentObject = { 92 | dependencies: { 93 | test: 'Test' 94 | } 95 | onDependencyChanged: sinon.spy() 96 | } 97 | firstValue = {} 98 | secondValue = {} 99 | @injector.map('Test').to firstValue 100 | @injector.injectInto dependentObject 101 | @injector.override('Test').to secondValue 102 | 103 | expect(dependentObject.onDependencyChanged).to.have.been.calledWith( 104 | 'test', secondValue 105 | ) 106 | 107 | # ========== INJECTING DEPENDENCIES ========= # 108 | 109 | describe 'injecting dependencies', -> 110 | 111 | it 'injects static values', -> 112 | value = {} 113 | @injector.map('test').to value 114 | instance = Space.Object.create dependencies: value: 'test' 115 | @injector.injectInto instance 116 | expect(instance.value).to.equal value 117 | 118 | it 'injects into provided dependencies', -> 119 | first = dependencies: value: 'test' 120 | second = dependencies: first: 'first' 121 | @injector.map('test').to 'value' 122 | @injector.map('first').to first 123 | 124 | @injector.injectInto second 125 | expect(second.first).to.equal first 126 | expect(first.value).to.equal 'value' 127 | 128 | it 'handles inherited dependencies', -> 129 | Base = Space.Object.extend dependencies: base: 'base' 130 | Extended = Base.extend dependencies: extended: 'extended' 131 | @injector.map('base').to 'base' 132 | @injector.map('extended').to 'extended' 133 | 134 | instance = new Extended() 135 | @injector.injectInto instance 136 | expect(instance.base).to.equal 'base' 137 | expect(instance.extended).to.equal 'extended' 138 | 139 | it 'never overrides existing properties', -> 140 | instance = Space.Object.create 141 | dependencies: test: 'test' 142 | test: 'value' 143 | 144 | @injector.map('test').to('test') 145 | @injector.injectInto instance 146 | 147 | expect(instance.test).to.equal 'value' 148 | 149 | describe 'when dependencies are ready', -> 150 | 151 | it 'tells the instance that they are ready', -> 152 | value = 'test' 153 | instance = Space.Object.create 154 | dependencies: value: 'value' 155 | onDependenciesReady: sinon.spy() 156 | 157 | @injector.map('value').to('value') 158 | @injector.injectInto instance 159 | @injector.injectInto instance # shouldnt trigger twice 160 | 161 | expect(instance.onDependenciesReady).to.have.been.calledOnce 162 | 163 | it 'tells every single instance exactly once', -> 164 | readySpy = sinon.spy() 165 | class TestClass extends Space.Object 166 | dependencies: value: 'test' 167 | onDependenciesReady: readySpy 168 | 169 | @injector.map('test').to 'test' 170 | @injector.map('TestClass').toInstancesOf TestClass 171 | 172 | first = @injector.create 'TestClass' 173 | second = @injector.create 'TestClass' 174 | 175 | expect(readySpy).to.have.been.calledTwice 176 | expect(readySpy).to.have.been.calledOn first 177 | expect(readySpy).to.have.been.calledOn second 178 | 179 | # ============ DEFAULT PROVIDERS ============ # 180 | 181 | describe 'default providers', -> 182 | 183 | describe 'static value providers', -> 184 | 185 | it 'maps to static value', -> 186 | value = 'test' 187 | @injector.map('first').to value 188 | @injector.map('second').toStaticValue value 189 | 190 | expect(@injector.get('first')).to.equal value 191 | expect(@injector.get('second')).to.equal value 192 | 193 | it 'supports global namespace lookup', -> 194 | global.Space.__test__ = TestClass: Space.Object.extend() 195 | path = 'Space.__test__.TestClass' 196 | @injector.map(path).asStaticValue() 197 | 198 | expect(@injector.get(path)).to.equal Space.__test__.TestClass 199 | delete global.Space.__test__ 200 | 201 | it 'can uses static toString method if available', -> 202 | class Test 203 | @toString: -> 'Test' 204 | 205 | @injector.map(Test).asStaticValue() 206 | expect(@injector.get('Test')).to.equal Test 207 | 208 | describe 'instance provider', -> 209 | 210 | it 'creates new instances for each request', -> 211 | class Test 212 | @injector.map('Test').toClass Test 213 | 214 | first = @injector.get 'Test' 215 | second = @injector.get 'Test' 216 | 217 | expect(first).to.be.instanceof Test 218 | expect(second).to.be.instanceof Test 219 | expect(first).not.to.equal second 220 | 221 | describe 'singleton provider', -> 222 | 223 | it 'maps class as singleton', -> 224 | class Test 225 | @toString: -> 'Test' 226 | @injector.map(Test).asSingleton() 227 | first = @injector.get('Test') 228 | second = @injector.get('Test') 229 | 230 | expect(first).to.be.instanceof Test 231 | expect(first).to.equal second 232 | 233 | it 'maps id to singleton of class', -> 234 | class Test 235 | @injector.map('Test').toSingleton Test 236 | first = @injector.get('Test') 237 | second = @injector.get('Test') 238 | 239 | expect(first).to.be.instanceof Test 240 | expect(first).to.equal second 241 | 242 | it 'looks up the value on global namespace if only a path is given', -> 243 | global.Space.__test__ = TestClass: Space.Object.extend() 244 | @injector.map('Space.__test__.TestClass').asSingleton() 245 | 246 | first = @injector.get('Space.__test__.TestClass') 247 | second = @injector.get('Space.__test__.TestClass') 248 | 249 | expect(first).to.be.instanceof Space.__test__.TestClass 250 | expect(first).to.equal second 251 | delete global.Space.__test__ 252 | 253 | # ============ CUSTOM PROVIDERS ============ # 254 | 255 | describe 'adding custom providers', -> 256 | 257 | it 'adds the provider to the api', -> 258 | 259 | loremIpsum = 'lorem ipsum' 260 | 261 | @injector.addProvider 'toLoremIpsum', Space.Object.extend 262 | Constructor: -> @provide = -> loremIpsum 263 | 264 | @injector.map('test').toLoremIpsum() 265 | expect(@injector.get 'test').to.equal loremIpsum 266 | -------------------------------------------------------------------------------- /tests/unit/injector_annotations.unit.js: -------------------------------------------------------------------------------- 1 | describe('Space.Injector annotations', function() { 2 | 3 | describe('Dependency annotation', function() { 4 | 5 | it('adds the dependency to the dependencies map', function() { 6 | @Space.Dependency('propertyName1', 'dependencyName1') 7 | @Space.Dependency('propertyName2', 'dependencyName2') 8 | @Space.Dependency('dependencyName3') 9 | class FixtureClass {} 10 | 11 | expect(FixtureClass.prototype.dependencies).to.deep.equal({ 12 | 'propertyName1': 'dependencyName1', 13 | 'propertyName2': 'dependencyName2', 14 | 'dependencyName3': 'dependencyName3' 15 | }); 16 | }); 17 | 18 | it('does not modify the dependencies map of the parent', function() { 19 | @Space.Dependency('propertyName0', 'dependencyName0') 20 | class FixtureParentClass {} 21 | 22 | @Space.Dependency('propertyName1', 'dependencyName1') 23 | @Space.Dependency('propertyName2', 'dependencyName2') 24 | class FixtureClass extends FixtureParentClass {} 25 | 26 | expect(FixtureParentClass.prototype.dependencies).to.deep.equal({ 27 | 'propertyName0': 'dependencyName0' 28 | }); 29 | 30 | expect(FixtureClass.prototype.dependencies).to.deep.equal({ 31 | 'propertyName0': 'dependencyName0', 32 | 'propertyName1': 'dependencyName1', 33 | 'propertyName2': 'dependencyName2' 34 | }); 35 | }); 36 | }); 37 | 38 | describe('RequireModule annotation', function() { 39 | 40 | it('adds the required module to the requiredModules array', function() { 41 | @Space.RequireModule('fooModule1') 42 | @Space.RequireModule('fooModule2') 43 | class FixtureClass {} 44 | 45 | expect(FixtureClass.prototype.requiredModules).to.contain('fooModule1'); 46 | expect(FixtureClass.prototype.requiredModules).to.contain('fooModule2'); 47 | }); 48 | 49 | it('does not modify the requiredModules array of the parent', function() { 50 | @Space.RequireModule('fooModule0') 51 | class FixtureParentClass {} 52 | 53 | @Space.RequireModule('fooModule1') 54 | @Space.RequireModule('fooModule2') 55 | class FixtureClass extends FixtureParentClass {} 56 | 57 | expect(FixtureParentClass.prototype.requiredModules).to.contain('fooModule0'); 58 | expect(FixtureParentClass.prototype.requiredModules).not.to.contain('fooModule1'); 59 | expect(FixtureParentClass.prototype.requiredModules).not.to.contain('fooModule2'); 60 | 61 | expect(FixtureClass.prototype.requiredModules).to.contain('fooModule0'); 62 | expect(FixtureClass.prototype.requiredModules).to.contain('fooModule1'); 63 | expect(FixtureClass.prototype.requiredModules).to.contain('fooModule2'); 64 | }); 65 | 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /tests/unit/logger.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.Logger", function() { 2 | 3 | beforeEach(function() { 4 | this.log = new Space.Logger(); 5 | }); 6 | 7 | afterEach(function() { 8 | this.log.stop(); 9 | }); 10 | 11 | it('extends Space.Object', function() { 12 | expect(Space.Logger).to.extend(Space.Object); 13 | }); 14 | 15 | it("is available of both client and server", function() { 16 | if (Meteor.isServer || Meteor.isClient) 17 | expect(this.log).to.be.instanceOf(Space.Logger); 18 | }); 19 | 20 | it("only logs after starting", function() { 21 | this.log.start(); 22 | this.log._logger.info = sinon.spy(); 23 | let message = 'My Log Message'; 24 | this.log.info(message); 25 | expect(this.log._logger.info).to.be.calledWithExactly(message); 26 | }); 27 | 28 | it("it can log a debug message to the output channel when min level is equal but not less", function() { 29 | this.log.start(); 30 | this.log.setMinLevel('debug'); 31 | this.log._logger.debug = sinon.spy(); 32 | let message = 'My log message'; 33 | this.log.debug(message); 34 | expect(this.log._logger.debug).to.be.calledWithExactly(message); 35 | this.log._logger.debug = sinon.spy(); 36 | this.log.setMinLevel('info'); 37 | this.log.debug(message); 38 | expect(this.log._logger.debug).not.to.be.called; 39 | }); 40 | 41 | it("it can log an info message to the output channel when min level is equal or higher, but not less", function() { 42 | this.log.start(); 43 | this.log.setMinLevel('info'); 44 | this.log._logger.info = sinon.spy(); 45 | this.log._logger.debug = sinon.spy(); 46 | let message = 'My log message'; 47 | this.log.info(message); 48 | expect(this.log._logger.info).to.be.calledWithExactly(message); 49 | expect(this.log._logger.debug).not.to.be.called; 50 | this.log._logger.info = sinon.spy(); 51 | this.log.setMinLevel('warning'); 52 | this.log.info(message); 53 | expect(this.log._logger.info).not.to.be.called; 54 | }); 55 | 56 | it.server("it can log a warning message to the output channel when min level is equal or higher, but not less", function() { 57 | this.log.start(); 58 | this.log.setMinLevel('warning'); 59 | this.log._logger.warning = sinon.spy(); 60 | this.log._logger.info = sinon.spy(); 61 | let message = 'My log message'; 62 | this.log.warning(message); 63 | expect(this.log._logger.warning).to.be.calledWithExactly(message); 64 | expect(this.log._logger.info).not.to.be.called; 65 | this.log._logger.warning = sinon.spy(); 66 | this.log.setMinLevel('error'); 67 | this.log.warning(message); 68 | expect(this.log._logger.warning).not.to.be.called; 69 | }); 70 | 71 | it.client("it can log a warning message to the output channel when min level is equal or higher, but not less", function() { 72 | this.log.start(); 73 | this.log.setMinLevel('warning'); 74 | this.log._logger.warn = sinon.spy(); 75 | this.log._logger.info = sinon.spy(); 76 | let message = 'My log message'; 77 | this.log.warning(message); 78 | expect(this.log._logger.warn).to.be.calledWithExactly(message); 79 | expect(this.log._logger.info).not.to.be.called; 80 | this.log._logger.warn = sinon.spy(); 81 | this.log.setMinLevel('error'); 82 | this.log.warning(message); 83 | expect(this.log._logger.warn).not.to.be.called; 84 | }); 85 | 86 | it("it can log an error message to the output channel when min level is equal", function() { 87 | this.log.start(); 88 | this.log.setMinLevel('error'); 89 | this.log._logger.error = sinon.spy(); 90 | this.log._logger.info = sinon.spy(); 91 | let message = 'My log message'; 92 | this.log.error(message); 93 | expect(this.log._logger.error).to.be.calledWithExactly(message); 94 | expect(this.log._logger.info).not.to.be.called; 95 | this.log._logger.info = sinon.spy(); 96 | this.log.setMinLevel('debug'); 97 | this.log.error(message); 98 | expect(this.log._logger.error).to.be.calledWithExactly(message); 99 | }); 100 | 101 | it("allows logging output to be stopped", function() { 102 | this.log._logger.info = sinon.spy(); 103 | this.log.start(); 104 | expect(this.log._is('running')).to.be.true; 105 | this.log.stop(); 106 | let message = 'My Log Message'; 107 | this.log.info(message); 108 | expect(this.log._logger.info).not.to.be.called; 109 | expect(this.log._is('stopped')).to.be.true; 110 | }); 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /tests/unit/module.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Space.Module', -> 3 | 4 | beforeEach -> 5 | # Reset published space modules 6 | Space.Module.published = {} 7 | 8 | it 'extends space object', -> expect(Space.Module).to.extend Space.Object 9 | 10 | describe '@publish', -> 11 | 12 | it 'adds given module to the static collection of published modules', -> 13 | module = Space.Module.define 'test' 14 | expect(Space.Module.published['test']).to.equal module 15 | 16 | it 'throws an error if two modules try to publish under same name', -> 17 | publishTwoModulesWithSameName = -> 18 | Space.Module.define 'test' 19 | Space.Module.define 'test' 20 | expect(publishTwoModulesWithSameName).to.throw Error 21 | 22 | describe '@require', -> 23 | 24 | it 'returns published module for given identifier', -> 25 | module = Space.Module.define 'test' 26 | requiredModule = Space.Module.require 'test' 27 | expect(requiredModule).to.equal module 28 | 29 | it 'throws and error if no module was registered for given identifier', -> 30 | requireUnkownModule = -> Space.Module.require 'unknown module' 31 | expect(requireUnkownModule).to.throw Error 32 | 33 | describe 'constructor', -> 34 | 35 | it 'sets required modules to empty array if none defined', -> 36 | module = new Space.Module() 37 | expect(module.requiredModules).to.be.instanceof Array 38 | expect(module.requiredModules).to.be.empty 39 | 40 | it 'leaves the defined required modules intact', -> 41 | testArray = [] 42 | module = Space.Module.create requiredModules: testArray 43 | expect(module.requiredModules).to.equal testArray 44 | 45 | it 'sets the correct state', -> 46 | module = new Space.Module() 47 | expect(module.is 'constructed').to.be.true 48 | 49 | 50 | describe 'Space.Module - #initialize', -> 51 | 52 | beforeEach -> 53 | # Reset published space modules 54 | Space.Module.published = {} 55 | @injector = new Space.Injector() 56 | sinon.spy @injector, 'injectInto' 57 | @module = new Space.Module() 58 | # faked required modules to spy on 59 | @SubModule1 = Space.Module.define 'SubModule1' 60 | @SubModule2 = Space.Module.define 'SubModule2' 61 | @app = modules: {} 62 | 63 | it 'asks the injector to inject dependencies into the module', -> 64 | @module.initialize @app, @injector 65 | expect(@injector.injectInto).to.have.been.calledWith @module 66 | 67 | it 'throws an error if no injector is provided', -> 68 | initializeWithoutInjector = => @module.initialize() 69 | expect(initializeWithoutInjector).to.throw Error 70 | 71 | it 'sets the initialized flag correctly', -> 72 | @module.initialize @app, @injector 73 | expect(@module.is 'initialized').to.be.true 74 | 75 | it.server 'adds Npm as property to the module', -> 76 | @module.initialize @app, @injector 77 | expect(@module.npm.require).to.be.defined 78 | 79 | it 'invokes the onInitialize method on itself', -> 80 | @module.onInitialize = sinon.spy() 81 | @module.initialize @app, @injector 82 | expect(@module.onInitialize).to.have.been.calledOnce 83 | 84 | it 'creates required modules and adds them to the app', -> 85 | @module.requiredModules = [@SubModule1.name, @SubModule2.name] 86 | @module.initialize @app, @injector 87 | expect(@app.modules[@SubModule1.name]).to.be.instanceof(@SubModule1) 88 | expect(@app.modules[@SubModule2.name]).to.be.instanceof(@SubModule2) 89 | 90 | it 'initializes required modules', -> 91 | sinon.stub @SubModule1.prototype, 'initialize' 92 | @module.requiredModules = [@SubModule1.name] 93 | @module.initialize @app, @injector 94 | expect(@SubModule1.prototype.initialize).to.have.been.calledOnce 95 | 96 | it 'can only be initialized once', -> 97 | @module.onInitialize = sinon.spy() 98 | @module.initialize @app, @injector 99 | @module.initialize @app, @injector 100 | expect(@module.onInitialize).to.have.been.calledOnce 101 | 102 | describe 'Space.Module - #start', -> 103 | 104 | beforeEach -> 105 | @module = new Space.Module() 106 | @module.start() 107 | @module._runLifeCycleAction = sinon.spy() 108 | 109 | it 'sets the state to running', -> 110 | expect(@module.is 'running').to.be.true 111 | 112 | it 'ignores start calls on a running module', -> 113 | @module.start() 114 | expect(@module._runLifeCycleAction).not.to.have.been.called 115 | 116 | describe 'Space.Module - #stop', -> 117 | 118 | beforeEach -> 119 | @module = new Space.Module() 120 | @module.start() 121 | @module.stop() 122 | @module._runLifeCycleAction = sinon.spy() 123 | 124 | it 'sets the state to stopped', -> 125 | expect(@module.is 'stopped').to.be.true 126 | 127 | it 'ignores stop calls on a stopped module', -> 128 | @module.stop() 129 | expect(@module._runLifeCycleAction).not.to.have.been.called 130 | 131 | describe 'Space.Module - #reset', -> 132 | 133 | beforeEach -> 134 | @module = new Space.Module() 135 | @module._runLifeCycleAction = sinon.spy() 136 | 137 | it.server 'rejects attempts to reset when in production', -> 138 | nodeEnvBackup = process.env.NODE_ENV 139 | process.env.NODE_ENV = 'production' 140 | @module.reset() 141 | process.env.NODE_ENV = nodeEnvBackup 142 | expect(@module._runLifeCycleAction).not.to.have.been.called 143 | 144 | describe "Space.Module - wrappable lifecycle hooks", -> 145 | 146 | it "allows mixins to hook into the module lifecycle", -> 147 | moduleOnInitializeSpy = sinon.spy() 148 | mixinOnInitializeSpy = sinon.spy() 149 | MyModule = Space.Module.extend { 150 | onInitialize: moduleOnInitializeSpy 151 | } 152 | MyModule.mixin { 153 | onDependenciesReady: -> 154 | @_wrapLifecycleHook 'onInitialize', (onInitialize) -> 155 | onInitialize.call(this) 156 | mixinOnInitializeSpy.call(this) 157 | } 158 | module = new MyModule() 159 | module.initialize(module, new Space.Injector()) 160 | 161 | expect(moduleOnInitializeSpy).to.have.been.calledOnce 162 | expect(mixinOnInitializeSpy).to.have.been.calledOnce 163 | expect(moduleOnInitializeSpy).to.have.been.calledBefore(mixinOnInitializeSpy) 164 | -------------------------------------------------------------------------------- /tests/unit/object.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Space.Object', -> 3 | 4 | beforeEach -> @namespace = {} 5 | 6 | describe 'extending', -> 7 | 8 | it 'creates and returns a subclass', -> 9 | Space.Object.extend(@namespace, 'MyClass') 10 | expect(@namespace.MyClass).to.extend Space.Object 11 | 12 | it 'applies the arguments to the super constructor', -> 13 | [first, second, third] = ['first', 2, {}] 14 | spy = sinon.spy() 15 | Space.Object.extend @namespace, 'Base', { 16 | Constructor: -> spy.apply this, arguments 17 | } 18 | @namespace.Base.extend(@namespace, 'Extended') 19 | instance = new @namespace.Extended first, second, third 20 | expect(spy).to.have.been.calledWithExactly first, second, third 21 | expect(spy).to.have.been.calledOn instance 22 | 23 | it 'allows to extend the prototype', -> 24 | First = Space.Object.extend first: 1, get: (property) -> @[property] 25 | Second = First.extend second: 2, get: -> First::get.apply this, arguments 26 | class Third extends Second 27 | get: (property) -> super property 28 | instance = new Third() 29 | expect(instance.get('first')).to.equal 1 30 | expect(instance.get('second')).to.equal 2 31 | 32 | describe "providing fully qualified class path", -> 33 | 34 | it "registers the class for internal lookup", -> 35 | Space.namespace('My.custom') 36 | FirstClass = Space.Object.extend('My.custom.FirstClass', {}) 37 | SecondClass = Space.Object.extend('My.custom.SecondClass', {}) 38 | expect(Space.resolvePath 'My.custom.FirstClass').to.equal(FirstClass) 39 | expect(Space.resolvePath 'My.custom.SecondClass').to.equal(SecondClass) 40 | 41 | it "assigns the class path", -> 42 | className = 'My.custom.Class' 43 | MyClass = Space.Object.extend(className) 44 | expect(MyClass.toString()).to.equal(className) 45 | expect(new MyClass().toString()).to.equal(className) 46 | 47 | it "exposes the class on the global scope if possible", -> 48 | my = {} 49 | my.namespace = Space.namespace('my.namespace') 50 | MyClass = Space.Object.extend('my.namespace.MyClass') 51 | expect(my.namespace.MyClass).to.equal(MyClass) 52 | 53 | it "works correctly without nested namespaces", -> 54 | MyClass = Space.Object.extend('MyClass') 55 | expect(Space.resolvePath 'MyClass').to.equal(MyClass) 56 | 57 | describe "working with static class properties", -> 58 | 59 | it 'allows you to define static class properties', -> 60 | myStatics = {} 61 | MyClass = Space.Object.extend statics: { myStatics: myStatics } 62 | expect(MyClass.myStatics).to.equal(myStatics) 63 | 64 | it 'provides an api for defining a callback while extending', -> 65 | onExtendingSpy = sinon.spy() 66 | MyClass = Space.Object.extend onExtending: onExtendingSpy 67 | expect(onExtendingSpy).to.have.been.calledOn(MyClass) 68 | 69 | describe 'creating instances', -> 70 | 71 | it 'creates a new instance of given class', -> 72 | expect(Space.Object.create()).to.be.instanceof Space.Object 73 | 74 | it 'allows to initialize the instance with given properties', -> 75 | instance = Space.Object.create first: 1, get: (property) -> @[property] 76 | expect(instance.get 'first').to.equal 1 77 | 78 | it 'forwards any number of arguments to the constructor', -> 79 | Base = Space.Object.extend Constructor: (@first, @second) -> 80 | instance = Base.create 1, 2 81 | expect(instance.first).to.equal 1 82 | expect(instance.second).to.equal 2 83 | 84 | describe "inheritance helpers", -> 85 | 86 | Base = Space.Object.extend { 87 | statics: { prop: 'static', method: -> } 88 | prop: 'prototype' 89 | method: -> 90 | } 91 | Sub = Base.extend() 92 | GrandSub = Sub.extend() 93 | 94 | describe "static", -> 95 | 96 | it "can tell if there is a super class", -> 97 | expect(Sub.hasSuperClass()).to.be.true 98 | 99 | it "can return the super class", -> 100 | expect(Sub.superClass()).to.equal(Base) 101 | 102 | it "returns undefined if there is no super class", -> 103 | expect(Space.Object.superClass()).to.equal(undefined) 104 | 105 | it "can return a static prop or method of the super class", -> 106 | expect(Sub.superClass('prop')).to.equal(Base.prop) 107 | expect(Sub.superClass('method')).to.equal(Base.method) 108 | 109 | it "can give back a flat array of sub classes", -> 110 | expect(Base.subClasses()).to.eql [Sub, GrandSub] 111 | expect(Sub.subClasses()).to.eql [GrandSub] 112 | expect(GrandSub.subClasses()).to.eql [] 113 | 114 | describe "prototype", -> 115 | 116 | it "can tell if there is a super class", -> 117 | expect(new Sub().hasSuperClass()).to.be.true 118 | 119 | it "can return the super class", -> 120 | expect(new Sub().superClass()).to.equal(Base) 121 | 122 | it "can return a static prop or method of the super class", -> 123 | expect(new Sub().superClass('prop')).to.equal(Base::prop) 124 | expect(new Sub().superClass('method')).to.equal(Base::method) 125 | 126 | describe 'mixins', -> 127 | 128 | it 'adds methods to the prototype', -> 129 | testMixin = test: -> 130 | TestClass = Space.Object.extend() 131 | TestClass.mixin testMixin 132 | expect(TestClass::test).to.equal testMixin.test 133 | 134 | it 'overrides existing methods of the prototype', -> 135 | testMixin = test: -> 136 | TestClass = Space.Object.extend test: -> 137 | TestClass.mixin testMixin 138 | expect(TestClass::test).to.equal testMixin.test 139 | 140 | it 'merges object properties', -> 141 | testMixin = dependencies: second: 'second' 142 | TestClass = Space.Object.extend dependencies: first: 'first' 143 | TestClass.mixin testMixin 144 | expect(TestClass::dependencies.first).to.equal 'first' 145 | expect(TestClass::dependencies.second).to.equal 'second' 146 | 147 | it "does not modify other mixins when merging properties", -> 148 | FirstMixin = dependencies: firstMixin: 'onExtending' 149 | FirstClass = Space.Object.extend { 150 | mixin: [FirstMixin] 151 | dependencies: first: 'first' 152 | } 153 | FirstClass.mixin dependencies: firstMixin: 'afterExtending' 154 | expect(FirstMixin).toMatch dependencies: firstMixin: 'onExtending' 155 | expect(FirstClass.prototype.dependencies).toMatch { 156 | first: 'first' 157 | firstMixin: 'afterExtending' 158 | } 159 | 160 | it "can provide a hook that is called when the mixin is applied", -> 161 | myMixin = onMixinApplied: sinon.spy() 162 | TestClass = Space.Object.extend() 163 | TestClass.mixin myMixin 164 | expect(myMixin.onMixinApplied).to.have.been.calledOnce 165 | 166 | it 'can be defined as prototype property when extending classes', -> 167 | myMixin = { onMixinApplied: sinon.spy() } 168 | MyClass = Space.Object.extend mixin: [myMixin] 169 | expect(myMixin.onMixinApplied).to.have.been.calledOn(MyClass) 170 | 171 | it 'can be used to mixin static properties on to the class', -> 172 | myMixin = statics: { myMethod: sinon.spy() } 173 | MyClass = Space.Object.extend mixin: [myMixin] 174 | MyClass.myMethod() 175 | expect(myMixin.statics.myMethod).to.have.been.calledOn(MyClass) 176 | 177 | it 'can be checked which mixins a class has', -> 178 | FirstMixin = {} 179 | SecondMixin = {} 180 | ThirdMixin = {} 181 | MyClass = Space.Object.extend({ mixin: FirstMixin }) 182 | MyClass.mixin(SecondMixin) 183 | instance = new MyClass() 184 | # Static checks 185 | expect(MyClass.hasMixin(FirstMixin)).to.be.true 186 | expect(MyClass.hasMixin(SecondMixin)).to.be.true 187 | expect(MyClass.hasMixin(ThirdMixin)).to.be.false 188 | # Instance checks 189 | expect(instance.hasMixin(FirstMixin)).to.be.true 190 | expect(instance.hasMixin(SecondMixin)).to.be.true 191 | expect(instance.hasMixin(ThirdMixin)).to.be.false 192 | 193 | describe "mixin inheritance", -> 194 | 195 | it "does not apply mixins to super classes", -> 196 | firstMixin = {} 197 | secondMixin = {} 198 | SuperClass = Space.Object.extend mixin: firstMixin 199 | SubClass = SuperClass.extend mixin: secondMixin 200 | expect(SuperClass.hasMixin(firstMixin)).to.be.true 201 | expect(SuperClass.hasMixin(secondMixin)).to.be.false 202 | expect(SubClass.hasMixin(firstMixin)).to.be.true 203 | expect(SubClass.hasMixin(secondMixin)).to.be.true 204 | 205 | it "inherits mixins to children when added to base class later on", -> 206 | LateMixin = { statics: { test: 'property' } } 207 | # Base class with a mixin 208 | BaseClass = Space.Object.extend() 209 | # Sublcass with its own mixin 210 | SubClass = BaseClass.extend() 211 | # Later we extend base class 212 | BaseClass.mixin LateMixin 213 | # Sub class should have all three mixins correctly applied 214 | expect(SubClass.hasMixin(LateMixin)).to.be.true 215 | expect(SubClass.test).to.equal LateMixin.statics.test 216 | 217 | describe "onDependenciesReady hooks", -> 218 | 219 | it "can provide a hook that is called when dependencies of host class are ready", -> 220 | myMixin = onDependenciesReady: sinon.spy() 221 | TestClass = Space.Object.extend() 222 | TestClass.mixin myMixin 223 | new TestClass().onDependenciesReady() 224 | expect(myMixin.onDependenciesReady).to.have.been.calledOnce 225 | 226 | it "inherits the onDependenciesReady hooks to sub classes", -> 227 | firstMixin = onDependenciesReady: sinon.spy() 228 | secondMixin = onDependenciesReady: sinon.spy() 229 | SuperClass = Space.Object.extend() 230 | SuperClass.mixin firstMixin 231 | SubClass = SuperClass.extend() 232 | SubClass.mixin secondMixin 233 | new SubClass().onDependenciesReady() 234 | expect(firstMixin.onDependenciesReady).to.have.been.calledOnce 235 | expect(secondMixin.onDependenciesReady).to.have.been.calledOnce 236 | 237 | it "calls inherited mixin hooks only once per chain", -> 238 | myMixin = onDependenciesReady: sinon.spy() 239 | SuperClass = Space.Object.extend() 240 | SuperClass.mixin myMixin 241 | SubClass = SuperClass.extend() 242 | new SubClass().onDependenciesReady() 243 | expect(myMixin.onDependenciesReady).to.have.been.calledOnce 244 | 245 | describe "construction hooks", -> 246 | 247 | it "can provide a hook that is called on construction of host class", -> 248 | myMixin = onConstruction: sinon.spy() 249 | TestClass = Space.Object.extend() 250 | TestClass.mixin myMixin 251 | first = {} 252 | second = {} 253 | new TestClass(first, second) 254 | expect(myMixin.onConstruction).to.have.been.calledWithExactly(first, second) 255 | 256 | -------------------------------------------------------------------------------- /tests/unit/struct.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Space.Struct', -> 3 | 4 | class MyTestStruct extends Space.Struct 5 | @type 'MyTestStruct' 6 | fields: -> name: String, age: Match.Integer 7 | 8 | class MyExtendedTestStruct extends MyTestStruct 9 | @type 'MyExtendedTestStruct' 10 | fields: -> 11 | fields = super() 12 | fields.extra = Match.Integer 13 | return fields 14 | 15 | it "is a Space.Object", -> 16 | expect(Space.Struct).to.extend(Space.Object) 17 | 18 | it "calls the super constructor", -> 19 | constructorSpy = sinon.spy(Space.Object.prototype, 'constructor') 20 | data = {} 21 | struct = new Space.Struct(data) 22 | expect(constructorSpy).to.have.been.calledWithExactly(data) 23 | expect(constructorSpy).to.have.been.calledOn(struct) 24 | constructorSpy.restore() 25 | 26 | describe 'defining fields', -> 27 | 28 | it 'assigns the properties to the instance', -> 29 | properties = name: 'Dominik', age: 26 30 | instance = new MyTestStruct properties 31 | expect(instance).toMatch properties 32 | 33 | it 'provides a method to cast to plain object', -> 34 | instance = new MyTestStruct name: 'Dominik', age: 26 35 | copy = instance.toPlainObject() 36 | expect(copy.name).to.equal 'Dominik' 37 | expect(copy.age).to.equal 26 38 | expect(copy).to.be.an.object 39 | expect(copy).not.to.be.instanceof MyTestStruct 40 | 41 | it 'throws a match error if a property is of wrong type', -> 42 | expect(-> new MyTestStruct name: 5, age: 26).to.throw Match.Error 43 | 44 | it 'throws a match error if additional properties are given', -> 45 | expect(-> new MyTestStruct name: 5, age: 26, extra: 0).to.throw Match.Error 46 | 47 | it 'throws a match error if a property is missing', -> 48 | expect(-> new MyTestStruct name: 5).to.throw Match.Error 49 | 50 | it 'allows to extend the fields of base classes', -> 51 | expect(-> new MyExtendedTestStruct name: 'test', age: 26, extra: 0) 52 | .not.to.throw Match.Error 53 | 54 | # TODO: remove when breaking change is made for next major version: 55 | it 'stays backward compatible with static fields api', -> 56 | class StaticFieldsStruct extends Space.Struct 57 | @fields: { name: String, age: Match.Integer } 58 | 59 | properties = name: 'Dominik', age: 26 60 | instance = new StaticFieldsStruct properties 61 | expect(instance).toMatch properties 62 | expect(-> new StaticFieldsStruct name: 5).to.throw Match.Error 63 | 64 | 65 | --------------------------------------------------------------------------------