Merge pull request #54 from strongloop/feature/support-nested-values-in-config-overrides

Support nested values in config overrides
This commit is contained in:
Miroslav Bajtoš 2014-10-09 19:33:13 +02:00
commit 38c4944e2e
3 changed files with 210 additions and 15 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
*.pid
*.swp
*.swo
*.iml
node_modules
checkstyle.xml
loopback-boot-*.tgz

View File

@ -112,7 +112,7 @@ function mergeConfigurations(configObjects, mergeFn) {
function mergeDataSourceConfig(target, config, fileName) {
for (var ds in target) {
var err = applyCustomConfig(target[ds], config[ds]);
var err = mergeObjects(target[ds], config[ds]);
if (err) {
throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err);
}
@ -120,23 +120,73 @@ function mergeDataSourceConfig(target, config, fileName) {
}
function mergeAppConfig(target, config, fileName) {
var err = applyCustomConfig(target, config);
var err = mergeObjects(target, config);
if (err) {
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}
function applyCustomConfig(target, config) {
function mergeObjects(target, config, keyPrefix) {
for (var key in config) {
var value = config[key];
if (typeof value === 'object') {
return 'override for the option `' + key + '` is not a value type.';
}
target[key] = value;
var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
var err = mergeSingleItemOrProperty(target, config, key, fullKey);
if (err) return err;
}
return null; // no error
}
function mergeSingleItemOrProperty(target, config, key, fullKey) {
var origValue = target[key];
var newValue = config[key];
if (!hasCompatibleType(origValue, newValue)) {
return 'Cannot merge values of incompatible types for the option `' +
fullKey + '`.';
}
if (Array.isArray(origValue)) {
return mergeArrays(origValue, newValue, fullKey);
}
if (typeof origValue === 'object') {
return mergeObjects(origValue, newValue, fullKey);
}
target[key] = newValue;
return null; // no error
}
function mergeArrays(target, config, keyPrefix) {
if (target.length !== config.length) {
return 'Cannot merge array values of different length' +
' for the option `' + keyPrefix + '`.';
}
// Use for(;;) to iterate over undefined items, for(in) would skip them.
for (var ix=0; ix < target.length; ix++) {
var fullKey = keyPrefix + '[' + ix + ']';
var err = mergeSingleItemOrProperty(target, config, ix, fullKey);
if (err) return err;
}
return null; // no error
}
function hasCompatibleType(origValue, newValue) {
if (origValue === null || origValue === undefined)
return true;
if (Array.isArray(origValue))
return Array.isArray(newValue);
if (typeof origValue === 'object')
return typeof newValue === 'object';
// Note: typeof Array() is 'object' too,
// we don't need to explicitly check array types
return typeof newValue !== 'object';
}
/**
* Try to read a config file with .json extension
* @param cwd Dirname of the file

View File

@ -125,24 +125,168 @@ describe('compiler', function() {
expect(db).to.have.property('fromJs', true);
});
it('refuses to merge Object properties', function() {
it('merges new Object values', function() {
var objectValue = { key: 'value' };
appdir.createConfigFilesSync();
appdir.writeConfigFileSync('datasources.local.json', {
db: { nested: { key: 'value' } }
db: { nested: objectValue }
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/`nested` is not a value type/);
var instructions = boot.compile(appdir.PATH);
var db = instructions.dataSources.db;
expect(db).to.have.property('nested');
expect(db.nested).to.eql(objectValue);
});
it('refuses to merge Array properties', function() {
it('deeply merges Object values', function() {
appdir.createConfigFilesSync({}, {
email: {
transport: {
host: 'localhost'
}
}
});
appdir.writeConfigFileSync('datasources.local.json', {
email: {
transport: {
host: 'mail.example.com'
}
}
});
var instructions = boot.compile(appdir.PATH);
var email = instructions.dataSources.email;
expect(email.transport.host).to.equal('mail.example.com');
});
it('deeply merges Array values of the same length', function() {
appdir.createConfigFilesSync({}, {
rest: {
operations: [
{
template: {
method: 'POST',
url: 'http://localhost:12345'
}
}
]
}
});
appdir.writeConfigFileSync('datasources.local.json', {
rest: {
operations: [
{
template: {
url: 'http://api.example.com'
}
}
]
}
});
var instructions = boot.compile(appdir.PATH);
var rest = instructions.dataSources.rest;
expect(rest.operations[0].template).to.eql({
method: 'POST', // the value from datasources.json
url: 'http://api.example.com' // overriden in datasources.local.json
});
});
it('merges Array properties', function() {
var arrayValue = ['value'];
appdir.createConfigFilesSync();
appdir.writeConfigFileSync('datasources.local.json', {
db: { nested: ['value'] }
db: { nested: arrayValue }
});
var instructions = boot.compile(appdir.PATH);
var db = instructions.dataSources.db;
expect(db).to.have.property('nested');
expect(db.nested).to.eql(arrayValue);
});
it('refuses to merge Array properties of different length', function() {
appdir.createConfigFilesSync({
nest: {
array: []
}
});
appdir.writeConfigFileSync('config.local.json', {
nest: {
array: [
{
key: 'value'
}
]
}
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/`nested` is not a value type/);
.to.throw(/array values of different length.*nest\.array/);
});
it('refuses to merge Array of different length in Array', function() {
appdir.createConfigFilesSync({
key: [[]]
});
appdir.writeConfigFileSync('config.local.json', {
key: [['value']]
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/array values of different length.*key\[0\]/);
});
it('returns full key of an incorrect Array value', function() {
appdir.createConfigFilesSync({
toplevel: [
{
nested: []
}
]
});
appdir.writeConfigFileSync('config.local.json', {
toplevel: [
{
nested: [ 'value' ]
}
]
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/array values of different length.*toplevel\[0\]\.nested/);
});
it('refuses to merge incompatible object properties', function() {
appdir.createConfigFilesSync({
key: []
});
appdir.writeConfigFileSync('config.local.json', {
key: {}
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/incompatible types.*key/);
});
it('refuses to merge incompatible array items', function() {
appdir.createConfigFilesSync({
key: [[]]
});
appdir.writeConfigFileSync('config.local.json', {
key: [{}]
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/incompatible types.*key\[0\]/);
});
it('merges app configs from multiple files', function() {