Skip to content

Commit 89237bb

Browse files
committed
Merge pull request #20 from Spy-Seth/hooks
Implement post instanciation method call on service
2 parents d814c70 + a992844 commit 89237bb

27 files changed

+650
-253
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sudo: false
55
node_js:
66
- "iojs-v2.4.0"
77
- "iojs-v3.1.0"
8+
- "iojs-v3.2.0"
89
- "0.12"
910

1011
script:

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ Skippy is designed to be an easy to use, robust, and well tested dependencies co
1010
- Instantiate service as described in the dependencies configuration.
1111
- Configure what should be used as service constructor parameters: you can pass another services as reference, parameters, or any values you have configured.
1212
- Manage singleton instance of service (not defined by design pattern, but by configuration).
13-
- Check the dependencies graph to avoid cyclic dependencies (only on development environment: `NODE_ENV="development"`).
13+
- Call custom method on service instance to finish it configuration.
14+
- Check the dependencies graph to avoid cyclic dependencies.
1415
- Allowing injection of service mock (only in test environment: `NODE_ENV="test"`).
1516

1617
#### What Skippy don't do (and never will)
1718
- Introspect JSDoc or parameters name to determine witch service should be inject in a constructor function. You have to define service dependencies in a configuration file.
1819
- Coffee
1920

2021
#### What Skippy don't at the moment (maybe one day)
21-
- Use factory service to generate another service
22-
- Generate a lazy loading proxy function
2322
- Manage service scope (allowing private service, who can only be used to instantiate other service, not exposed to the world)
23+
- Use factory service to generate another service (delayed, the post service creation hooks do a part of the job for now)
24+
- Generate a lazy loading proxy function (delayed, until ES6 proxy is well supported)
2425

2526

