diff options
| -rw-r--r-- | lib/puppet/string.rb | 8 | ||||
| -rw-r--r-- | lib/puppet/string/action.rb | 32 | ||||
| -rw-r--r-- | lib/puppet/string/action_builder.rb | 11 | ||||
| -rw-r--r-- | lib/puppet/string/action_manager.rb | 10 | ||||
| -rw-r--r-- | lib/puppet/string/option.rb | 24 | ||||
| -rw-r--r-- | lib/puppet/string/option_builder.rb | 25 | ||||
| -rw-r--r-- | lib/puppet/string/option_manager.rb | 46 | ||||
| -rwxr-xr-x | spec/unit/string/action_builder_spec.rb | 33 | ||||
| -rwxr-xr-x | spec/unit/string/action_spec.rb | 82 | ||||
| -rw-r--r-- | spec/unit/string/option_builder_spec.rb | 57 | ||||
| -rw-r--r-- | spec/unit/string/option_spec.rb | 61 | ||||
| -rwxr-xr-x | spec/unit/string_spec.rb | 89 |
12 files changed, 458 insertions, 20 deletions
diff --git a/lib/puppet/string.rb b/lib/puppet/string.rb index 04db1f33b..517cf4506 100644 --- a/lib/puppet/string.rb +++ b/lib/puppet/string.rb @@ -2,12 +2,16 @@ require 'puppet' require 'puppet/util/autoload' class Puppet::String - require 'puppet/string/action_manager' require 'puppet/string/string_collection' + require 'puppet/string/action_manager' include Puppet::String::ActionManager extend Puppet::String::ActionManager + require 'puppet/string/option_manager' + include Puppet::String::OptionManager + extend Puppet::String::OptionManager + include Puppet::Util class << self @@ -58,7 +62,7 @@ class Puppet::String def initialize(name, version, &block) unless Puppet::String::StringCollection.validate_version(version) - raise ArgumentError, "Cannot create string with invalid version number '#{version}'!" + raise ArgumentError, "Cannot create string #{name.inspect} with invalid version number '#{version}'!" end @name = Puppet::String::StringCollection.underscorize(name) diff --git a/lib/puppet/string/action.rb b/lib/puppet/string/action.rb index 5a7f3f203..4219aca0a 100644 --- a/lib/puppet/string/action.rb +++ b/lib/puppet/string/action.rb @@ -1,4 +1,5 @@ require 'puppet/string' +require 'puppet/string/option' class Puppet::String::Action attr_reader :name @@ -8,11 +9,10 @@ class Puppet::String::Action end def initialize(string, name, attrs = {}) - name = name.to_s - raise "'#{name}' is an invalid action name" unless name =~ /^[a-z]\w*$/ - - @string = string - @name = name + raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ + @string = string + @name = name.to_sym + @options = {} attrs.each do |k,v| send("#{k}=", v) end end @@ -27,4 +27,26 @@ class Puppet::String::Action @string.meta_def(@name, &block) end end + + def add_option(option) + if option? option.name then + raise ArgumentError, "#{option.name} duplicates an existing option on #{self}" + elsif @string.option? option.name then + raise ArgumentError, "#{option.name} duplicates an existing option on #{@string}" + end + + @options[option.name] = option + end + + def option?(name) + @options.include? name.to_sym + end + + def options + (@options.keys + @string.options).sort + end + + def get_option(name) + @options[name.to_sym] || @string.get_option(name) + end end diff --git a/lib/puppet/string/action_builder.rb b/lib/puppet/string/action_builder.rb index b3db51104..fb2a749ae 100644 --- a/lib/puppet/string/action_builder.rb +++ b/lib/puppet/string/action_builder.rb @@ -5,10 +5,8 @@ class Puppet::String::ActionBuilder attr_reader :action def self.build(string, name, &block) - name = name.to_s - raise "Action '#{name}' must specify a block" unless block - builder = new(string, name, &block) - builder.action + raise "Action #{name.inspect} must specify a block" unless block + new(string, name, &block).action end def initialize(string, name, &block) @@ -24,4 +22,9 @@ class Puppet::String::ActionBuilder raise "Invoke called on an ActionBuilder with no corresponding Action" unless @action @action.invoke = block end + + def option(name, attrs = {}, &block) + option = Puppet::String::OptionBuilder.build(@action, name, attrs, &block) + @action.add_option(option) + end end diff --git a/lib/puppet/string/action_manager.rb b/lib/puppet/string/action_manager.rb index c29dbf454..c980142ce 100644 --- a/lib/puppet/string/action_manager.rb +++ b/lib/puppet/string/action_manager.rb @@ -5,20 +5,15 @@ module Puppet::String::ActionManager # the code to do so. def action(name, &block) @actions ||= {} - name = name.to_s.downcase.to_sym - raise "Action #{name} already defined for #{self}" if action?(name) - action = Puppet::String::ActionBuilder.build(self, name, &block) - - @actions[name] = action + @actions[action.name] = action end # This is the short-form of an action definition; it doesn't use the # builder, just creates the action directly from the block. def script(name, &block) @actions ||= {} - name = name.to_s.downcase.to_sym raise "Action #{name} already defined for #{self}" if action?(name) @actions[name] = Puppet::String::Action.new(self, name, :invoke => block) end @@ -36,7 +31,8 @@ module Puppet::String::ActionManager end def get_action(name) - @actions[name].dup + @actions ||= {} + @actions[name.to_sym] end def action?(name) diff --git a/lib/puppet/string/option.rb b/lib/puppet/string/option.rb new file mode 100644 index 000000000..bdc3e07c5 --- /dev/null +++ b/lib/puppet/string/option.rb @@ -0,0 +1,24 @@ +class Puppet::String::Option + attr_reader :name, :string + + def initialize(string, name, attrs = {}) + raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/ + @string = string + @name = name.to_sym + attrs.each do |k,v| send("#{k}=", v) end + end + + def to_s + @name.to_s.tr('_', '-') + end + + Types = [:boolean, :string] + def type + @type ||= :boolean + end + def type=(input) + value = begin input.to_sym rescue nil end + Types.include?(value) or raise ArgumentError, "#{input.inspect} is not a valid type" + @type = value + end +end diff --git a/lib/puppet/string/option_builder.rb b/lib/puppet/string/option_builder.rb new file mode 100644 index 000000000..2087cbc99 --- /dev/null +++ b/lib/puppet/string/option_builder.rb @@ -0,0 +1,25 @@ +require 'puppet/string/option' + +class Puppet::String::OptionBuilder + attr_reader :option + + def self.build(string, name, attrs = {}, &block) + new(string, name, attrs, &block).option + end + + private + def initialize(string, name, attrs, &block) + @string = string + @option = Puppet::String::Option.new(string, name, attrs) + block and instance_eval(&block) + @option + end + + # Metaprogram the simple DSL from the option class. + Puppet::String::Option.instance_methods.grep(/=$/).each do |setter| + next if setter =~ /^=/ # special case, darn it... + + dsl = setter.sub(/=$/, '') + define_method(dsl) do |value| @option.send(setter, value) end + end +end diff --git a/lib/puppet/string/option_manager.rb b/lib/puppet/string/option_manager.rb new file mode 100644 index 000000000..df3ae6b4b --- /dev/null +++ b/lib/puppet/string/option_manager.rb @@ -0,0 +1,46 @@ +require 'puppet/string/option_builder' + +module Puppet::String::OptionManager + # Declare that this app can take a specific option, and provide + # the code to do so. + def option(name, attrs = {}, &block) + @options ||= {} + raise ArgumentError, "Option #{name} already defined for #{self}" if option?(name) + actions.each do |action| + if get_action(action).option?(name) then + raise ArgumentError, "Option #{name} already defined on action #{action} for #{self}" + end + end + option = Puppet::String::OptionBuilder.build(self, name, &block) + @options[option.name] = option + end + + def options + @options ||= {} + result = @options.keys + + if self.is_a?(Class) and superclass.respond_to?(:options) + result += superclass.options + elsif self.class.respond_to?(:options) + result += self.class.options + end + result.sort + end + + def get_option(name) + @options ||= {} + result = @options[name.to_sym] + unless result then + if self.is_a?(Class) and superclass.respond_to?(:get_option) + result = superclass.get_option(name) + elsif self.class.respond_to?(:get_option) + result = self.class.get_option(name) + end + end + return result + end + + def option?(name) + options.include? name.to_sym + end +end diff --git a/spec/unit/string/action_builder_spec.rb b/spec/unit/string/action_builder_spec.rb index c3395cf6a..946244cbf 100755 --- a/spec/unit/string/action_builder_spec.rb +++ b/spec/unit/string/action_builder_spec.rb @@ -9,7 +9,7 @@ describe Puppet::String::ActionBuilder do action = Puppet::String::ActionBuilder.build(nil,:foo) do end action.should be_a(Puppet::String::Action) - action.name.should == "foo" + action.name.should == :foo end it "should define a method on the string which invokes the action" do @@ -24,7 +24,36 @@ describe Puppet::String::ActionBuilder do end it "should require a block" do - lambda { Puppet::String::ActionBuilder.build(nil,:foo) }.should raise_error("Action 'foo' must specify a block") + lambda { Puppet::String::ActionBuilder.build(nil,:foo) }. + should raise_error("Action :foo must specify a block") + end + + describe "when handling options" do + let :string do Puppet::String.new(:option_handling, '0.0.1') end + + it "should have a #option DSL function" do + method = nil + Puppet::String::ActionBuilder.build(string, :foo) do + method = self.method(:option) + end + method.should be + end + + it "should define an option without a block" do + action = Puppet::String::ActionBuilder.build(string, :foo) do + option :bar + end + action.should be_option :bar + end + + it "should accept an empty block" do + action = Puppet::String::ActionBuilder.build(string, :foo) do + option :bar do + # This space left deliberately blank. + end + end + action.should be_option :bar + end end end end diff --git a/spec/unit/string/action_spec.rb b/spec/unit/string/action_spec.rb index f4ca8316d..d182f0abe 100755 --- a/spec/unit/string/action_spec.rb +++ b/spec/unit/string/action_spec.rb @@ -63,4 +63,86 @@ describe Puppet::String::Action do string.qux.should == "the value of foo in baz is '25'" end end + + describe "with action-level options" do + it "should support options without arguments" do + string = Puppet::String.new(:action_level_options, '0.0.1') do + action(:foo) do + option :bar + end + end + + string.should_not be_option :bar + string.get_action(:foo).should be_option :bar + end + + it "should support options with an empty block" do + string = Puppet::String.new(:action_level_options, '0.0.1') do + action :foo do + option :bar do + # this line left deliberately blank + end + end + end + + string.should_not be_option :bar + string.get_action(:foo).should be_option :bar + end + + it "should return only action level options when there are no string options" do + string = Puppet::String.new(:action_level_options, '0.0.1') do + action :foo do option :bar end + end + + string.get_action(:foo).options.should =~ [:bar] + end + + describe "with both string and action options" do + let :string do + Puppet::String.new(:action_level_options, '0.0.1') do + action :foo do option :bar end + action :baz do option :bim end + option :quux + end + end + + it "should return combined string and action options" do + string.get_action(:foo).options.should =~ [:bar, :quux] + end + + it "should get an action option when asked" do + string.get_action(:foo).get_option(:bar). + should be_an_instance_of Puppet::String::Option + end + + it "should get a string option when asked" do + string.get_action(:foo).get_option(:quux). + should be_an_instance_of Puppet::String::Option + end + + it "should return options only for this action" do + string.get_action(:baz).options.should =~ [:bim, :quux] + end + end + + it "should fail when a duplicate option is added" do + expect { + Puppet::String.new(:action_level_options, '0.0.1') do + action :foo do + option :foo + option :foo + end + end + }.should raise_error ArgumentError, /foo duplicates an existing option/ + end + + it "should fail when a string option duplicates an action option" do + expect { + Puppet::String.new(:action_level_options, '0.0.1') do + option :foo + action :bar do option :foo end + end + }.should raise_error ArgumentError, /duplicates an existing option .*action_level/i + end + end end diff --git a/spec/unit/string/option_builder_spec.rb b/spec/unit/string/option_builder_spec.rb new file mode 100644 index 000000000..685787808 --- /dev/null +++ b/spec/unit/string/option_builder_spec.rb @@ -0,0 +1,57 @@ +require 'puppet/string/option_builder' + +describe Puppet::String::OptionBuilder do + let :string do Puppet::String.new(:option_builder_testing, '0.0.1') end + + it "should be able to construct an option without a block" do + Puppet::String::OptionBuilder.build(string, :foo). + should be_an_instance_of Puppet::String::Option + end + + it "should set attributes during construction" do + # Walk all types, since at least one of them should be non-default... + Puppet::String::Option::Types.each do |type| + option = Puppet::String::OptionBuilder.build(string, :foo, :type => type) + option.should be_an_instance_of Puppet::String::Option + option.type.should == type + end + end + + describe "when using the DSL block" do + it "should work with an empty block" do + option = Puppet::String::OptionBuilder.build(string, :foo) do + # This block deliberately left blank. + end + + option.should be_an_instance_of Puppet::String::Option + end + + describe "#type" do + Puppet::String::Option::Types.each do |valid| + it "should accept #{valid.inspect}" do + option = Puppet::String::OptionBuilder.build(string, :foo) do + type valid + end + option.should be_an_instance_of Puppet::String::Option + end + + it "should accept #{valid.inspect} as a string" do + option = Puppet::String::OptionBuilder.build(string, :foo) do + type valid.to_s + end + option.should be_an_instance_of Puppet::String::Option + end + + [:foo, nil, true, false, 12, '12', 'whatever', ::String, URI].each do |input| + it "should reject #{input.inspect}" do + expect { + Puppet::String::OptionBuilder.build(string, :foo) do + type input + end + }.should raise_error ArgumentError, /not a valid type/ + end + end + end + end + end +end diff --git a/spec/unit/string/option_spec.rb b/spec/unit/string/option_spec.rb new file mode 100644 index 000000000..9bb4309cd --- /dev/null +++ b/spec/unit/string/option_spec.rb @@ -0,0 +1,61 @@ +require 'puppet/string/option' + +describe Puppet::String::Option do + let :string do Puppet::String.new(:option_testing, '0.0.1') end + + it "requires a string when created" do + expect { Puppet::String::Option.new }. + should raise_error ArgumentError, /wrong number of arguments/ + end + + it "also requires a name when created" do + expect { Puppet::String::Option.new(string) }. + should raise_error ArgumentError, /wrong number of arguments/ + end + + it "should create an instance when given a string and name" do + Puppet::String::Option.new(string, :foo). + should be_instance_of Puppet::String::Option + end + + describe "#to_s" do + it "should transform a symbol into a string" do + Puppet::String::Option.new(string, :foo).to_s.should == "foo" + end + + it "should use - rather than _ to separate words" do + Puppet::String::Option.new(string, :foo_bar).to_s.should == "foo-bar" + end + end + + describe "#type" do + Puppet::String::Option::Types.each do |type| + it "should accept #{type.inspect}" do + Puppet::String::Option.new(string, :foo, :type => type). + should be_an_instance_of Puppet::String::Option + end + + it "should accept #{type.inspect} when given as a string" do + Puppet::String::Option.new(string, :foo, :type => type.to_s). + should be_an_instance_of Puppet::String::Option + end + end + + [:foo, nil, true, false, 12, '12', 'whatever', ::String, URI].each do |input| + it "should reject #{input.inspect}" do + expect { Puppet::String::Option.new(string, :foo, :type => input) }. + should raise_error ArgumentError, /not a valid type/ + end + end + end + + + # name short value type + # ca-location CA_LOCATION string + # debug d ---- boolean + # verbose v ---- boolean + # terminus TERMINUS string + # format FORMAT symbol + # mode r RUNMODE limited set of symbols + # server URL URL +end diff --git a/spec/unit/string_spec.rb b/spec/unit/string_spec.rb index 64d4f12f8..577505186 100755 --- a/spec/unit/string_spec.rb +++ b/spec/unit/string_spec.rb @@ -81,4 +81,93 @@ describe Puppet::String do end it "should be able to load all actions in all search paths" + + describe "with string-level options" do + it "should support options without arguments" do + string = Puppet::String.new(:with_options, '0.0.1') do + option :foo + end + string.should be_an_instance_of Puppet::String + string.should be_option :foo + end + + it "should support options with an empty block" do + string = Puppet::String.new(:with_options, '0.0.1') do + option :foo do + # this section deliberately left blank + end + end + string.should be_an_instance_of Puppet::String + string.should be_option :foo + end + + it "should return all the string-level options" do + string = Puppet::String.new(:with_options, '0.0.1') do + option :foo + option :bar + end + string.options.should =~ [:foo, :bar] + end + + it "should not return any action-level options" do + string = Puppet::String.new(:with_options, '0.0.1') do + option :foo + option :bar + action :baz do + option :quux + end + end + string.options.should =~ [:foo, :bar] + end + + it "should fail when a duplicate option is added" do + expect { + Puppet::String.new(:action_level_options, '0.0.1') do + option :foo + option :foo + end + }.should raise_error ArgumentError, /option foo already defined for/i + end + + it "should fail when a string option duplicates an action option" do + expect { + Puppet::String.new(:action_level_options, '0.0.1') do + action :bar do option :foo end + option :foo + end + }.should raise_error ArgumentError, /foo already defined on action bar/i + end + + it "should work when two actions have the same option" do + string = Puppet::String.new(:with_options, '0.0.1') do + action :foo do option :quux end + action :bar do option :quux end + end + + string.get_action(:foo).options.should =~ [:quux] + string.get_action(:bar).options.should =~ [:quux] + end + end + + describe "with inherited options" do + let :string do + parent = Class.new(Puppet::String) + parent.option(:inherited, :type => :string) + string = parent.new(:example, '0.2.1') + string.option(:local) + string + end + + describe "#options" do + it "should list inherited options" do + string.options.should =~ [:inherited, :local] + end + end + + describe "#get_option" do + it "should return an inherited option object" do + string.get_option(:inherited).should be_an_instance_of Puppet::String::Option + end + end + end end |
