summaryrefslogtreecommitdiffstats
path: root/lib/puppet/interface/action.rb
blob: e4a37a1f7684fe46e3226d3183e37e1ad95a1105 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# -*- coding: utf-8 -*-
require 'puppet/interface'
require 'puppet/interface/option'

class Puppet::Interface::Action
  def initialize(face, name, attrs = {})
    raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/
    @face    = face
    @name    = name.to_sym
    @options = {}
    attrs.each do |k, v| send("#{k}=", v) end
  end

  attr_reader :name
  def to_s() "#{@face}##{@name}" 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 faces delegate to
  # the action object for invocation and all.
  #
  # It turns out that we have a binding problem to solve: @face 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 = @face.get_action(name)
  #     @action.invoke(arg1, arg2, arg3)
  #
  # ...with...
  #
  #     @action = @face.get_action(name)
  #     @face.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)
  #   @face.send(name, *args, &block)
  # end

  def when_invoked=(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 @face.is_a?(Class)
      @face.class_eval do eval wrapper, nil, file, line end
      @face.define_method(internal_name, &block)
    else
      @face.instance_eval do eval wrapper, nil, file, line end
      @face.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 = @face.get_option(name) then
        raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@face}"
      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 + @face.options).sort
  end

  def get_option(name)
    @options[name.to_sym] || @face.get_option(name)
  end
end