2627
## Installation
@@ -100,6 +101,15 @@ module.exports = [
100101
"@foo.serviceA",
101102
"@foo.serviceB"
102103
]
104+
},
105+
{
106+
"name": "foo.serviceD",
107+
"service": require("./ServiceD"),
108+
"calls": {
109+
"setLocale": [
110+
"@foo.serviceB",
111+
"%default.locale%"
112+
]
103113
}
104114
];
105115
```
@@ -109,6 +119,7 @@ A service configuration have four possible keys:
109119
- `service` (`function`, mandatory): the service constructor function reference.
110120
- `singleton` (`boolean`, optional, default `true`): If `true`, the service will be a singleton. If `false`, a new service will be instantiated each time.
111121
- `arguments` (`array`, optional, default empty array): An array of the value to inject in the service constructor (see "Arguments" section for more informations).
122+
- `calls` (`object`, optional, default empty array): A map of the method to call on the service, each method should have it `arguments` array (see "Arguments" section for more informations).
112123

113124

114125
### Arguments

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
"devDependencies": {
2828
"chai": "~3.2.0",
2929
"coveralls": "~2.11.4",
30-
"eslint": "~1.2.1",
31-
"istanbul": "~0.3.18",
32-
"mocha": "~2.2.5",
30+
"eslint": "~1.3.1",
31+
"istanbul": "~0.3.19",
32+
"mocha": "~2.3.0",
3333
"sinon": "~1.16.1",
3434
"sinon-chai": "~2.8.0"
3535
},

src/Call.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
/**
4+
* @param {String} methodName
5+
* @param {FunctionArgumentCollection} functionArgumentCollection
6+
*/
7+
var Call = function Call(methodName, functionArgumentCollection) {
8+
this.methodName = methodName;
9+
this.functionArgumentCollection = functionArgumentCollection;
10+
};
11+
12+
/**
13+
* @param {Container} container
14+
* @param {Object} instance
15+
*/
16+
Call.prototype.trigger = function trigger(container, instance) {
17+
if (!instance[this.methodName]) {
18+
throw new Error('Can\'t call the given method: "' + this.methodName + '" does not exist on the given instance.');
19+
}
20+
21+
if (!(instance[this.methodName] instanceof Function)) {
22+
throw new Error('Can\'t call the given method: "' + this.methodName + '" is not a callable function on the given instance.');
23+
}
24+
25+
instance[this.methodName].apply(instance, this.functionArgumentCollection.resolveArguments(container));
26+
};
27+
28+
/**
29+
* @param {ServiceDefinitionCollection} serviceDefinitionCollection
30+
* @param {Function} serviceConstructor
31+
*/
32+
Call.prototype.validate = function validate(serviceDefinitionCollection, serviceConstructor) {
33+
if (!this._isMethodDefined(serviceConstructor, this.methodName)) {
34+
throw new Error('The method "' + this.methodName + '" does not exist on the given constructor.');
35+
}
36+
};
37+
38+
/**
39+
* @param {Function} serviceConstructor
40+
* @param {String} methodName
41+
* @return {Boolean}
42+
*/
43+
Call.prototype._isMethodDefined = function _isMethodDefined(serviceConstructor, methodName) {
44+
if (serviceConstructor[this.methodName]) {
45+
return true;
46+
}
47+
48+
if (serviceConstructor.prototype && (serviceConstructor.prototype !== Function.prototype)) {
49+
return this._isMethodDefined(serviceConstructor.prototype, methodName);
50+
}
51+
52+
return false;
53+
};
54+
55+
module.exports = Call;

src/CallCollection.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
var each = require('lodash/collection/each');
4+
var Call = require('./Call');
5+
6+
/**
7+
* @param {Array<Call>} calls
8+
*/
9+
var CallCollection = function CallCollection(calls) {
10+
calls = calls || [];
11+
12+
each(calls, function (call, index) {
13+
if (!(call instanceof Call)) {
14+
throw new Error('Wrong paramater type at position #' + index + '. Only accept Call instance.');
15+
}
16+
});
17+
18+
this.calls = calls;
19+
};
20+
21+
/**
22+
* @return {Boolean}
23+
*/
24+
CallCollection.prototype.isEmpty = function () {
25+
return (this.calls.length === 0);
26+
};
27+
28+
/**
29+
* @param {Function} cb
30+
*/
31+
CallCollection.prototype.forEach = function (cb) {
32+
each(this.calls, cb);
33+
};
34+
35+
module.exports = CallCollection;

src/Container.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@ var ServiceStorage = require('./ServiceStorage');
77
*
88
* @param {ServiceDefinitionCollection} serviceDefinitionCollection
99
* @param {ParameterCollection} parameterCollection
10+
* @param {Boolean} validateContainer
1011
* @private
1112
* @constructor
1213
*/
13-
var Container = function Container(serviceDefinitionCollection, parameterCollection) {
14+
var Container = function Container(serviceDefinitionCollection, parameterCollection, validateContainer) {
1415
this.serviceDefinitionCollection = serviceDefinitionCollection;
1516
this.parameterCollection = parameterCollection;
1617

1718
this.serviceStorage = new ServiceStorage();
19+
20+
if (validateContainer) {
21+
this.serviceDefinitionCollection.checkCyclicDependencies();
22+
this.serviceDefinitionCollection.validateCalls();
23+
// TODO: Validate the existance of used parameter as service arguments or call arguments.
24+
}
1825
};
1926

2027
/**
@@ -50,10 +57,15 @@ Container.prototype.getService = function (name) {
5057

5158
var serviceDefinition = this.serviceDefinitionCollection.getServiceDefinition(name);
5259
var serviceInstance = serviceDefinition.createInstance(this);
60+
5361
if (serviceDefinition.isSingleton()) {
5462
this.serviceStorage.addInstance(name, serviceInstance);
5563
}
5664

65+
if (serviceDefinition.hasCalls()) {
66+
serviceDefinition.triggerCalls(this, serviceInstance);
67+
}
68+
5769
return serviceInstance;
5870
};
5971

@@ -77,4 +89,5 @@ if (process.env.NODE_ENV === 'test') {
7789
this.serviceStorage.replaceInstance(name, mock);
7890
};
7991
}
92+
8093
module.exports = Container;

src/ContainerFactory.js

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
'use strict';
22

33
var each = require('lodash/collection/each');
4+
var Call = require('./Call');
5+
var CallCollection = require('./CallCollection');
46
var Container = require('./Container');
5-
var ObjectHelper = require('./ObjectHelper');
67
var Parameter = require('./Parameter');
78
var ParameterCollection = require('./ParameterCollection');
8-
var ServiceArgument = require('./ServiceArgument');
9-
var ServiceArgumentCollection = require('./ServiceArgumentCollection');
9+
var FunctionArgument = require('./FunctionArgument');
10+
var FunctionArgumentCollection = require('./FunctionArgumentCollection');
1011
var ServiceDefinition = require('./ServiceDefinition');
1112
var ServiceDefinitionCollection = require('./ServiceDefinitionCollection');
1213

1314
/**
1415
* @param {Array} services
1516
* @return {ServiceDefinitionCollection}
17+
* @private
1618
*/
1719
var buildServiceDefinitionCollection = function buildServiceDefinitionCollection(services) {
1820
var servicesConfigurationList = services || [];
@@ -21,16 +23,28 @@ var buildServiceDefinitionCollection = function buildServiceDefinitionCollection
2123
each(servicesConfigurationList, function (value) {
2224
var argumentConfigurationList = value.arguments || [];
2325

24-
var serviceArgumentList = [];
26+
var functionArgumentList = [];
2527
each(argumentConfigurationList, function (argumentValue) {
26-
serviceArgumentList.push(new ServiceArgument(argumentValue));
28+
functionArgumentList.push(new FunctionArgument(argumentValue));
29+
});
30+
31+
var calls = value.calls || {};
32+
var callList = [];
33+
each(calls, function (callArguments, methodName) {
34+
var callArgumentList = [];
35+
each(callArguments, function (argumentValue) {
36+
callArgumentList.push(new FunctionArgument(argumentValue));
37+
});
38+
39+
callList.push(new Call(methodName, new FunctionArgumentCollection(callArgumentList)));
2740
});
2841

2942
servicesDefinitionList.push(new ServiceDefinition(
3043
value.name,
3144
value.service,
32-
new ServiceArgumentCollection(serviceArgumentList),
33-
value.singleton || undefined
45+
new FunctionArgumentCollection(functionArgumentList),
46+
value.singleton || undefined,
47+
new CallCollection(callList)
3448
));
3549
});
3650

@@ -53,51 +67,22 @@ var buildParameterCollection = function buildParameterCollection(parameters) {
5367
return new ParameterCollection(parameterList);
5468
};
5569

56-
var checkCyclicDependencies = function checkCyclicDependencies(serviceDefinition, serviceDefinitionCollection, parentDependentServiceNames) {
57-
parentDependentServiceNames = parentDependentServiceNames || [serviceDefinition.getName()];
58-
59-
var serviceArguments = serviceDefinition.getArgumentCollection().getServiceArguments();
60-
each(serviceArguments, function (argument) {
61-
var serviceName = argument.getName();
62-
63-
if (!serviceDefinitionCollection.hasServiceDefinition(serviceName)) {
64-
throw new Error('The service "' + serviceDefinition.getName() + '" has dependencies on the unknown service "' + serviceName + '".');
65-
}
66-
67-
if (parentDependentServiceNames.indexOf(serviceName) !== -1) {
68-
parentDependentServiceNames.push(serviceName); // To show the complete dependency graph.
69-
throw new Error('Cyclic dependencies detected: "' + parentDependentServiceNames.join(' > ') + '".');
70-
}
71-
72-
var childDependentServiceNames = ObjectHelper.clone(parentDependentServiceNames);
73-
childDependentServiceNames.push(serviceName);
74-
75-
checkCyclicDependencies(serviceDefinitionCollection.getServiceDefinition(serviceName), serviceDefinitionCollection, childDependentServiceNames);
76-
});
77-
};
78-
7970
var ContainerFactory = {};
8071

8172
/**
82-
* @param {Array<{name: String, service: Function, arguments: Array<String|*>}>} services
73+
* @param {Array<{name: String, service: Function, arguments: Array<String|*>, calls: {String: Array<String|*>}}>} services
8374
* @param {{String: *}} parameters
75+
* @param {Boolean} validateContainer
8476
* @return {Container}
85-
* @public
8677
*/
87-
ContainerFactory.create = function create(services, parameters) {
78+
ContainerFactory.create = function create(services, parameters, validateContainer) {
8879
services = services || [];
8980
parameters = parameters || {};
9081

9182
var serviceDefinitionCollection = buildServiceDefinitionCollection(services);
9283
var parameterCollection = buildParameterCollection(parameters);
9384

94-
if (process.env.NODE_ENV === 'development') {
95-
serviceDefinitionCollection.forEach(function (serviceDefinition) {
96-
checkCyclicDependencies(serviceDefinition, serviceDefinitionCollection);
97-
});
98-
}
99-
100-
return new Container(serviceDefinitionCollection, parameterCollection);
85+
return new Container(serviceDefinitionCollection, parameterCollection, validateContainer);
10186
};
10287

10388
module.exports = ContainerFactory;

0 commit comments

Comments
 (0)