Skip to content

Commit

Permalink
v0.0.1 - Initial library with examples
Browse files Browse the repository at this point in the history
- Completed the basic service container
- Can perform Constructor, Setter, and Property Injection
- Can recursively resolve other services as arguments
- Will throw exception on resolving a circular dependency
- Unit test coverage at 100%
- TODO: Tags and scoped services
  • Loading branch information
Brandon Eum authored and Brandon Eum committed Jul 5, 2013
1 parent b636c08 commit 94d1a18
Show file tree
Hide file tree
Showing 21 changed files with 2,240 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/nbproject
lib-cov
coverage.html
node_modules
.DS_Store
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License

Copyright (c) 2011, 2012 Brandon Eum and contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
unittest:
export NODE_TEST_ENV=test && \
./node_modules/mocha/bin/mocha --reporter spec --recursive

coverage:
./node_modules/jscoverage/bin/jscoverage lib lib-cov && \
export NODE_TEST_ENV=coverage && \
./node_modules/mocha/bin/mocha --reporter html-cov --recursive > coverage.html

.PHONY: test
160 changes: 158 additions & 2 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,160 @@
service-container
=================
# Service Container


A Symfony2-style dependency injection library for Node.js

# Purpose


Inspired by the [Symfony2 Service Container](http://fabien.potencier.org/article/13/introduction-to-the-symfony-service-container)

A service container increases the usability of classes by allowing a configuration
file to specify which classes should be used to construct a class. The container
will construct an instance of the class for you using the parameters that you
specify, allowing you to specify different classes to use in different situations
or different environments.

For example, if you have a class that makes HTTP requests. In your production
environment you would want this to be a real http client. In your unit test
environment, there is significant complication and overhead in using a real HTTP
client, so you would probably want to use a mock class instead that delivers canned
and predictable responses.

With a service container, you can reconfigure your class through a configuration
file without actually touching your code.

The Symfony 2 Guide Book provides a great [explanation](http://symfony.com/doc/current/book/service_container.html)


# Installation


npm install service-container

Or add to your package.json dependencies.


# Usage

There are two pieces to using the service container, the container itself and the
`services.json` configuration files.

The `services.json` file specifies [3 types of dependency injection](http://symfony.com/doc/current/components/dependency_injection/types.html):
* Constructor Injection
* Setter Injection
* Property Injection

Constructor injection allows you to specify JSON literal parameters as arguments
for your class instances or it will allow you to specify another service as an
argument (be careful about circular dependencies!).

Setter injection will attempt to call the method that you specify with the arguments
(either parameters or other services) that you define. This is useful for adding
dependency injection for existing libraries that do not conform to Constructor
Injection.

Property injection will directly attempt to set the property on the object with
the argument that you specify, either another service or a JSON literal parameter.


## Initializing and using the container

Create a container like:

```javascript
var ServiceContainer = require('service-container');

// Create an instance of the container with the environment option set
// This will include both services.json and services_[ENV].json files to override
// any environment-specific parameters
var options = {
env: 'test'
};
var container = ServiceContainer(__dirname, options);
```

Get an instance of a service like:

```javascript
// Get an example mailer service
var mailer = container.get('mailer');
mailer.sendMail('Hello');

// Get a parameter
var maxcount = container.getParameter('mailer.maxcount');
```

The section below goes over how to configure and construct services.

## services.json

When initializing the container, you'll pass it the root directory for it to search
for the services.json files. The Builder will recursively search through directories
below the root directory to search for services.json files. If the `env` option
is specified then files named services_[ENV].json will also be parsed. Allowing
you to override any parameters setup earlier.

The hierarchy of services and parameters are as follows:
* services.json files at lower folder levels are overridden by those at higher levels
* services.json files are overriden by services_[ENV].json files

This allows you to override the specifics of a module from the application level
and allows you to override any parameter based on whether this is your dev, test,
qa or production environment.

```javascript
{
"parameters": {
"dependency_service.file":"./CoolClass", // File paths are relative to the root passed into the builder
"dependency2_service.file":"./OtherClass",
"my_service.file":"MyClass",
"my_service.example_param":"yes", // Parameter names are arbitrary
"my_service.obj_param":{isExample: true} // Can use literals as parameters
},
"services": {
"dependency_service": {
"class":"%depency_service.file%",
"arguments":[] // No Arguments
},
"dependency2_service": {
"class":"%depency2_service.file%",
"arguments":[]
},
"my_service": {
"class":"%my_service.file%" // Parameters use % symbols, services use @
"arguments": ["@dependency_service", "%my_service.example_param%", "@?optional_service"] // Optional services have @? at the beginning
"calls": [
["setSecondDependency", ["@dependency2_service"]] // Method calls have the method name and an array of arguments
],
"properties": {
"myServiceProperty":"%my_service.obj_param%"
}
}
}
}
```

## Examples

Check out the `example` directory to see some of the more common use cases for a
service container.

Included:
* Basic usage
* Overriding/Using mocks with the environment option
* Using an argument as a service


##TODO
* tags
* scoped services

## Run the Tests

make test

## Create Unit Test Coverage

make coverage

Open the file `coverage.html` that gets generated in the root directory
25 changes: 25 additions & 0 deletions ServiceContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
*
* INDEX.JS
*
* Author: Brandon Eum
* Date: July 2013
*
******************************************************************************/

/**
* Construct a Builder object
*/
module.exports = (function () {
var fs, Builder, Container, Definition;
fs = require('fs');
Builder = require('./lib/Builder');
Container = require('./lib/Container');
Definition = require('./lib/Definition');

// Create an instance of the builder with the proper dependencies injected
var builder = new Builder(fs, require, Container, Definition);

// Return the initialized builder
return builder;
}());
16 changes: 16 additions & 0 deletions example/Messenger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Example class that logs messages to the console
*/
var Messenger = function Messenger(msg) {
this.message = msg || '';
};

Messenger.prototype.setMessage = function (msg) {
this.message = msg;
};

Messenger.prototype.printMessage = function () {
console.log(this.message);
};

module.exports = Messenger;
20 changes: 20 additions & 0 deletions example/MessengerManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Example class takes the example messenger classes and prints all messages
* to the console
*/
var MessengerManager = function MessengerManager() {
this.messengers = [];
};

MessengerManager.prototype.addMessenger = function (messenger) {
this.messengers.push(messenger);
};

MessengerManager.prototype.printAllMessages = function () {
var i;
for (i in this.messengers) {
this.messengers[i].printMessage();
}
};

module.exports = MessengerManager;
18 changes: 18 additions & 0 deletions example/MockMessenger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Example mock class
*
* You can achieve similar things with sinon.js for unit testing purposes
*/
var MockMessenger = function MockMessenger(msg) {
this.message = msg || '';
};

MockMessenger.prototype.setMessage = function (msg) {
this.message = msg;
};

MockMessenger.prototype.printMessage = function () {
console.log('This is a test: ' + this.message);
};

module.exports = MockMessenger;
32 changes: 32 additions & 0 deletions example/environmentOverrideExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* An example of how to override the parameters based on environment
*
* In the simpleExample we used the same class multiple times with different
* parameters to demonstrate the power of the service container. This time the
* demo is about overriding the messages and the service class using a separate
* services_test.json file.
*
*/

var ServiceContainer, container, helloMessenger, worldMessenger, exclamationMessenger;

// Create a service container with this directory as the root - this will load
// the services.json file from this directory and create service definitions
// Passing the options as the second parameter with the env specified as "test"
// will force the container to load services_test.json files
ServiceContainer = require('../ServiceContainer');
container = ServiceContainer.buildContainer(__dirname, {env: 'test'});

// Grab instances of the services - the underlying class will be different
helloMessenger = container.get('hello_messenger');
worldMessenger = container.get('world_messenger');
exclamationMessenger = container.get('exclamation_messenger');


// Using a different class, the printMessage function is overridden and appears
// differently
// This is great for injecting mock classes (with a mock library like sinon.js)
// to unit test specific JS Objects
helloMessenger.printMessage(); // Print "This is a test: Hello"
worldMessenger.printMessage(); // Print "This is a test: world"
exclamationMessenger.printMessage(); // Print "This is a test: !"
24 changes: 24 additions & 0 deletions example/serviceAsAnArgumentExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* An example of how to use another service as an argument
*
* In the services.json file for the messenger_manager service, the "addMessenger"
* method is called with the 3 services we saw in the simpleExample.js.
*
* By using Setter Injection through the calls array, we can add an arbitrary
* number of messenger services controlled through the configuration file and
* not through code.
*/

var ServiceContainer, container, helloMessenger, worldMessenger, exclamationMessenger;

// Create a service container with this directory as the root - this will load
// the services.json file from this directory and create service definitions
ServiceContainer = require('../ServiceContainer');
container = ServiceContainer.buildContainer(__dirname);

// Grab an instance of the messenger manager service
messengerManager = container.get('messenger_manager');

// Loop through all of the services that were added through setter injection and
// print their messages
messengerManager.printAllMessages();
36 changes: 36 additions & 0 deletions example/services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"parameters": {
"messenger.file":"./Messenger",
"messenger_manager.file":"./MessengerManager",
"hello_messenger.message":"Hello ",
"world_messenger.message":"World",
"exclamation_messenger.message":"!"
},
"services": {
"hello_messenger": {
"class":"%messenger.file%",
"arguments":["%hello_messenger.message%"]
},
"world_messenger": {
"class":"%messenger.file%",
"arguments":[],
"calls": [
["setMessage", ["%world_messenger.message%"]]
]
},
"exclamation_messenger": {
"class":"%messenger.file%",
"properties": {
"message":"%exclamation_messenger.message%"
}
},
"messenger_manager": {
"class":"%messenger_manager.file%",
"calls": [
["addMessenger", ["@hello_messenger"]],
["addMessenger", ["@world_messenger"]],
["addMessenger", ["@exclamation_messenger"]]
]
}
}
}
5 changes: 5 additions & 0 deletions example/services_test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"parameters": {
"messenger.file":"./MockMessenger"
}
}
Loading

0 comments on commit 94d1a18

Please sign in to comment.