Multistep can now take parameters from any arbitrary step prior.

This commit is contained in:
Mitchell Hashimoto 2011-12-06 09:46:57 -08:00
parent 5b87165e97
commit 7d3746b292
2 changed files with 151 additions and 20 deletions

View File

@ -1,49 +1,123 @@
module Vagrant
class Action
class MultiStep < Step
# This class is used as a placeholder to represent a parameter that
# needs to be replaced at runtime. For example: A step might take a
# parameter B that is outputted as A from a previous step. An instance
# of this class can represent that the previous A should be remapped
# to this new B.
class Param
attr_reader :name
attr_reader :variable
def initialize(name, variable)
@name = name
@variable = variable
end
end
# Represents a remapping that comes from the group inputs.
class GroupInput < Param; end
# Represents a remapping that comes from the output of a previous step.
class StepOutput < Param; end
def initialize
@steps = []
@step_names = []
@steps = {}
end
# This returns a custom object that represents an input parameter
# given to this group.
#
# @param [Symbol] key Parameter name of the input to use from the group.
# @return [GroupInput] A `param` type that can be used for remappings.
def input(key)
return GroupInput.new(:global, key)
end
# This returns a custom object that represents an output parameter
# from another step in this group.
#
# @param [Object] name Name of the step. This is either an explicit name
# like a symbol or the class for the step if it is unique.
# @param [Symbol] output The output variable name from the step.
# @return [StepOutput] A `param` type that can be used for remappings.
def output(name, output)
return StepOutput.new(name, output)
end
def step(step_class, *extra_inputs)
# Determine the name for this step.
step_name = nil
if step_class.is_a?(Symbol)
step_name = step_class
step_class = extra_inputs.shift
else
step_name = step_class
end
if @steps.has_key?(step_name)
raise NameError, "Step with name #{step_name} already exists."
elsif !step_class.is_a?(Class)
raise ArgumentError, "Step class must be a class."
end
# Get the options hash and set the defaults
options = {}
options = extra_inputs.pop if extra_inputs.last.kind_of?(Hash)
maps = {}
maps = extra_inputs.pop if extra_inputs.last.kind_of?(Hash)
extra_inputs.each do |direct|
if direct.is_a?(Symbol)
# Symbols are assumed to be inputs to this group
direct = input(direct)
end
maps[direct] = direct.variable
end
# Append the step
@steps << [step_class, extra_inputs, options]
@step_names << step_name
@steps[step_name] = [step_class, maps]
end
def call(params=nil)
params ||= {}
# Instantiate all the steps
instances = @steps.map { |s, inputs, options| [s.new, inputs, options] }
steps = @step_names.map do |name|
step_class, maps = @steps[name]
[name, step_class.new, maps]
end
# 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
step_outputs = {}
steps.inject(params) do |inputs, data|
name, step, mappings = data
# 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.
mappings.each do |from, to|
if from.kind_of?(GroupInput)
# Group inputs get their data from the initial parameters given
# to this group.
inputs[to] = params[from.variable]
elsif from.kind_of?(StepOutput)
# Step outputs get their data from a previous step's output.
inputs[to] = step_outputs[from.name][from.variable]
else
# A basic remapping remaps the previous steps outputs to an
# input for this step.
inputs[to] = inputs.delete(from)
end
end
# Call the actual step, using the results for the next
# iteration.
step.call(inputs)
step_outputs[name] = step.call(inputs)
# Return a shallow dup of the results
step_outputs[name].dup
end
end
end

View File

@ -76,7 +76,64 @@ describe Vagrant::Action::MultiStep do
ms = described_class.new
ms.step step_A
ms.step step_B, :map => { :foo => :from }
ms.step step_B, :foo => :from
ms.call.should == { :value => "A" }
end
it "should be able to reference variables from steps before other steps" do
step_A = Class.new(Vagrant::Action::Step) do
output :foo
def enter
return :foo => 10
end
end
step_B = Class.new(Vagrant::Action::Step) do
input :from
output :value
def enter
return :value => @from
end
end
step_C = Class.new(Vagrant::Action::Step) do
input :number
output :value
def enter
return :value => @number * 2
end
end
obj = []
g = described_class.new
g.step step_A
g.step step_B, :foo => :from
g.step step_C, g.output(step_A, :foo) => :number
g.call.should == { :value => 20 }
end
it "should error if multiple steps of the same class are given without explicit names" do
step_A = Class.new(Vagrant::Action::Step)
g = described_class.new
g.step step_A
expect { g.step step_A }.to raise_error(NameError)
end
it "should not error if multiple steps of the same class are given with custom names" do
step_A = Class.new(Vagrant::Action::Step)
g = described_class.new
g.step step_A
expect { g.step :another, step_A }.to_not raise_error
end
it "should error if a step is not a class" do
g = described_class.new
expect { g.step :foo }.to raise_error(ArgumentError)
end
end