Heavily inspired by Symfony Dependency Injecion Component
You will need node v6.0.0
or higher to be able to use canister. Please get in touch or submit a PR if you need compatibility with lower versions or iojs.
yarn add canister.js
//or
npm install --save canister.js
The parser now accepts dictionary on each parse call rather than on construction. If you used the default bootstrap then there's no changes to be made, if you decided to assemble the container yourself you will need to adjust.
Canister.js provides a simplistic bootstrap method as its default export, it will take a path to yaml config as well as the root dir of your modules (or assume process.cwd()
if not provided).
const canister = require('canister.js')(
'./path/to/my/config.yml',
__dirname
);
if(process.env.NODE_ENV === 'test') {
canister.configure('./test.services.yml'); //load test mocks
}
canister.env(); //load environment overrides
const container = canister.build();
container.get('reference');
If you require more control over configuration, you can assemble the container yourself using the exposed API, below will achieve the same result as the bootstrap above:
const canister = require('canister.js');
const moduleLoader = new canister.ModuleLoader(__dirname);
const builder = new canister.Builder(moduleLoader);
const yamlLoader = new canister.definitionLoader.YAML();
const envLoader = new canister.definitionLoader.Environment();
yamlLoader.fromFile('./path/to/my/config.yml');
envLoader.load();
const parser = new canister.Parser();
for (let definition of parser.parse(yamlLoader.toJS())) {
builder.addDefinition(definition);
}
for (let definition of parser.parse(envLoader.toJS())) {
builder.addDefinition(definition);
}
const container = builder.build();
Currently, only yml configuration is supported, following examples cover allowed schema.
parameters:
my.parameter.name: "string value"
components:
my.bespoke.module: { module: './relative/to/given/root' }
my.node.module:
module: http
my.absolute.module:
module: /absolute/path/to/a/module
components:
my.module.property: { property: './module::PropertyPath.PropertyName' }
my.node.property:
property: 'path::join'
components:
my.class.as.only.export: {class: './module' }
my.class:
class: './module::ClassName'
my.instances:
class: './module::OtherClass'
transient: true
Where a class is the only export from a module you need to provide the module path as the sole value of class
key.
By default, the container will only ever return the same instance of a Class on each request, to change that behaviour you need to provide the transient: true
flag.
components:
my.class:
class: ./module::ClassName
with:
- first argument
- 2
- - 1
- 2
Above will result in an invocation equivalent to new require('./module').ClassName)('first argument', 2, [1, 2])
.
Classes with constructor arguments can also be transient.
components:
my.factory.as.only.export: {factory: './module' }
my.factory:
factory: './module::factoryFunction'
with:
- first argument
- 2
my.instances:
factory: './module::factoryFunction'
transient: true
components:
my.class:
class: ./module::ClassName
call:
- method: setValue
with:
- key
- value
Class arguments can reference another component registered in the container. To do this, you need to prefix reference name with @
parameters:
db.host: mongodb://localhost
components:
db.connection:
class: ./db
with:
- '@db.host'
user.repository:
class: ./repository::User
with:
- '@db.connection'
Canister will build components using an order obtained from dependency graph, if cyclic dependency is detected it will fail hard during build phase.
Useful when you want to get access to some properties of a registered component without having to define each one as a property.
Kinda bad:
components:
name:
property: './package.json::name'
logger:
factory: 'bunyan::createLogger'
with:
- {name: '@name'}
Much better:
components:
pkg:
module: './package.json'
logger:
factory: 'bunyan::createLogger'
with:
- {name: '@pkg::name'}
While not recommended it is possible to pass the entire container as an argument. To do this just reference it with @canister
.
components:
my.class:
class: ./module::ClassName
tags:
myTagName: myTagValue
'my tag name': 'my tag value'
By using tags you can dynamically modify definitions held by the builder before the container is built. This allows for 'dynamic' service composition as illustrated by this example.
If you are developig a library or framework that can expose canister.js for further configuration, you need to ensure that the components pulled in via your configuration come from your library node modules and not from the implementing project.
You will need to provide a second parameter to the parser which is a basePath and your components need to provide relative path to node_modules starting with __/
const parser = new canister.Parser(yamlLoader.toJS(), __dirname);
components:
express:
module: __/src/logger
You can then expose alternative loader and parser for the implementing application.
You can use the supplied environment loader during container build, this method is available via both the simplistic bootstrap and manual assembly (check examples above).
By default the env loader will load all variables starting with CONFIG_
prefix, lowercasing all keys and replacing __
with .
. Since environment variables are all strings the default config for this loader will cast them to the 'nearest' type. Example below:
process.env.CONFIG_A=1 ==> {a: 1}
process.env.CONFIG_B='1' ==> {b: 1}
process.env.CONFIG_C='1e1' ==> {c: 10} // watch out on this one, it will also take bi/oct/hex notations
process.env.CONFIG_D='true' ==> {d: true}
process.env.CONFIG_E='false' ==> {e: false}
process.env.CONFIG_F='abc' ==> {f: 'abc}
process.env.CONFIG_G__H__I_J='abc' ==> {'g.h.u_j': 'abc}
This behaviour can be controlled by configuring env loader according to below interface:
envLoader.load({
prefix: RegExp,
getKey: fn(key, prefix) : string,
castValue: fn(value) : mixed
});
Environment loader can only load parameters, not components.
One the container is built, it will be frozen so no further modifications are possible.
The only method exposed by container is get(key)
which returns value under requested key.
All contributions are more than welcome. Fork and PR not forgetting about linting and testing.
MIT