File: adaptable.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (197 lines) | stat: -rw-r--r-- 7,898 bytes parent folder | download
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# Adaptable is a mix-in module that adds adaptability to a class.
# This means that an adapter can
# associate itself with an instance of the class and store additional data/have behavior.
#
# This mechanism should be used when there is a desire to keep implementation concerns separate.
# In Ruby it is always possible to open and modify a class or instance to teach it new tricks, but it
# is however not possible to do this for two different versions of some service at the same time.
# The Adaptable pattern is also good when only a few of the objects of some class needs to have extra
# information (again possible in Ruby by adding instance variables dynamically). In fact, the implementation
# of Adaptable does just that; it adds an instance variable named after the adapter class and keeps an
# instance of this class in this slot.
#
# @note the implementation details; the fact that an instance variable is used to keep the adapter
#   instance data should not
#   be exploited as the implementation of _being adaptable_ may change in the future.
# @api private
#
module Puppet::Pops
module Adaptable
  # Base class for an Adapter.
  #
  # A typical adapter just defines some accessors.
  #
  # A more advanced adapter may need to setup the adapter based on the object it is adapting.
  # @example Making Duck adaptable
  #   class Duck
  #     include Puppet::Pops::Adaptable
  #   end
  # @example Giving a Duck a nick name
  #   class NickNameAdapter < Puppet::Pops::Adaptable::Adapter
  #     attr_accessor :nick_name
  #   end
  #   d = Duck.new
  #   NickNameAdapter.adapt(d).nick_name = "Daffy"
  #   NickNameAdapter.get(d).nick_name # => "Daffy"
  #
  # @example Giving a Duck a more elaborate nick name
  #   class NickNameAdapter < Puppet::Pops::Adaptable::Adapter
  #     attr_accessor :nick_name, :object
  #     def initialize o
  #       @object = o
  #       @nick_name = "Yo"
  #     end
  #     def nick_name
  #       "#{@nick_name}, the #{o.class.name}"
  #     end
  #     def NickNameAdapter.create_adapter(o)
  #       x = new o
  #       x
  #     end
  #   end
  #   d = Duck.new
  #   n = NickNameAdapter.adapt(d)
  #   n.nick_name # => "Yo, the Duck"
  #   n.nick_name = "Daffy"
  #   n.nick_name # => "Daffy, the Duck"
  # @example Using a block to set values
  #   NickNameAdapter.adapt(o) { |a| a.nick_name = "Buddy!" }
  #   NickNameAdapter.adapt(o) { |a, o| a.nick_name = "You're the best #{o.class.name} I met."}
  #
  class Adapter
    # Returns an existing adapter for the given object, or nil, if the object is not
    # adapted.
    #
    # @param o [Adaptable] object to get adapter from
    # @return [Adapter<self>] an adapter of the same class as the receiver of #get
    # @return [nil] if the given object o has not been adapted by the receiving adapter
    # @raise [ArgumentError] if the object is not adaptable
    #
    def self.get(o)
      attr_name = self_attr_name
      o.instance_variable_get(attr_name)
    end

    # Returns an existing adapter for the given object, or creates a new adapter if the
    # object has not been adapted, or the adapter has been cleared.
    #
    # @example Using a block to set values
    #   NickNameAdapter.adapt(o) { |a| a.nick_name = "Buddy!" }
    #   NickNameAdapter.adapt(o) { |a, o| a.nick_name = "Your the best #{o.class.name} I met."}
    # @overload adapt(o)
    # @overload adapt(o, {|adapter| block})
    # @overload adapt(o, {|adapter, o| block})
    # @param o [Adaptable] object to add adapter to
    # @yieldparam adapter [Adapter<self>] the created adapter
    # @yieldparam o [Adaptable] optional, the given adaptable
    # @param block [Proc] optional, evaluated in the context of the adapter (existing or new)
    # @return [Adapter<self>] an adapter of the same class as the receiver of the call
    # @raise [ArgumentError] if the given object o is not adaptable
    #
    def self.adapt(o, &block)
      attr_name = self_attr_name
      value = o.instance_variable_get(attr_name)
      adapter = value || associate_adapter(create_adapter(o), o)
      if block_given?
        if block.arity == 1
          block.call(adapter)
        else
          block.call(adapter, o)
        end
      end
      adapter
    end

    # Creates a new adapter, associates it with the given object and returns the adapter.
    #
    # @example Using a block to set values
    #   NickNameAdapter.adapt_new(o) { |a| a.nick_name = "Buddy!" }
    #   NickNameAdapter.adapt_new(o) { |a, o| a.nick_name = "Your the best #{o.class.name} I met."}
    # This is used when a fresh adapter is wanted instead of possible returning an
    # existing adapter as in the case of {Adapter.adapt}.
    # @overload adapt_new(o)
    # @overload adapt_new(o, {|adapter| block})
    # @overload adapt_new(o, {|adapter, o| block})
    # @yieldparam adapter [Adapter<self>] the created adapter
    # @yieldparam o [Adaptable] optional, the given adaptable
    # @param o [Adaptable] object to add adapter to
    # @param block [Proc] optional, evaluated in the context of the new adapter
    # @return [Adapter<self>] an adapter of the same class as the receiver of the call
    # @raise [ArgumentError] if the given object o is not adaptable
    #
    def self.adapt_new(o, &block)
      adapter = associate_adapter(create_adapter(o), o)
      if block_given?
        if block.arity == 1
          block.call(adapter)
        else
          block.call(adapter, o)
        end
      end
      adapter
    end

    # Clears the adapter set in the given object o. Returns any set adapter or nil.
    # @param o [Adaptable] the object where the adapter should be cleared
    # @return [Adapter] if an adapter was set
    # @return [nil] if the adapter has not been set
    #
    def self.clear(o)
      attr_name = self_attr_name
      if o.instance_variable_defined?(attr_name)
        o.send(:remove_instance_variable, attr_name)
      else
        nil
      end
    end

    # This base version creates an instance of the class (i.e. an instance of the concrete subclass
    # of Adapter). A Specialization may want to create an adapter instance specialized for the given target
    # object.
    # @param o [Adaptable] The object to adapt. This implementation ignores this variable, but a
    #   specialization may want to initialize itself differently depending on the object it is adapting.
    # @return [Adapter<self>] instance of the subclass of Adapter receiving the call
    #
    def self.create_adapter(o)
      new
    end

    # Associates the given adapter with the given target object
    # @param adapter [Adapter] the adapter to associate with the given object _o_
    # @param o [Adaptable] the object to adapt
    # @return [adapter] the given adapter
    #
    def self.associate_adapter(adapter, o)
      o.instance_variable_set(self_attr_name, adapter)
      adapter
    end

    # Returns a suitable instance variable name given a class name.
    # The returned string is the fully qualified name of a class with '::' replaced by '_' since
    # '::' is not allowed in an instance variable name.
    # @param name [String] the fully qualified name of a class
    # @return [String] the name with all '::' replaced by '_'
    # @api private
    #
    def self.instance_var_name(name)
      name.split(DOUBLE_COLON).join(USCORE)
    end

    # Returns the name of the class, or the name of the type if the class represents an Object type
    # @return [String] the name of the class or type
    def self.type_name
      self.name
    end

    # Returns a suitable instance variable name for the _name_ of this instance. The name is created by calling
    # Adapter#instance_var_name and then cached.
    # @return [String] the instance variable name for _name_
    # @api private
    #
    def self.self_attr_name
      @attr_name_sym ||= :"@#{instance_var_name(type_name)}"
    end
  end
end
end