diff --git a/lib/vagrant/config/v1.rb b/lib/vagrant/config/v1.rb index 1cd00f338..526a7517d 100644 --- a/lib/vagrant/config/v1.rb +++ b/lib/vagrant/config/v1.rb @@ -10,6 +10,60 @@ module Vagrant # @param [Proc] config_proc # @return [Object] def self.load(config_proc) + # Create a root configuration object + root = new_root_object + + # Call the proc with the root + config_proc.call(root) + + # Return the root object, which doubles as the configuration object + # we actually use for accessing as well. + root + end + + # Merges two configuration objects. + # + # @param [V1::Root] old The older root config. + # @param [V1::Root] new The newer root config. + # @return [V1::Root] + def self.merge(old, new) + # Grab the internal states, we use these heavily throughout the process + old_state = old.__internal_state + new_state = new.__internal_state + + # The config map for the new object is the old one merged with the + # new one. + config_map = old_state["config_map"].merge(new_state["config_map"]) + + # Merge the keys. + old_keys = old_state["keys"] + new_keys = new_state["keys"] + keys = {} + old_keys.each do |key, old| + if new_keys.has_key?(key) + # We need to do a merge, which we expect to be available + # on the config class itself. + keys[key] = old.merge(new_keys[key]) + else + # We just take the old value, but dup it so that we can modify. + keys[key] = old.dup + end + end + + new_keys.each do |key, new| + # Add in the keys that the new class has that we haven't merged. + if !keys.has_key?(key) + keys[key] = new.dup + end + end + + # Return the final root object + V1::Root.new(config_map, keys) + end + + protected + + def self.new_root_object # Get all the registered plugins config_map = {} Vagrant.plugin("1").registered.each do |plugin| @@ -19,14 +73,7 @@ module Vagrant end # Create the configuration root object - root = V1::Root.new(config_map) - - # Call the proc with the root - config_proc.call(root) - - # Return the root object, which doubles as the configuration object - # we actually use for accessing as well. - root + V1::Root.new(config_map) end end end diff --git a/lib/vagrant/config/v1/root.rb b/lib/vagrant/config/v1/root.rb index 58dcbe324..7e2b9be15 100644 --- a/lib/vagrant/config/v1/root.rb +++ b/lib/vagrant/config/v1/root.rb @@ -8,8 +8,8 @@ module Vagrant # configuration classes. # # @param [Hash] config_map Map of key to config class. - def initialize(config_map) - @keys = {} + def initialize(config_map, keys=nil) + @keys = keys || {} @config_map = config_map end @@ -29,6 +29,17 @@ module Vagrant super end end + + # Returns the internal state of the root object. This is used + # by outside classes when merging, and shouldn't be called directly. + # Note the strange method name is to attempt to avoid any name + # clashes with potential configuration keys. + def __internal_state + { + "config_map" => @config_map, + "keys" => @keys + } + end end end end diff --git a/test/unit/vagrant/config/v1/root_test.rb b/test/unit/vagrant/config/v1/root_test.rb index 752df2502..55cd2880b 100644 --- a/test/unit/vagrant/config/v1/root_test.rb +++ b/test/unit/vagrant/config/v1/root_test.rb @@ -17,4 +17,18 @@ describe Vagrant::Config::V1::Root do instance = described_class.new({}) expect { instance.foo }.to raise_error(NoMethodError) end + + it "can be created with initial state" do + instance = described_class.new({}, { :foo => "bar" }) + instance.foo.should == "bar" + end + + it "should return internal state" do + map = { "foo" => Object, "bar" => Object } + instance = described_class.new(map) + instance.__internal_state.should == { + "config_map" => map, + "keys" => {} + } + end end diff --git a/test/unit/vagrant/config/v1_test.rb b/test/unit/vagrant/config/v1_test.rb index 179ae63bc..d411bb2b5 100644 --- a/test/unit/vagrant/config/v1_test.rb +++ b/test/unit/vagrant/config/v1_test.rb @@ -31,4 +31,51 @@ describe Vagrant::Config::V1 do end end end + + describe "merging" do + it "should merge available configuration keys" do + old = Vagrant::Config::V1::Root.new({ :foo => Object }) + new = Vagrant::Config::V1::Root.new({ :bar => Object }) + result = described_class.merge(old, new) + result.foo.should be_kind_of(Object) + result.bar.should be_kind_of(Object) + end + + it "should merge instantiated objects" do + config_class = Class.new do + attr_accessor :value + end + + old = Vagrant::Config::V1::Root.new({ :foo => config_class }) + old.foo.value = "old" + + new = Vagrant::Config::V1::Root.new({ :bar => config_class }) + new.bar.value = "new" + + result = described_class.merge(old, new) + result.foo.value.should == "old" + result.bar.value.should == "new" + end + + it "should merge conflicting classes by calling `merge`" do + config_class = Class.new do + attr_accessor :value + + def merge(new) + result = self.class.new + result.value = @value + new.value + result + end + end + + old = Vagrant::Config::V1::Root.new({ :foo => config_class }) + old.foo.value = 10 + + new = Vagrant::Config::V1::Root.new({ :foo => config_class }) + new.foo.value = 15 + + result = described_class.merge(old, new) + result.foo.value.should == 25 + end + end end