From 49d299956f099244068b4b3a58f7c0f4d078046a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 4 Dec 2011 17:14:21 -0800 Subject: [PATCH] Add the "Step" abstraction --- lib/vagrant/action.rb | 1 + lib/vagrant/action/step.rb | 89 +++++++++++++++++++++++++++ test/unit/vagrant/action/step_test.rb | 57 +++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 lib/vagrant/action/step.rb create mode 100644 test/unit/vagrant/action/step_test.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index c81c2adfd..96cdd32e0 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 :Step, 'vagrant/action/step' autoload :Warden, 'vagrant/action/warden' include Util diff --git a/lib/vagrant/action/step.rb b/lib/vagrant/action/step.rb new file mode 100644 index 000000000..14621a5e9 --- /dev/null +++ b/lib/vagrant/action/step.rb @@ -0,0 +1,89 @@ +module Vagrant + class Action + # A step is a callable action that Vagrant uses to build up + # more complex actions sequences. + # + # A step is really just a reimplementation of a "function" + # within the runtime of Ruby. A step advertises parameters + # that it requires (inputs), as well as return values (outputs). + # The `Step` class handles validating that all the required + # parameters are set on the step as well as can validate the + # return values. + class Step + # The keys given to this will be required by the step. Each key + # should be a symbol. + def self.input(*keys) + inputs.concat(keys) + end + + # The values that this step provides. Each key should be a symbol. + def self.output(*keys) + outputs.concat(keys) + end + + # This is the array of required keys. + def self.inputs + @inputs ||= [] + end + + # The array of keys that are provided by this Step. + def self.outputs + @outputs ||= [] + end + + # Validates that the output matches the specification provided, and + # raises a RuntimeError if it does not. + def self.validate_output(value) + missing = outputs - value.keys + raise RuntimeError, "Missing output keys: #{missing}" if !missing.empty? + end + + # This calls the step with the given parameters, and returns a hash + # of the outputs. + # + # Additional options may be provided via the options hash at the end. + # + # @param [Hash] params Parameters for the step. + # @param [Hash] options Options hash + # @option options [Symbol] :method Method to execute. + # @option options [Boolean] :validate_output Whether to validate the + # output or not. + # @return [Hash] Output + def call(params, options=nil) + options = { + :method => :execute, + :validate_output => true + }.merge(options || {}) + + # Set and validate the inputs + set_inputs(params) + + # Call the actual implementation + results = send(options[:method]) + + # Validate the outputs + self.class.validate_output(results) if options[:validate_output] + + # Return the final results + results + end + + protected + + # Sets the parameters for the step. + # + # This will raise an exception if all the `requires` are not + # properly met. Otherwise, the parameters are set as instance variables + # on this instance. + def set_inputs(params) + inputs = self.class.inputs + remaining = inputs - params.keys + raise ArgumentError, "Missing parameters: #{remaining}" if !remaining.empty? + + inputs.each do |key| + instance_variable_set("@#{key}", params[key]) + end + end + end + end +end diff --git a/test/unit/vagrant/action/step_test.rb b/test/unit/vagrant/action/step_test.rb new file mode 100644 index 000000000..0af28845f --- /dev/null +++ b/test/unit/vagrant/action/step_test.rb @@ -0,0 +1,57 @@ +require File.expand_path("../../../base", __FILE__) + +describe Vagrant::Action::Step do + it "provides the parameters as instance variables" do + step_class = Class.new(described_class) do + input :foo + + def execute + return :value => @foo + end + end + + step_class.new.call(:foo => 12).should == { :value => 12 } + end + + it "raises an exception if not all required parameters are given" do + step_class = Class.new(described_class) do + input :foo + end + + expect { step_class.new.call({}) }.to raise_error(ArgumentError) + end + + it "calls a custom method if given" do + step_class = Class.new(described_class) do + def prepare + return :foo => 12 + end + end + + 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 + end + end + + expect { step_class.new.call({}) }.to raise_error(RuntimeError) + end + + 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