├── .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 | 
2 |
3 | # SPACE [](https://circleci.com/gh/meteor-space/base) [](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 |
--------------------------------------------------------------------------------