├── test ├── loader │ ├── sub │ │ ├── hello.js │ │ ├── second.json │ │ └── with-options.js │ └── first.json └── index.test.js ├── .travis.yml ├── .babelrc ├── .npmignore ├── CHANGELOG.md ├── .editorconfig ├── .jshintrc ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── src └── index.js /test/loader/sub/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = 'world'; 2 | -------------------------------------------------------------------------------- /test/loader/sub/second.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "./hello" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - 'iojs' 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "add-module-exports" ], 3 | "presets": [ "es2015" ] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .babelrc 5 | .idea/ 6 | src/ 7 | test/ 8 | !lib/ 9 | -------------------------------------------------------------------------------- /test/loader/sub/with-options.js: -------------------------------------------------------------------------------- 1 | module.exports = function(options) { 2 | return new Promise(resolve => 3 | setTimeout(() => resolve({ 4 | result: options.path.join(options.test, options.other) 5 | }), 100) 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.1.0](https://github.com/daffl/json-di/tree/v0.1.0) (2016-06-30) 4 | 5 | 6 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/loader/first.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "./sub/with-options", 3 | "options": [{ 4 | "test": { 5 | "require": "./sub/second.json" 6 | }, 7 | "other": "thing", 8 | "path": { "require": "path" } 9 | }], 10 | "extended": { "require": "./sub/hello" } 11 | } 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": false, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": false, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": false, 21 | "node": true, 22 | "globals": { 23 | "it": true, 24 | "describe": true, 25 | "before": true, 26 | "beforeEach": true, 27 | "after": true, 28 | "afterEach": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | # The compiled/babelified modules 33 | lib/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-di", 3 | "description": "Dependency injection through JSON JavaScript objects", 4 | "version": "0.1.0", 5 | "homepage": "https://github.com/daffl/json-di", 6 | "main": "lib/", 7 | "keywords": [ 8 | "json", 9 | "dependency-injection" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/daffl/json-di.git" 15 | }, 16 | "author": { 17 | "name": "David Luecke", 18 | "email": "daff@neyeon.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [], 22 | "bugs": { 23 | "url": "https://github.com/daffl/json-di/issues" 24 | }, 25 | "engines": { 26 | "node": ">= 0.12.0" 27 | }, 28 | "scripts": { 29 | "prepublish": "npm run compile", 30 | "publish": "npm run changelog && git push origin && git push origin --tags", 31 | "release:patch": "npm version patch && npm publish", 32 | "release:minor": "npm version minor && npm publish", 33 | "release:major": "npm version major && npm publish", 34 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 35 | "compile": "rimraf lib/ && babel -d lib/ src/", 36 | "watch": "babel --watch -d lib/ src/", 37 | "jshint": "jshint src/. test/. --config", 38 | "mocha": "mocha --recursive test/ --compilers js:babel-core/register", 39 | "test": "npm run compile && npm run jshint && npm run mocha" 40 | }, 41 | "directories": { 42 | "lib": "lib" 43 | }, 44 | "dependencies": { 45 | "debug": "^2.2.0", 46 | "lodash": "^4.13.1" 47 | }, 48 | "devDependencies": { 49 | "babel-cli": "^6.10.1", 50 | "babel-core": "^6.10.4", 51 | "babel-plugin-add-module-exports": "^0.2.1", 52 | "babel-preset-es2015": "^6.9.0", 53 | "jshint": "^2.9.2", 54 | "mocha": "^2.5.3", 55 | "rimraf": "^2.5.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-di 2 | 3 | [![Build Status](https://travis-ci.org/daffl/json-di.png?branch=master)](https://travis-ci.org/daffl/json-di) 4 | 5 | > JSON Dependency Injection 6 | 7 | ## What is it? 8 | 9 | `json-di` is a module for loading and initializing other modules through JSON configuration files. The main reason for this is that JSON files are much easier to modify and analyze than source code directly. They allow tools like generators to understand and modify your application structure and dependencies which otherwise would only be possible with messy templates and brittle abstract syntax tree transformations. 10 | 11 | [feathers-bootstrap](https://github.com/feathersjs/feathers-bootstrap) uses it to easily create and configure [Feathers](http://feathersjs.com/) applications. 12 | 13 | ## Usage 14 | 15 | > npm install node-di 16 | 17 | `node-di` requires a data object, a parent filename (usually `__dirname`) and an optional converter which can run to convert properties and returns a standard `Promise`: 18 | 19 | ```js 20 | const di = require('node-di'); 21 | 22 | di({ 23 | path: { require: "path" }, 24 | otherOption: 'test' 25 | }, __dirname).then(result => { 26 | // result.path === Node's `path` module 27 | // otherOption === 'test' 28 | }); 29 | ``` 30 | 31 | ### `require` 32 | 33 | - `{ "require": "modulename" }` will load the module `modulename` 34 | - `{ "require": "./mymodule" }` will load `mymodule.js` relative to the JSON file 35 | 36 | ### `options` 37 | 38 | If the module declared in `require` returns a function the `options` property will be passed as the `arguments` to that function. The function can return a promise in which case it will wait until the promise is resolved. With a `mainmodule.js` like this: 39 | 40 | ```js 41 | module.exports = function(options) { 42 | return new Promise(resolve => { 43 | setTimeout(() => resolve(`Hello ${options.text}`), 500); 44 | }); 45 | } 46 | ``` 47 | 48 | A `world.js` like this: 49 | 50 | ```js 51 | module.exports = 'World'; 52 | ``` 53 | 54 | And a `main.json` like this: 55 | 56 | ```js 57 | { 58 | "require": "./mainmodule", 59 | "options": [{ 60 | "text": { "require": "./world" } 61 | }] 62 | } 63 | ``` 64 | 65 | `node-di` can be used like this: 66 | 67 | ```js 68 | const di = require('node-di'); 69 | 70 | di({ 71 | require: "./main.json" 72 | }, __dirname).then(result => { 73 | // result === 'Hello World' 74 | }); 75 | ``` 76 | 77 | ## License 78 | 79 | Copyright (c) 2016 80 | 81 | Licensed under the [MIT license](LICENSE). 82 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import path from 'path'; 3 | import loadAndProcess from '../src/'; 4 | 5 | const { load, process } = loadAndProcess; 6 | 7 | describe('json-di', () => { 8 | it('is CommonJS compatible', () => { 9 | assert.equal(typeof require('../lib'), 'function'); 10 | }); 11 | 12 | it('`load` and processes configuration files recursively', function() { 13 | const result = load({ 14 | require: './loader/first.json' 15 | }, __filename); 16 | 17 | assert.equal(result.module.options[0].test.module.module, 'world'); 18 | }); 19 | 20 | it('`load` can require Node builtins', function() { 21 | const result = load({ 22 | require: 'path' 23 | }, __filename); 24 | 25 | assert.equal(result.module, require('path')); 26 | }); 27 | 28 | it('`process` returns an error when options is not an array', function(done) { 29 | process({ 30 | require: 'somewhere.json', 31 | module: { 32 | module: function(opts) { 33 | return `${opts.test} ran`; 34 | }, 35 | options: { test: 'testing' } 36 | } 37 | }).then(() => { 38 | assert.equal(true, false, 'Should not get here'); 39 | done(); 40 | }).catch(error => { 41 | assert.notEqual(error, undefined); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('`process` simple processing', function(done) { 47 | process({ 48 | require: 'somewhere.json', 49 | module: { 50 | module: function(opts) { 51 | return `${opts.test} ran`; 52 | }, 53 | options: [{ test: 'testing' }] 54 | } 55 | }).then(data => { 56 | assert.equal(data, 'testing ran'); 57 | done(); 58 | }).catch(done); 59 | }); 60 | 61 | it('`process` result is extended with additional data', function(done) { 62 | process({ 63 | require: 'somewhere.json', 64 | module: { 65 | module: function(opts) { 66 | return { result: `${opts.test} ran` }; 67 | }, 68 | options: [{ test: 'testing' }], 69 | hi: 'there', 70 | otherOption: { test: true } 71 | } 72 | }).then(data => { 73 | assert.deepEqual(data, { 74 | result: 'testing ran', 75 | hi: 'there', 76 | otherOption: { test: true } 77 | }); 78 | done(); 79 | }).catch(done); 80 | }); 81 | 82 | it('does not process normal modules', function(done) { 83 | process({ 84 | module: require('path') 85 | }).then(path => { 86 | assert.equal(path, require('path')); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('`process` with promises from functions', function(done) { 92 | process({ 93 | require: 'somewhere.json', 94 | module: { 95 | module: function(options) { 96 | return new Promise(resolve => 97 | setTimeout(() => resolve(`${options.test} ran`), 100) 98 | ); 99 | }, 100 | options: [{ test: 'testing' }] 101 | } 102 | }).then(data => { 103 | assert.equal(data, 'testing ran'); 104 | done(); 105 | }).catch(done); 106 | }); 107 | 108 | it('`process` simple conversion', function(done) { 109 | process({ 110 | test: 'thing' 111 | }, () => 'converted').then(data => { 112 | assert.equal(data.test, 'converted'); 113 | done(); 114 | }).catch(done); 115 | }); 116 | 117 | it('`process` processes a JSON configuration', function(done) { 118 | const loaded = load({ 119 | require: './loader/first.json' 120 | }, __filename); 121 | 122 | process(loaded).then(data => { 123 | assert.deepEqual(data, { 124 | result: path.join('world', 'thing'), 125 | extended: 'world' 126 | }); 127 | done(); 128 | }).catch(done); 129 | }); 130 | 131 | it('`default` processes a JSON configuration runs a converter', function(done) { 132 | const converter = value => value === 'thing' ? 'converted' : value; 133 | 134 | loadAndProcess({ 135 | require: './loader/first.json' 136 | }, __filename, converter).then(data => { 137 | assert.deepEqual(data, { 138 | result: path.join('world', 'converted'), 139 | extended: 'world' 140 | }); 141 | done(); 142 | }).catch(done); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import each from 'lodash/each'; 3 | import map from 'lodash/map'; 4 | import omit from 'lodash/omit'; 5 | import clone from 'lodash/cloneDeep'; 6 | import isEmpty from 'lodash/isEmpty'; 7 | import assign from 'lodash/assignIn'; 8 | 9 | const debug = require('debug')('json-di'); 10 | const keywords = [ 'options', 'module', 'require' ]; 11 | const extendProcessed = (obj, data) => { 12 | const toAssign = omit(data, keywords); 13 | 14 | if(!isEmpty(toAssign)) { 15 | debug('Extending result with additional data', obj, toAssign); 16 | return assign(obj, toAssign); 17 | } 18 | 19 | return obj; 20 | }; 21 | 22 | function load(mod, parent) { 23 | const data = clone(mod || {}); 24 | const name = data.require; 25 | 26 | let location = ''; 27 | let loadedModule; 28 | 29 | // If the object has a `require` statement 30 | if(name) { 31 | const root = path.dirname(parent); 32 | debug(`Loading require ${name}`); 33 | 34 | // The location of the file to require 35 | try { 36 | location = require.resolve(name); 37 | debug(`require.resolve for ${name} returned ${location}`); 38 | } catch(error) { 39 | location = path.isAbsolute(name) ? name : path.join(root, name); 40 | debug(`resolved location for ${name} to ${location}`); 41 | } 42 | 43 | try { 44 | loadedModule = require(location); 45 | } catch(error) { 46 | throw new Error(`Can not load module ${name} defined in ${parent} (\`${JSON.stringify(data)}\`)`); 47 | } 48 | 49 | if(path.extname(name) === '.json') { 50 | // if it s a JSON configuration file, clone its data 51 | // (since we are modifying it and required JSON files are singletons) 52 | // and process it recursively 53 | debug(`Recursively loading JSON configuration ${name}`); 54 | data.module = load(loadedModule, location); 55 | } else { 56 | data.module = loadedModule; 57 | } 58 | } 59 | 60 | // Except for `require` keys, process all other keys 61 | each(data, (value, key ) => { 62 | if(typeof value === 'object' && key !== 'module') { 63 | data[key] = load(value, parent); 64 | } 65 | }); 66 | 67 | return data; 68 | } 69 | 70 | function process(data, convert = value => value) { 71 | if(typeof data !== 'object') { 72 | return Promise.resolve(data); 73 | } 74 | 75 | if(Array.isArray(data)) { // Map data arrays 76 | return Promise.all(data.map(current => process(current, convert))); 77 | } 78 | 79 | const result = {}; 80 | 81 | // Process all options, run the converter or process recursively 82 | return Promise.all(map(data, (value, key) => { 83 | if(key !== 'module') { 84 | const processed = typeof value === 'object' ? process(value, convert) : 85 | Promise.resolve(convert(value, key, data)); 86 | 87 | return processed.then(data => result[key] = data); 88 | } 89 | 90 | return Promise.resolve(); 91 | })) 92 | .then(() => { 93 | const mod = data.module; 94 | 95 | // If this is a loaded module 96 | if(typeof mod !== 'undefined') { 97 | // If the module is a function and `options` or `arguments` 98 | // is specified in the configuration, run that function with options or arguments 99 | if(result.options && typeof mod === 'function') { 100 | if (!Array.isArray(result.options)) { 101 | throw new Error(`Options for ${data.require} have to be an array`); 102 | } 103 | 104 | const args = result.options; 105 | 106 | debug(`Calling function returned from ${data.require} with`, args); 107 | 108 | // Call the module with the given arguments since it can return a 109 | // promise we'll always wrap it before extending the result with 110 | // any additional data 111 | return Promise.resolve(mod.call(this, ...args)) 112 | .then(modResult => extendProcessed(modResult, result)); 113 | } 114 | 115 | // If we are loading a JSON configuration file, process it recursively 116 | if(data.require && path.extname(data.require) === '.json') { 117 | debug(`Recursively processing JSON configuration file ${data.require}`); 118 | return process(mod, convert); 119 | } 120 | 121 | // Otherwise just return the module 122 | return mod; 123 | } 124 | 125 | return result; 126 | }); 127 | } 128 | 129 | export default function loadAndProcess(data, parent, convert) { 130 | const loaded = load(data, parent); 131 | 132 | return process(loaded, convert); 133 | } 134 | 135 | assign(loadAndProcess, { load, process }); 136 | --------------------------------------------------------------------------------