diff --git a/lib/puppet/string/action.rb b/lib/puppet/string/action.rb index ff419c090..ee3b2991b 100644 --- a/lib/puppet/string/action.rb +++ b/lib/puppet/string/action.rb @@ -1,89 +1,121 @@ # -*- coding: utf-8 -*- require 'puppet/string' require 'puppet/string/option' class Puppet::String::Action attr_reader :name def to_s "#{@string}##{@name}" end def initialize(string, name, attrs = {}) 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 # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the string delegate to # the action object for invocation and all. # # It turns out that we have a binding problem to solve: @string was bound to # the parent class, not the subclass instance, and we don't pass the # appropriate context or change the binding enough to make this work. # # We could hack around it, by either mandating that you pass the context in # to invoke, or try to get the binding right, but that has probably got # subtleties that we don't instantly think of – especially around threads. # # So, we are pulling this method for now, and will return it to life when we # have the time to resolve the problem. For now, you should replace... # # @action = @string.get_action(name) # @action.invoke(arg1, arg2, arg3) # # ...with... # # @action = @string.get_action(name) # @string.send(@action.name, arg1, arg2, arg3) # # I understand that is somewhat cumbersome, but it functions as desired. # --daniel 2011-03-31 # # PS: This code is left present, but commented, to support this chunk of # documentation, for the benefit of the reader. # # def invoke(*args, &block) # @string.send(name, *args, &block) # end def invoke=(block) + # We need to build an instance method as a wrapper, using normal code, to + # be able to expose argument defaulting between the caller and definer in + # the Ruby API. An extra method is, sadly, required for Ruby 1.8 to work. + # + # In future this also gives us a place to hook in additional behaviour + # such as calling out to the action instance to validate and coerce + # parameters, which avoids any exciting context switching and all. + # + # Hopefully we can improve this when we finally shuffle off the last of + # Ruby 1.8 support, but that looks to be a few "enterprise" release eras + # away, so we are pretty stuck with this for now. + # + # Patches to make this work more nicely with Ruby 1.9 using runtime + # version checking and all are welcome, but they can't actually help if + # the results are not totally hidden away in here. + # + # Incidentally, we though about vendoring evil-ruby and actually adjusting + # the internal C structure implementation details under the hood to make + # this stuff work, because it would have been cleaner. Which gives you an + # idea how motivated we were to make this cleaner. Sorry. --daniel 2011-03-31 + + internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym + file = __FILE__ + "+eval" + line = __LINE__ + 1 + wrapper = "def #{@name}(*args, &block) + args << {} unless args.last.is_a? Hash + args << block if block_given? + self.__send__(#{internal_name.inspect}, *args) + end" + if @string.is_a?(Class) - @string.define_method(@name, &block) + @string.class_eval do eval wrapper, nil, file, line end + @string.define_method(internal_name, &block) else - @string.meta_def(@name, &block) + @string.instance_eval do eval wrapper, nil, file, line end + @string.meta_def(internal_name, &block) end end def add_option(option) option.aliases.each do |name| if conflict = get_option(name) then raise ArgumentError, "Option #{option} conflicts with existing option #{conflict}" elsif conflict = @string.get_option(name) then raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@string}" end end option.aliases.each do |name| @options[name] = option end 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/indirector.rb b/lib/puppet/string/indirector.rb index 0f5f405ff..48280cc77 100644 --- a/lib/puppet/string/indirector.rb +++ b/lib/puppet/string/indirector.rb @@ -1,86 +1,86 @@ require 'puppet' require 'puppet/string' class Puppet::String::Indirector < Puppet::String warn "REVISIT: Need to redefine this to take arguments again, eh." option "--terminus TERMINUS" do desc "REVISIT: You can select a terminus, which has some bigger effect that we should describe in this file somehow." end def self.indirections Puppet::Indirector::Indirection.instances.collect { |t| t.to_s }.sort end def self.terminus_classes(indirection) Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort end action :destroy do invoke { |*args| call_indirection_method(:destroy, *args) } end action :find do invoke { |*args| call_indirection_method(:find, *args) } end action :save do invoke { |*args| call_indirection_method(:save, *args) } end action :search do invoke { |*args| call_indirection_method(:search, *args) } end # Print the configuration for the current terminus class action :info do invoke do |*args| if t = indirection.terminus_class puts "Run mode '#{Puppet.run_mode.name}': #{t}" else $stderr.puts "No default terminus class for run mode '#{Puppet.run_mode.name}'" end end end attr_accessor :from def indirection_name @indirection_name || name.to_sym end # Here's your opportunity to override the indirection name. By default # it will be the same name as the string. def set_indirection_name(name) @indirection_name = name end # Return an indirection associated with an string, if one exists # One usually does. def indirection unless @indirection @indirection = Puppet::Indirector::Indirection.instance(indirection_name) @indirection or raise "Could not find terminus for #{indirection_name}" end @indirection end def set_terminus(from) begin indirection.terminus_class = from rescue => detail raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }" end end def call_indirection_method(method, *args) begin - result = indirection.send(method, *args) + result = indirection.__send__(method, *args) rescue => detail puts detail.backtrace if Puppet[:trace] raise "Could not call '#{method}' on '#{indirection_name}': #{detail}" end result end end