Class: Petra::Proxies::MethodHandlers

Inherits:
Object
  • Object
show all
Defined in:
lib/petra/proxies/method_handlers.rb

Overview

This class holds method handlers for certain method groups a proxy may encounter (readers, writers, etc). They are encapsulated in an own class instead of a module mixin to keep the proxy objects as small as possible, hopefully avoiding using the same method names as a proxied object

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(proxy, proxy_binding) ⇒ MethodHandlers

Returns a new instance of MethodHandlers.



13
14
15
16
# File 'lib/petra/proxies/method_handlers.rb', line 13

def initialize(proxy, proxy_binding)
  @proxy         = proxy
  @proxy_binding = proxy_binding
end

Class Method Details

.proxy_method(name, underscore_prefix = false) ⇒ Object

Helper method to call private (or public) methods on the associated proxy object. It will define an own method in this class which can be used as if it would be called directly on the proxy.

Parameters:

  • name (String, Symbol)

    the method name to be called on the proxy

  • underscore_prefix (Boolean) (defaults to: false)

    If set to true, two underscores will be prefixed to the given method name, e.g. for __attribute_reader?



30
31
32
33
34
35
36
37
38
# File 'lib/petra/proxies/method_handlers.rb', line 30

def self.proxy_method(name, underscore_prefix = false)
  define_method(name) do |*args|
    if underscore_prefix
      @proxy.send("__#{name}", *args)
    else
      @proxy.send(name, *args)
    end
  end
end

.proxy_methods(*methods, underscore_prefix: false) ⇒ Object

Shortcut function to call ‘proxy_method` for multiple functions



43
44
45
# File 'lib/petra/proxies/method_handlers.rb', line 43

def self.proxy_methods(*methods, underscore_prefix: false)
  methods.each { |m| proxy_method(m, underscore_prefix) }
end

Instance Method Details

#execute_missing_queue(method_name, *args, block: nil) {|queue = []| ... } ⇒ Object

Yields an array and executes the given handlers afterwards.

Parameters:

  • block (Proc, NilClass) (defaults to: nil)

    As this method itself accepts a block, a proc passed to method_missing has to be passed in in its normal parameter form

Yields:

  • (queue = [])

Returns:

  • (Object)

    the first handler’s execution result



59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/petra/proxies/method_handlers.rb', line 59

def execute_missing_queue(method_name, *args, block: nil)
  yield queue = []
  queue << :handle_missing_method if queue.empty?

  send(queue.first, method_name, *args).tap do
    queue[1..-1].each do |handler|
      if block
        send(handler, method_name, *args, &block)
      else
        send(handler, method_name, *args)
      end
    end
  end
end

#handle_attribute_change(method_name, *args) ⇒ Object

Logs changes made to attributes of the proxied object. This means that the attribute change is documented within the currently active transaction section and added to the temporary write set.



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
# File 'lib/petra/proxies/method_handlers.rb', line 107

def handle_attribute_change(method_name, *args)
  # Remove a possible "=" at the end of the setter method name
  attribute_name = method_name
  attribute_name = method_name[0..-2] if method_name =~ /^.*=$/

  # As there might not be a corresponding getter, our fallback value for
  # the old attribute value is +nil+. TODO: See if this causes unexpected behaviour
  old_value      = nil
  # To get the actual old value of an attribute reader, we have to
  # act as if it was requested externally by either serving it from the object
  # itself or the transaction's write set.
  # TODO: (Better) way to determine the reader method name, it might be a different one...
  old_value      = handle_attribute_read(attribute_name) if attribute_reader?(attribute_name)

  # As we currently only handle simple setters, we expect the first given argument
  # to be the new attribute value.
  new_value      = args.first # type_cast_attribute_value(attribute_name, args.first)

  transaction.log_attribute_change(@proxy,
                                   attribute: attribute_name,
                                   old_value: old_value,
                                   new_value: new_value,
                                   method:    method_name.to_s)

  new_value
end

#handle_attribute_read(method_name, *args) ⇒ Object

Handles a getter method for the proxied object. As attribute changes are not actually forwarded to the actual object, we have to retrieve them from the transaction’s write set.



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
# File 'lib/petra/proxies/method_handlers.rb', line 139

def handle_attribute_read(method_name, *args)
  # We wrote this attribute before, so we have to serve its value
  # from the transaction's write set
  if transaction.attribute_value?(@proxy, attribute: method_name)
    # As we wrote this attribute before, we have the value we read back then on record.
    # Therefore, we may check if the value changed in the mean time which would invalidate
    # the transaction (most likely).
    transaction.verify_attribute_integrity!(@proxy, attribute: method_name)

    transaction.attribute_value(@proxy, attribute: method_name).tap do |result|
      Petra.logger.debug "Served value from write set: #{method_name}  => #{result}", :yellow, :bold
    end
  elsif transaction.read_attribute_value?(@proxy, attribute: method_name)
    # If we didn't write the attribute before, we may at least have already read it.
    # In this case, we don't have to generate a new read log entry
    transaction.verify_attribute_integrity!(@proxy, attribute: method_name)

    # We also may simply return the last accepted read set value
    transaction.read_attribute_value(@proxy, attribute: method_name).tap do |result|
      Petra.logger.debug "Re-read attribute: #{method_name}  => #{result}", :yellow, :bold
    end
  else
    proxied_object.send(method_name, *args).tap do |val|
      transaction.log_attribute_read(@proxy, attribute: method_name, value: val, method: method_name)
    end
  end
end

#handle_dynamic_attribute_read(method_name, *args) ⇒ Object

A “dynamic attribute” in this case is a method which usually formats one or multiple attributes and returns the result. An example would be ‘#first_name #last_name` within a user class. As methods which are no simple readers/writers are usually forwarded to the proxied object, we have to make sure that these methods are called in this proxy’s context, otherwise the used attribute readers would return the actual values, not the ones from our write set.

