diff --git a/.gitignore b/.gitignore index bff8718..ec7a8f1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.pid *.swp *.swo +*.iml node_modules checkstyle.xml loopback-boot-*.tgz diff --git a/lib/config-loader.js b/lib/config-loader.js index d9b503f..17834eb 100644 --- a/lib/config-loader.js +++ b/lib/config-loader.js @@ -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 diff --git a/test/compiler.test.js b/test/compiler.test.js index ee712d1..73d812b 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -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(/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(/`nested` is not a value type/); + .to.throw(/incompatible types.*key\[0\]/); }); it('merges app configs from multiple files', function() {