Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved submodule support #670

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 99 additions & 17 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ var DEFAULT_CLONE_DEPTH = 20,
env = {},
configSources = [], // Configuration sources - array of {name, original, parsed}
checkMutability = true, // Check for mutability/immutability on first get
gitCryptTestRegex = /^.GITCRYPT/; // regular expression to test for gitcrypt files.
gitCryptTestRegex = /^.GITCRYPT/, // regular expression to test for gitcrypt files.
moduleConfigs = {}; // Storage for submodule configs

/**
* <p>Application Configurations</p>
Expand Down Expand Up @@ -172,7 +173,17 @@ Config.prototype.get = function(property) {
checkMutability = false;
}
var t = this,
value = getImpl(t, property);
value = getImpl(t, property),
tmpValue;

// If value is an object it may be only partial, check to see if the
// full object is in dynamicallyImportedModules
if (value === undefined || util.isObject(value)) {
tmpValue = getImpl(moduleConfigs, property);
if (tmpValue !== undefined) {
value = tmpValue;
}
}

// Produce an exception if the property doesn't exist
if (value === undefined) {
Expand Down Expand Up @@ -203,7 +214,13 @@ Config.prototype.has = function(property) {
return false;
}
var t = this;
return (getImpl(t, property) !== undefined);
var value = getImpl(t, property);

// If the value is undefined, check to see if it was dynamically loaded later
if (value === undefined) {
return (getImpl(moduleConfigs, property) !== undefined);
}
return (value !== undefined);
};

/**
Expand All @@ -226,7 +243,6 @@ Config.prototype.has = function(property) {
* });
* <br>
* // Template name may be overridden by application config files
* console.log("Template: " + CONFIG.MyModule.templateName);
* </pre>
*
* <p>
Expand All @@ -240,6 +256,12 @@ Config.prototype.has = function(property) {
* @return moduleConfig {object} - The module level configuration object.
*/
util.setModuleDefaults = function (moduleName, defaultProperties) {

// Throw error if submodule defaults has already been registered
var localModuleConfig = getImpl(moduleConfigs, moduleName);
if (localModuleConfig !== undefined) {
throw new Error('Submodule "' + moduleName + '" has already been set');
}

// Copy the properties into a new object
var t = this,
Expand All @@ -255,26 +277,86 @@ util.setModuleDefaults = function (moduleName, defaultProperties) {
util.setPath(configSources[0].parsed, moduleName.split('.'), {});
util.extendDeep(getImpl(configSources[0].parsed, moduleName), defaultProperties);

// Create a top level config for this module if it doesn't exist
util.setPath(t, moduleName.split('.'), getImpl(t, moduleName) || {});
// override default properties with local overrides
util.extendDeep(moduleConfig, util.cloneDeep(getImpl(t, moduleName)) || {});

// Extend local configurations into the module config
util.extendDeep(moduleConfig, getImpl(t, moduleName));
// Create a module entry populated with defaults and overrides
util.setPath(moduleConfigs, moduleName.split('.'), moduleConfig || {});

// Merge the extended configs without replacing the original
util.extendDeep(getImpl(t, moduleName), moduleConfig);
// Attach handlers & watchers onto the module config object
util.attachProtoDeep(getImpl(moduleConfigs, moduleName));

// reset the mutability check for "config.get" method.
// we are not making t[moduleName] immutable immediately,
// since there might be more modifications before the first config.get
// Make the settings for this module immutable
if (!util.initParam('ALLOW_CONFIG_MUTATIONS', false)) {
checkMutability = true;
util.makeImmutable(moduleConfigs, moduleName.split('.').shift());
}

// Attach handlers & watchers onto the module config object
return util.attachProtoDeep(getImpl(t, moduleName));
return getImpl(moduleConfigs, moduleName);
};


/**
* <p>
* Retrieve registered properties for your node.js module with overrides
* applied.
* </p>
*
* <p>
* This allows module developers to account for multiple instances of their
* module by generating configurations at will. The returned moduleConfig
* object will have the same lifecycle as the node module instance.
* </p>
*
* <p>Using the function within your module:</p>
* <pre>
* var CONFIG = require("config");
* CONFIG.util.setModuleDefaults("MyModule", {
* &nbsp;&nbsp;templateName: "t-50",
* &nbsp;&nbsp;colorScheme: "green"
* });
* <br>
* function MyModuleConstructor(options) {
* this.moduleConfig = config.getModuleConfig("MyModule", options);
* }
* <br>
* MyModuleConstructor.prototype.getColorScheme = function() {
* return this.moduleConfig.get('colorScheme');
* }
* <br>
* module.exports = MyModuleConstructor;
* <br>
* // Template name may be overridden by application config files
* </pre>
*
* <p>
* The above example results in a "MyModule" element of the configuration
* object, containing an object with the specified default values.
* </p>
*
* @method getModuleConfig
* @param moduleName {string} - Name of your module.
* @param [overrideProperties] {object} - The properties to override the default options.
* @return moduleConfig {object} - The module level configuration object with overrides applied.
*/
util.getModuleConfig = function(moduleName, overrideProperties) {
// Throw error if submodule defaults has already been registered
var moduleConfig,
localModuleConfig = getImpl(moduleConfigs, moduleName);

if (localModuleConfig === undefined) {
throw new Error('Submodule "' + moduleName + '" defaults has not been set');
}

moduleConfig = util.extendDeep(util.cloneDeep(localModuleConfig), util.cloneDeep(overrideProperties || {}));
util.attachProtoDeep(moduleConfig);

if (!util.initParam('ALLOW_CONFIG_MUTATIONS', false)) {
util.makeImmutable(moduleConfig);
}

return moduleConfig;
}

/**
* <p>Make a configuration property hidden so it doesn't appear when enumerating
* elements of the object.</p>
Expand Down Expand Up @@ -1311,7 +1393,7 @@ util.extendDeep = function(mergeInto) {

// Copy property descriptor otherwise, preserving accessors
else if (Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)){
Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop));
Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop));
} else {
mergeInto[prop] = mergeFrom[prop];
}
Expand Down
6 changes: 3 additions & 3 deletions test/2-config-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,14 @@ vows.describe('Test suite for node-config')
},

'The module config is in the CONFIG object': function(moduleConfig) {
assert.isObject(MODULE_CONFIG.TestModule);
assert.deepEqual(MODULE_CONFIG.TestModule, moduleConfig);
assert.isObject(MODULE_CONFIG.get('TestModule'));
assert.deepEqual(MODULE_CONFIG.get('TestModule'), moduleConfig);
},

// Regression test for https://github.com/lorenwest/node-config/issues/518
'The module config did not extend itself with its own name': function(moduleConfig) {
assert.isFalse('TestModule' in moduleConfig);
assert.isFalse('TestModule' in MODULE_CONFIG.TestModule);
assert.isFalse('TestModule' in MODULE_CONFIG.get('TestModule'));
},

'Local configurations are mixed in': function(moduleConfig) {
Expand Down
163 changes: 163 additions & 0 deletions test/21-dynamic-module-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
var requireUncached = require('./_utils/requireUncached');

// Dependencies
var vows = require('vows'),
assert = require('assert'),
FileSystem = require('fs');

/**
* <p>Unit tests for the node-config library. To run type:</p>
* <pre>npm test</pre>
* <p>Or, in a project that uses node-config:</p>
* <pre>npm test config</pre>
*
* @class ConfigTest
*/

var config;
vows.describe('Dynamically module import test')
.addBatch({
'Library initialization': {
topic : function () {
// Change the configuration directory for testing
process.env.NODE_CONFIG_DIR = __dirname + '/22-dynamic-modules';

// Hard-code $NODE_ENV=test for testing
process.env.NODE_ENV='development';

config = requireUncached(__dirname + '/../lib/config');

return config;

},
'Config library is available': function() {
assert.isObject(config);
},
'Config settings for Stooge is available': function() {
assert.deepEqual(config.Stooge, {
smart: true
});
},
'Make configs immutable': function() {
assert.deepEqual(config.util.cloneDeep(config.get('Stooge')), {
smart: true
});
}
},
})
.addBatch({
'Load Stooge Library Dynamically': {
topic : function () {
// Dynamically load the Stooge module
return require(__dirname + '/22-dynamic-modules/Stooge.js')
},
'Config override settings for Stooge match': function(Stooge) {
assert.deepEqual(config.Stooge, {
smart: true
});
},
'Config module defaults for Stooge are overridden': function(Stooge) {
assert.deepEqual(config.util.cloneDeep(config.get('Stooge')), {
bald: false,
happy: true,
smart: true,
canPlayViolin: false
});
},
'Create Moe with overrides and verify his settings': function(Stooge) {
var moe = new Stooge({
happy: false
});
assert.deepEqual(config.util.cloneDeep(moe.moduleConfig), {
bald: false,
happy: false,
smart: true,
canPlayViolin: false
});
assert.equal(moe.isBald(), false);
assert.equal(moe.isHappy(), false);
assert.equal(moe.isSmart(), true);
assert.throws(function () {
moe.playViolin();
}, {
name: "Error",
message: "I'm a victim of soikemstance!"
});
},
'Create Larry with overrides and verify his settings': function(Stooge) {
var larry = new Stooge({
happy: true,
smart: false,
canPlayViolin: true
});
assert.deepEqual(config.util.cloneDeep(larry.moduleConfig), {
bald: false,
happy: true,
smart: false,
canPlayViolin: true
});
assert.equal(larry.isBald(), false);
assert.equal(larry.isHappy(), true);
assert.equal(larry.isSmart(), false);
assert.equal(larry.playViolin(), true);
},
'Create Curly with overrides and verify his settings': function(Stooge) {
var curly = new Stooge({
bald: true,
happy: true,
smart: false
});
assert.deepEqual(config.util.cloneDeep(curly.moduleConfig), {
bald: true,
happy: true,
smart: false,
canPlayViolin: false
});
assert.equal(curly.isBald(), true);
assert.equal(curly.isHappy(), true);
assert.equal(curly.isSmart(), false);
assert.throws(function () {
curly.playViolin();
}, {
name: "Error",
message: "I'm a victim of soikemstance!"
});
},
'Config module defaults for Stooge haven\'t changed with each module instantiation': function(Stooge) {
assert.deepEqual(config.util.cloneDeep(config.get('Stooge')), {
bald: false,
happy: true,
smart: true,
canPlayViolin: false
});
},
}
})
.addBatch({
'Load TestModule Library Dynamically': {
topic : function () {
// Dynamically load the TestModule module
return require(__dirname + '/22-dynamic-modules/TestModule.js')
},
'Config override settings for TestModule match': function(TestModule) {
assert.isUndefined(config.TestModule);
},
'Config module defaults for TestModule are overridden': function(TestModule) {
assert.deepEqual(config.util.cloneDeep(config.get('TestModule')), {
test: true,
example: true
});
},
'Create Moe with overrides and verify his settings': function(TestModule) {
var testModule = new TestModule({
happy: false
});
assert.deepEqual(config.util.cloneDeep(testModule.moduleConfig), {
happy: false,
test: true,
example: true
});
}
}
})
.export(module);