diff --git a/dot-net/Centroid.Tests/ConfigTest.cs b/dot-net/Centroid.Tests/ConfigTest.cs index 99a1153..e6f45b3 100644 --- a/dot-net/Centroid.Tests/ConfigTest.cs +++ b/dot-net/Centroid.Tests/ConfigTest.cs @@ -142,7 +142,7 @@ public void test_enumerating_json_object() } Assert.That(itemCount, Is.EqualTo(1)); } - + [Test] public void test_all_environment_is_not_case_sensitive() { @@ -154,5 +154,27 @@ public void test_all_environment_is_not_case_sensitive() var lowerCaseAllEnvironmentConfig = lowerCaseAllConfig.ForEnvironment("Prod"); Assert.That(lowerCaseAllEnvironmentConfig.AllOnly, Is.EqualTo("works")); } + + [Test] + public void supports_deep_merge() + { + const string json = @" + { + ""Dev"": { + ""Database"": { + ""Server"": ""the-dev-database"" + } + }, + ""All"": { + ""Database"": { + ""MigrationsPath"": ""path/to/migrations"" + } + } + }"; + + dynamic config = new Config(json).ForEnvironment("Dev"); + Assert.That(config.Database.Server, Is.EqualTo("the-dev-database")); + Assert.That(config.Database.MigrationsPath, Is.EqualTo("path/to/migrations")); + } } -} \ No newline at end of file +} diff --git a/dot-net/Centroid/Config.cs b/dot-net/Centroid/Config.cs index b3aca1f..3dce438 100644 --- a/dot-net/Centroid/Config.cs +++ b/dot-net/Centroid/Config.cs @@ -45,10 +45,8 @@ public dynamic ForEnvironment(string environment) return new Config(envConfig); } - foreach (var cfg in envConfig) - { - allConfig[cfg.Name] = cfg.Value; - } + MergeInto(allConfig, envConfig); + return new Config(allConfig); } @@ -148,5 +146,33 @@ void ValidateUniqueKeys() var keys = duplicates.SelectMany(d => d.Select(x => x.Key)); throw new InvalidOperationException("Centroid.Config instance contains duplicate keys: " + string.Join(", ", keys)); } + + static void MergeInto(JContainer left, JToken right) + { + foreach (var rightChild in right.Children()) + { + var rightChildProperty = rightChild; + var leftProperty = left.SelectToken(rightChildProperty.Name); + + if (leftProperty == null) + { + left.Add(rightChild); + } + else + { + var leftObject = leftProperty as JObject; + + if (leftObject == null) + { + var leftParent = (JProperty) leftProperty.Parent; + leftParent.Value = rightChildProperty.Value; + } + else + { + MergeInto(leftObject, rightChildProperty.Value); + } + } + } + } } } \ No newline at end of file diff --git a/python/centroid.py b/python/centroid.py index d00ea65..a7b10f5 100644 --- a/python/centroid.py +++ b/python/centroid.py @@ -47,9 +47,8 @@ def for_environment(self, env): return Config(env_json) all_json = _get_value(actual_key, self.raw_config) - all_json.update(env_json); - return Config(all_json) + return Config(_dict_merge(all_json, env_json)) @staticmethod def from_file(filename): @@ -75,3 +74,13 @@ def _get_actual_key(key, hashtable): if len(result) > 0: return result[0] return None + +def _dict_merge(left, right): + if not isinstance(right, dict): + return right + for k, v in right.iteritems(): + if k in left and isinstance(left[k], dict): + left[k] = _dict_merge(left[k], v) + else: + left[k] = v + return left diff --git a/python/tests.py b/python/tests.py index c78e37d..3ce21b3 100644 --- a/python/tests.py +++ b/python/tests.py @@ -97,3 +97,9 @@ def test_all_environment_is_not_case_sensitive(self): config = Config('{"Prod": {"Shared": "production!"}, "all": {"Shared": "none", "AllOnly": "works"}}') config = config.for_environment("Prod") self.assertEqual(config.all_only, "works") + + def test_supports_deep_merge(self): + config = Config('{"Prod": {"Database": {"Server": "prod-sql"}}, "All": {"Database": {"MigrationsPath": "path/to/migrations"}}}') + config = config.for_environment("Prod") + self.assertEqual(config.database.server, "prod-sql") + self.assertEqual(config.database.migrations_path, "path/to/migrations") diff --git a/ruby/lib/centroid.rb b/ruby/lib/centroid.rb index 69432b7..59cadca 100644 --- a/ruby/lib/centroid.rb +++ b/ruby/lib/centroid.rb @@ -40,7 +40,7 @@ def for_environment(env) Config.new(env_json) else all_json = raw_config[all_key] - Config.new(all_json.merge(env_json)) + Config.new(deep_merge(all_json, env_json)) end end @@ -72,5 +72,20 @@ def validate_unique_keys! keys = dups.values.flat_map { |d| d.map { |e| e[:key] } } raise KeyError, "Centroid::Config instance contains duplicate keys: #{keys.join(', ')}" end + + def deep_merge(left, right) + return right if not right.is_a?(Hash) + + right.each_pair do |k, rv| + lv = left[k] + left[k] = if lv.is_a?(Hash) && rv.is_a?(Hash) + deep_merge(lv, rv) + else + rv + end + end + + left + end end end diff --git a/ruby/test/centroid_test.rb b/ruby/test/centroid_test.rb index 95f31ef..3c946c5 100644 --- a/ruby/test/centroid_test.rb +++ b/ruby/test/centroid_test.rb @@ -94,4 +94,11 @@ def test_all_environment_is_not_case_sensitive config = config.for_environment("Prod") assert_equal(config.all_only, "works") end + + def test_supports_deep_merge + config = Centroid::Config.new('{"Prod": {"Database": {"Server": "prod-sql"}}, "All": {"Database": {"MigrationsPath": "path/to/migrations"}}}') + config = config.for_environment("Prod") + assert_equal(config.database.server, "prod-sql") + assert_equal(config.database.migrations_path, "path/to/migrations") + end end