diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 96cdd32e0..a2878620f 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -56,6 +56,7 @@ module Vagrant # class Action autoload :Environment, 'vagrant/action/environment' + autoload :MultiStep, 'vagrant/action/multistep' autoload :Step, 'vagrant/action/step' autoload :Warden, 'vagrant/action/warden' diff --git a/lib/vagrant/action/multistep.rb b/lib/vagrant/action/multistep.rb new file mode 100644 index 000000000..e2a13d6c3 --- /dev/null +++ b/lib/vagrant/action/multistep.rb @@ -0,0 +1,51 @@ +module Vagrant + class Action + class MultiStep < Step + def initialize + @steps = [] + end + + def step(step_class, *extra_inputs) + # Get the options hash and set the defaults + options = {} + options = extra_inputs.pop if extra_inputs.last.kind_of?(Hash) + + # Append the step + @steps << [step_class, extra_inputs, options] + end + + def call(params=nil) + params ||= {} + + # Instantiate all the steps + instances = @steps.map { |s, inputs, options| [s.new, inputs, options] } + + # For each step, call it with proper inputs, using the output + # of that call as inputs to the next. + instances.inject(params) do |inputs, data| + step, extra_inputs, options = data + + # If there are extra inputs for this step, add them to the + # parameters based on the initial parameters. + extra_inputs.each do |extra_input| + inputs[extra_input] = params[extra_input] + end + + # If we have inputs to remap, remap them. + if options[:map] + options[:map].each do |from, to| + # This sets the input to the new key while removing the + # the old key from the same hash. Kind of sneaky, but + # hopefully this comment makes it clear. + inputs[to] = inputs.delete(from) + end + end + + # Call the actual step, using the results for the next + # iteration. + step.call(inputs) + end + end + end + end +end diff --git a/lib/vagrant/action/step.rb b/lib/vagrant/action/step.rb index 14621a5e9..250bf60ad 100644 --- a/lib/vagrant/action/step.rb +++ b/lib/vagrant/action/step.rb @@ -33,9 +33,15 @@ module Vagrant # Validates that the output matches the specification provided, and # raises a RuntimeError if it does not. - def self.validate_output(value) + def self.process_output(value) + # The return value must be a Hash, so we just coerce it to that. + value = {} if !value.kind_of?(Hash) + + # Verify that we have all the outputs missing = outputs - value.keys raise RuntimeError, "Missing output keys: #{missing}" if !missing.empty? + + return value end # This calls the step with the given parameters, and returns a hash @@ -49,7 +55,7 @@ module Vagrant # @option options [Boolean] :validate_output Whether to validate the # output or not. # @return [Hash] Output - def call(params, options=nil) + def call(params={}, options=nil) options = { :method => :execute, :validate_output => true @@ -61,8 +67,9 @@ module Vagrant # Call the actual implementation results = send(options[:method]) - # Validate the outputs - self.class.validate_output(results) if options[:validate_output] + # Validate the outputs if it is enabled and the list of configured + # outputs is not empty. + results = self.class.process_output(results) if options[:validate_output] # Return the final results results diff --git a/test/unit/vagrant/action/multistep_test.rb b/test/unit/vagrant/action/multistep_test.rb new file mode 100644 index 000000000..89976d343 --- /dev/null +++ b/test/unit/vagrant/action/multistep_test.rb @@ -0,0 +1,82 @@ +require File.expand_path("../../../base", __FILE__) + +describe Vagrant::Action::MultiStep do + it "should compose a series of steps" do + step_A = Class.new(Vagrant::Action::Step) do + input :obj + output :obj + + def execute + @obj << "A" + return :obj => @obj + end + end + + step_B = Class.new(Vagrant::Action::Step) do + input :obj + output :result + + def execute + return :result => (@obj << "B") + end + end + + obj = [] + + ms = described_class.new + ms.step step_A + ms.step step_B + ms.call(:obj => obj).should == { :result => ["A", "B"] } + end + + it "should allow for custom inputs to pass to specific steps" do + step_A = Class.new(Vagrant::Action::Step) do + def execute + # Do nothing. + end + end + + step_B = Class.new(Vagrant::Action::Step) do + input :obj + + def execute + @obj << "B" + end + end + + obj = [] + + ms = described_class.new + ms.step step_A + ms.step step_B, :obj + ms.call(:obj => obj) + + obj.should == ["B"] + end + + it "should be able to remap input names" do + step_A = Class.new(Vagrant::Action::Step) do + output :foo + + def execute + return :foo => "A" + end + end + + step_B = Class.new(Vagrant::Action::Step) do + input :from + output :value + + def execute + return :value => @from + end + end + + obj = [] + + ms = described_class.new + ms.step step_A + ms.step step_B, :map => { :foo => :from } + ms.call.should == { :value => "A" } + end +end diff --git a/test/unit/vagrant/action/step_test.rb b/test/unit/vagrant/action/step_test.rb index 0af28845f..89c63c111 100644 --- a/test/unit/vagrant/action/step_test.rb +++ b/test/unit/vagrant/action/step_test.rb @@ -18,7 +18,7 @@ describe Vagrant::Action::Step do input :foo end - expect { step_class.new.call({}) }.to raise_error(ArgumentError) + expect { step_class.new.call }.to raise_error(ArgumentError) end it "calls a custom method if given" do @@ -31,27 +31,39 @@ describe Vagrant::Action::Step do step_class.new.call({}, :method => :prepare).should == { :foo => 12 } end - it "raises an exception if missing outputs" do - step_class = Class.new(described_class) do - output :foo - - def execute - return :bar => 12 + describe "outputs" do + it "return an empty hash if no outputs are specified" do + step_class = Class.new(described_class) do + def execute + return 12 + end end + + step_class.new.call.should == {} end - expect { step_class.new.call({}) }.to raise_error(RuntimeError) - end + it "raises an exception if missing outputs" do + step_class = Class.new(described_class) do + output :foo - it "does nothing if missing outputs but we disabled validating" do - step_class = Class.new(described_class) do - output :foo - - def execute - return :bar => 12 + def execute + return :bar => 12 + end end + + expect { step_class.new.call }.to raise_error(RuntimeError) end - step_class.new.call({}, :validate_output => false).should == { :bar => 12 } + it "does nothing if missing outputs but we disabled validating" do + step_class = Class.new(described_class) do + output :foo + + def execute + return :bar => 12 + end + end + + step_class.new.call({}, :validate_output => false).should == { :bar => 12 } + end end end