There is no particularly elegant way to achieve this as all forms of bind or instance_eval/exec would not set the correct self (or be incompatible), we generate a new proc from the method’s source code and call it within our own context. This should therefore be only used for dynamic attributes like the above example, more complex methods might cause serious problems.



98
99
100
# File 'lib/petra/proxies/method_handlers.rb', line 98

def handle_dynamic_attribute_read(method_name, *args)
  method_source_proc(method_name).call(*args)
end

#handle_missing_method(method_name, *args, &block) ⇒ Object

Calls the given method on the proxied object and optionally wraps the result in another petra proxy



78
79
80
81
82
# File 'lib/petra/proxies/method_handlers.rb', line 78

def handle_missing_method(method_name, *args, &block)
  proxied_object
    .public_send(method_name, *args, &block)
    .petra(inherited: true, configuration_args: [method_name.to_s])
end

#handle_object_destruction(method_name, *args) ⇒ Object

Handles calls to a method which destroys the proxied object



186
187
188
189
# File 'lib/petra/proxies/method_handlers.rb', line 186

def handle_object_destruction(method_name, *args)
  transaction.log_object_destruction(@proxy, method: method_name, args: args)
  true
end

#handle_object_persistence(method_name, *args) ⇒ Object

Handles calls to a method which persists the proxied object. As we may not actually call the method on the proxied object, we may only log the persistence.

This is a very simple behaviour, so it makes sense to handle persistence methods differently in specialized object proxies (see ActiveRecordProxy)

TODO: Log parameters given to the persistence method so they can be used during the commit phase



177
178
179
180
181
# File 'lib/petra/proxies/method_handlers.rb', line 177

def handle_object_persistence(method_name, *args)
  transaction.log_object_persistence(@proxy, method: method_name, args: args)
  # TODO: Find a better return value for pure persistence calls
  true
end

#method_source_proc(method_name) ⇒ Object

Generates a new Proc object from the source code of a given instance method of the proxied object.

TODO: This does not work well with #unloadable, e.g. in Rails development environment TODO: method.parameters returns the required and optional parameters, these could be handed to the proc TODO: what happens with dynamically generated methods? is there a practical way to achieve this?



203
204
205
206
207
208
209
# File 'lib/petra/proxies/method_handlers.rb', line 203

def method_source_proc(method_name)
  method        = proxied_object.method(method_name.to_sym)
  method_source = method.source.lines[1..-2].join
  proc do
    @proxy_binding.eval method_source
  end
end