Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency injection library - renaming injected values

I'd like to inject lodash by name, something like this:

let val = function(lodash){
   // lodash will be injected, simply by using require('lodash');
};

but say I want to rename the import, I want do something like this:

let val = function({lodash:_}){

};

or

let val = function(lodash as _){

};

is there a way to do this with either ES6/ES7/ES8 or TypeScript?

Note that this DI framework does more work than just require('x')...it will try to inject other values first, if nothing else exists, then it will attempt to require the value.

Note also that the requirements here are that when you call val.toString() then "lodash" would be seen as the argument name. But _ instead of lodash would be seen at runtime inside the function body. This is because in order to inject lodash, we call fn.toString() to get the argument names.

like image 295
Alexander Mills Avatar asked Jun 18 '17 19:06

Alexander Mills


2 Answers

Update

Here's a link to the npm package di-proxy (inspired by this answer) with 100% code coverage, and support for memoization to increase performance, compatible with Node.js >=6.0.0.

Old answer

Here's an awesome solution I figured out while tinkering around with object destructuring and Proxy:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      /* this is just a demo, swap these two lines for actual injection */
      // return require(name);
      return { name };
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({ lodash: _, 'socket.io': sio, jquery: $, express, fs }, other, args) {
  // already have access to lodash, no need to even require() here
  console.log(_);
  console.log(sio);
  console.log($);
  console.log(express);
  console.log(fs);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');
.as-console-wrapper {
  max-height: 100% !important;
}

How it works

Passing parameters to a function via object destructuring invokes the getter methods for each property on the object literal in order to determine the values when the function is executed.

If the object being destructured is initialized as a Proxy, you can intercept each getter invocation with a reference to the property name attempting to be resolved, and return a value you choose to resolve it with. In this case, the resolution should be the require(name), which injects the module just by specifying it as a property name in the function object parameter.

Below is a link to a demo where you can actually see it working in Node.js.

Try it online!

Here's the code in that demo just for reference, because it demonstrates object destructuring to a larger degree:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      return require(name);
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({
  fs: { readFile: fsRead, writeFile: fsWrite },
  child_process: { fork: cpF, spawn: cpS, exec: cpE },
  events: { EventEmitter }
}, other, args) {
  // already have access to modules, no need to require() here
  console.log('fs:', { fsRead, fsWrite });
  console.log('child_process:', { fork: cpF, spawn: cpS, exec: cpE });
  console.log('EventEmitter:', EventEmitter);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');

As stated above, I have published a full npm package implementing this concept. I recommend you check it out if you like this syntax and want something a little more performant and tested than this very basic example.

like image 183
Patrick Roberts Avatar answered Oct 21 '22 01:10

Patrick Roberts


There's no syntax in JavaScript that supports such mapping. Even if custom function signature parser were written to to provide desired behaviour for destructured params like function({lodash:_}) ..., it would fail for transpiled functions, which is a major flaw. The most straightforward way to handle this is

function foo(lodash){
  const _ = lodash;
  ...
}

And it obviously won't work for invalid variable names like lodash.pick.

A common practice for DI recipes to do this is to provide annotations. All of described annotations can be combined together. They are particularly implemented in Angular DI. Angular injector is available for standalone use (including Node) as injection-js library.

Annotation property

This way function signature and the list of dependencies don't have to match. This recipe can be seen in action in AngularJS.

The property contains a list of DI tokens. They can be names of dependencies that will be loaded with require or something else.

// may be more convenient when it's a string
const ANNOTATION = Symbol();

...

foo[ANNOTATION] = ['lodash'];
function foo(_) {
  ...
}

bar[ANNOTATION] = ['lodash'];
function bar() {
  // doesn't need a param in signature
  const _ = arguments[0];
  ...
}

And DI is performed like

const fnArgs = require('fn-args');
const annotation = foo[ANNOTATION] || fnArgs(foo);
foo(...annotation.map(depName => require(depName));

This style of annotations disposes to make use of function definitions, because hoisting allows to place annotation above function signature for convenience.

Array annotation

Function signature and the list of dependencies don't have to match. This recipe can be seen in AngularJS, too.

When function is represented as an array, this means that it is annotated function, and its parameters should be treated as annotations, and the last one is function itself.

const foo = [
  'lodash',
  function foo(_) {
  ...
  }
];

...

const fn = foo[foo.length - 1];
const annotation = foo.slice(0, foo.length - 1);
foo(...annotation.map(depName => require(depName));

TypeScript type annotation

This recipe can be seen in Angular (2 and higher) and relies on TypeScript types. Types can be extracted from constructor signature and used for DI. Things that make it possible are Reflect metadata proposal and TypeScript's own emitDecoratorMetadata feature.

Emitted constructor types are stored as metadata for respective classes and can be retrieved with Reflect API to resolve dependencies. This is class-based DI, since decorators are supported only on classes, it works best with DI containers:

import 'core-js/es7/reflect';

abstract class Dep {}

function di(target) { /* can be noop to emit metadata */ }

@di
class Foo {
  constructor(dep: Dep) {
    ...
  }
}

...

const diContainer = { Dep: require('lodash') };
const annotations = Reflect.getMetadata('design:paramtypes', Foo);
new (Foo.bind(Foo, ...annotations.map(dep => diContainer [dep]))();

This will produce workable JS code but will create type issues, because Lodash object isn't an instance of Dep token class. This method is primarily effective for class dependencies that are injected into classes.

For non-class DI a fallback to other annotations is required.

like image 29
Estus Flask Avatar answered Oct 21 '22 03:10

Estus Flask