@00f100/jimple

A Dependency Injection container to NodeJS and to the browser developed in ES6

Usage no npm install needed!

<script type="module">
  import 00f100Jimple from 'https://cdn.skypack.dev/@00f100/jimple';
</script>

README

Jimple

Greenkeeper badge Build Status npm npm node Codacy Badge Code Climate Test Coverage Issue Count

This project is a port of Pimple Dependency Injection container for NodeJS and for browsers using features provided by ES6.

All the code is tested using Mocha and seems to be stable. Below is the documentation for the project:

Features

Good projects have good features. And because this here's the list of features that Jimple supports:

  • Define services;
  • Define factories;
  • Define parameters easily;
  • Defining services/parameters/factories from another files - because you should be able to split your configuration easily;
  • Simple API;
  • Runs on NodeJS and on browser;
  • Allows extending services easily;
  • Allow to get the raw service creator easily;
  • Pure Javascript;
  • Stable API;
  • No dependencies (in nodejs, in browser we need a shim);
  • No module loader integrated - You can use any module loader you want;
  • Fully tested on each commit;
  • 100% code coverage;
  • Fully Documented;
  • Less than 300 SLOC;
  • ~1KB minified and gzipped - Tested on CI using size-limit;
  • I already said that it have a really Simple API? :)

Testing without installing anything

If you liked that features, feel free to test Jimple free on a NodeJS environment without installing anything on your machine by using Runkit. Give it a try. :)

Installation

The installation of this package is very simple: In fact, it can be installed by just running:

    npm install --save @00f100/jimple

If using NodeJS (this installs the package based purely on ES 6)

If you want to use this package in the browser. You can also use the version provided by a CDN, like JSDelivr. So you can paste the code below on a page and start using Jimple really fast:

<script language="javascript" type="text/javascript" src="https://cdn.jsdelivr.net/npm/jimple@latest/src/Jimple.js"></script>

WARNING: Please note that the code above uses always the latest version of Jimple. In production, please replace latest with a valid version number from the Releases page or use Bower or NPM to install a fixed version for you. =)

Note that the browser version of this library uses a version compiled by Babel. And because this, and because browsers does not have great support to Map and Set yet, you will need to load babel-polyfill (or some other similar polyfill that implements Map and Set support) before loading this package on the browser.

Usage

Creating a Jimple Container is just a matter of creating a Jimple instance:

var Jimple = require("jimple");

var container = new Jimple();

In the browser, you can load Jimple using various ways:

  • AMD
    define(["jimple"], function(Jimple) {
        // Code using Jimple here..
    });
  • CommonJS/Browserify:
    var Jimple = require("jimple");
  • Script tag:
    <script language="javascript" src="path/to/Jimple.js"></script>

Again, it's important to note that Jimple needs of a polyfill to Map and Set classes - which are not supported by all the latest browsers actually - to do so, you can choice between some options, like babel-polyfill for example.

Jimple, as Pimple and many other dependency injections containers, manage two different kind of data: services and parameters.

Defining services

As Pimple describes, a service is an object that does something as part of a larger system. Examples of services: a database connection, a templating engine, or a mailer. Almost any global object can be a service.

Services in Jimple (and in Pimple too!) are defined by anonymous functions that return an instance of an object. Note that, in Jimple, you cannot use a generator as a service, as they will be detected as parameters, so, just pure functions can be a service. Different from Pimple, however, here we need to call the method set() on Jimple container, as Proxies in NodeJS seems to not be stable:

// define some services
container.set('session_storage', function (c) {
    return new SessionStorage('SESSION_ID');
});

container.set('session', function (c) {
    return new Session(c.get('session_storage'));
});

Notice that the anonymous function that define a service has access to the current container instance, allowing references to other services or parameters.

The objects are created on demand, just when you get them. The order of the definitions does not matter.

Using the defined services is very easy, too:

// get the session object
var session = container.get('session');

// the above call is roughly equivalent to the following code:
// var storage = new SessionStorage('SESSION_ID');
// var session = new Session(storage);

Defining factory services

By default, when you get a service, Jimple automatically cache it's value, returning always the same instance of it. If you want a different instance to be returned for all calls, wrap your anonymous function with the factory() method:

container.set('session', container.factory(function (c) {
    return new Session(c.get('session_storage'));
}));

Now, each time you call container.get('session'), a new instance of Session is returned for you.

Defining parameters

Defining a parameter allows to ease the configuration of your container from the outside and to store global values. In Jimple, parameters are defined as anything that it's not a function:

// define a parameter called cookie_name
container.set('cookie_name', 'SESSION_ID');

If you change the session_storage service definition like below:

container.set('session_storage', function (c) {
    return new SessionStorage(c.get('cookie_name'));
});

You can now easily change the cookie name by overriding the cookie_name parameter instead of redefining the service definition.

Defining parameters based on environment variables (NodeJS only)

Do you wanna do define parameters in the container based on environment variables? It's okay! You can define it easily like that:

//define parameter based on environment variable
container.set('cookie_name', process.env.COOKIE_NAME);

Optional/Default parameters/services

Not all services need all the services or parameters always, and you may do well with a default value for an parameter or service. In this case, you can do something like that:

container.set('session_storage', function (c) {
    return new SessionStorage(c.has('cookie_name') ? c.get('cookie_name') : 'COOKIE_ID');
});

In this example, if the parameter cookie_name does not exist, the SessionStorage will be instantiated with the default 'COOKIE_ID', and this works for services too. :)

Protecting parameters

Because Jimple see anything that is a function as a service, you need to wrap anonymous functions with the protect() method to store them as parameters:

container.set('random_func', container.protect(function () {
    return Math.random();
}));

Modifying Services after Definition

In some cases you may want to modify a service definition (note that you cannot extend a parameter, including protected parameters) after it has been defined. You can use the extend() method to define additional code to be run on your service just after it is created:

container.set('session_storage', function (c) {
    return new SessionStorage(c.get('cookie_name'));
});

container.extend('session_storage', function (storage, c) {
    storage.someMethod();

    return storage;
});

The first argument is the name of the service to extend, the second a function that gets access to the object instance and the container.

Extending a Container

If you use the same libraries over and over, you might want to reuse some services from one project to the next one; package your services into a provider by implementing the following object structure by duck-typing:

var provider = {
    "register": function(c) {
        // Define your services and parameters here
    }
}

Because JS has no support to interfaces yet, we cannot validate too much the structure of the provider.

After creating a object with that structure, you can register it in the container:

container.register(provider);

Extending a container from a file (NodeJS/Browserify only)

If you want to split your container's configuration (so each file is more..simple and specific), you can create multiple files like that:

file1.js:

module.exports.register = function(container) {
    // Define your services and parameters here
}

To load the package, you can do something like:

container.register(require("./file1"))

You can, inclusive, create directories with container's configuration, by doing something like that:

xpto/file1.js:

module.exports.register = function(container) {
    // Define your services and parameters here
}

xpto/file2.js:

module.exports.register = function(container) {
    // Define your services and parameters here
}

xpto/index.js:

module.exports.register = function(container) {
    container.register(require("./file1"));
    container.register(require("./file2"));   
}

And, finally, in some file creating the container:

container.register(require("./xpto"));

Note that the index.js file is loaded first on xpto directory, and that index.js file loads the files file1.js and file2.js present on that directory. You can do that for any number of directories. :)

Extending a container using the shorthand function

You can use the exported provider shorthand method to easily create your container's configuration with a simple callback:

const { provider } = require('jimple');

module.exports = provider((container) => {
    // Define your services and parameters here
});

It can be used exact same way as the previous method (from a file), but it also can be used to export multiple configurations at the same time:

const { provider } = require('jimple');

module.exports = {
    configurationA: provider((container) => { ... }),
    configurationB: provider((container) => { ... }),
};

Fetching the Service Creation Function

When you access an object, Jimple automatically calls the anonymous function that you defined, which creates the service object for you. If you want to get raw access to this function, but don't want to protect() that service, you can use the raw() method to access the function directly:

container.set('session', function (c) {
    return new Session(c.get('session_storage'));
});

var sessionFunction = container.raw('session');

Proxy - ES6

In ES6, we can use a Proxy object to define custom behavior for some fundamental operations (see more here clicking here). That allows us to customize Jimple to have a experience very near from that provided by Pimple, which can get services and parameters directly without calling get() or set services and parameters without calling set().

However, for access that mode you cannot use the Jimple constructor, but a static method called proxy(). So, the code below:

const container = Jimple.proxy();

container['session_storage'] = function (c) {
    return new SessionStorage('SESSION_ID');
};

container['session'] = function (c) {
    return new Session(c['session_storage']);
};

Is in fact equivalent to that:

const container = new Jimple();

container.set('session_storage', function (c) {
    return new SessionStorage('SESSION_ID');
});

container.set('session', function (c) {
    return new Session(c.get('session_storage'));
});

Please note that the proxy() method can receive a parameter, like the Jimple constructor. So you can note that:

const container = Jimple.proxy({"SESSION_ID": "test"});

Is in fact equivalent for:

const container = new Jimple({"SESSION_ID": "test"});

By the way, observe that Proxy is an API that's not really supported everywhere (it's supported in NodeJS >= 6, for example). So we do not recommend it's use in browser environments, for example.

Of course, this option has some limitations: basically, you CANNOT use certain names like the names of the methods available in the container as names of your services/parameters. So something like:

const container = Jimple.proxy();
container.set = 42;

Is forbidden and throws an exception automatically.

Last, but not least important: Customization

Do you wanna to customize Jimple's functionally? You can! Just extend it using ES6's class syntax:

var Jimple = require("jimple");

class ABigContainer extends Jimple {
    // Overwrite any of Jimple's method here, or add new methods
}

var container = new ABigContainer();

Good customization. :)