petra_core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +83 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +74 -0
- data/MIT-LICENSE +20 -0
- data/README.md +726 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/examples/continuation_error.rb +125 -0
- data/examples/dining_philosophers.rb +138 -0
- data/examples/showcase.rb +54 -0
- data/lib/petra/components/entries/attribute_change.rb +29 -0
- data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
- data/lib/petra/components/entries/attribute_read.rb +20 -0
- data/lib/petra/components/entries/object_destruction.rb +22 -0
- data/lib/petra/components/entries/object_initialization.rb +19 -0
- data/lib/petra/components/entries/object_persistence.rb +26 -0
- data/lib/petra/components/entries/read_integrity_override.rb +42 -0
- data/lib/petra/components/entry_set.rb +87 -0
- data/lib/petra/components/log_entry.rb +342 -0
- data/lib/petra/components/proxy_cache.rb +209 -0
- data/lib/petra/components/section.rb +543 -0
- data/lib/petra/components/transaction.rb +405 -0
- data/lib/petra/components/transaction_manager.rb +214 -0
- data/lib/petra/configuration/base.rb +132 -0
- data/lib/petra/configuration/class_configurator.rb +309 -0
- data/lib/petra/configuration/configurator.rb +67 -0
- data/lib/petra/core_ext.rb +27 -0
- data/lib/petra/exceptions.rb +181 -0
- data/lib/petra/persistence_adapters/adapter.rb +154 -0
- data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
- data/lib/petra/proxies/abstract_proxy.rb +149 -0
- data/lib/petra/proxies/enumerable_proxy.rb +44 -0
- data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
- data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
- data/lib/petra/proxies/method_handlers.rb +213 -0
- data/lib/petra/proxies/module_proxy.rb +12 -0
- data/lib/petra/proxies/object_proxy.rb +310 -0
- data/lib/petra/util/debug.rb +45 -0
- data/lib/petra/util/extended_attribute_accessors.rb +51 -0
- data/lib/petra/util/field_accessors.rb +35 -0
- data/lib/petra/util/registrable.rb +48 -0
- data/lib/petra/util/test_helpers.rb +9 -0
- data/lib/petra/version.rb +5 -0
- data/lib/petra.rb +100 -0
- data/lib/tasks/petra_tasks.rake +5 -0
- data/petra.gemspec +36 -0
- metadata +208 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Petra
|
4
|
+
module Proxies
|
5
|
+
class AbstractProxy
|
6
|
+
|
7
|
+
class << self
|
8
|
+
#
|
9
|
+
# Builds an ObjectProxy for the given object.
|
10
|
+
# If a more specific proxy class exists for the given object,
|
11
|
+
# it will be used instead of the generic Petra::Proxies::ObjectProxy.
|
12
|
+
#
|
13
|
+
# If there is no proxy for the exact class of the given +object+,
|
14
|
+
# its superclasses are automatically tested.
|
15
|
+
#
|
16
|
+
def for(object, inherited: false, **options)
|
17
|
+
# If the given object is configured not to use a possibly existing
|
18
|
+
# specialized proxy (e.g. the ActiveRecord::Base proxy), we simply
|
19
|
+
# build a default ObjectProxy for it, but we'll still try to extend it using
|
20
|
+
# available ModuleProxies
|
21
|
+
default_proxy = ObjectProxy.new(object, inherited, **options)
|
22
|
+
default_proxy.send :mixin_module_proxies!
|
23
|
+
return default_proxy unless inherited_config_for(object, :use_specialized_proxy)
|
24
|
+
|
25
|
+
# Otherwise, we search for a specialized proxy for the object's class
|
26
|
+
# and its superclasses until we either find one or reach the
|
27
|
+
# default ObjectProxy
|
28
|
+
klass = object.is_a?(Class) ? object : object.class
|
29
|
+
klass = klass.superclass until available_class_proxies.key?(klass.to_s)
|
30
|
+
proxy = available_class_proxies[klass.to_s].constantize.new(object, inherited, **options)
|
31
|
+
|
32
|
+
# If we reached Object, we might still find one or more ModuleProxy module we might
|
33
|
+
# mix into the resulting ObjectProxy. Otherwise, the specialized proxy will most likely
|
34
|
+
# have included the necessary ModuleProxies itself.
|
35
|
+
proxy.send(:mixin_module_proxies!) if proxy.instance_of?(Petra::Proxies::ObjectProxy)
|
36
|
+
proxy
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Determines the available object proxy classes and the ruby classes they
|
41
|
+
# can be used for. All classes in the Petra::Proxies namespace are automatically
|
42
|
+
# recognized as long as they define a CLASS_NAMES constant.
|
43
|
+
#
|
44
|
+
# If multiple proxies specify the same class name, the last one by sorting wins.
|
45
|
+
#
|
46
|
+
# @return [Hash] The available proxy classes in the format ("ClassName" => "ProxyClassName")
|
47
|
+
#
|
48
|
+
def available_class_proxies
|
49
|
+
@available_class_proxies ||= Petra::Proxies.constants.each_with_object({}) do |c, h|
|
50
|
+
klass = Petra::Proxies.const_get(c)
|
51
|
+
# Skip non-class constants (this includes modules)
|
52
|
+
next unless klass.is_a?(Class)
|
53
|
+
# Skip every class which is not an ObjectProxy. There shouldn't be any
|
54
|
+
# in this namespace, but you never know...
|
55
|
+
next unless klass <= Petra::Proxies::ObjectProxy
|
56
|
+
# Skip proxy classes which do not specify which classes
|
57
|
+
# they were built for
|
58
|
+
next unless klass.const_defined?(:CLASS_NAMES)
|
59
|
+
|
60
|
+
klass.const_get(:CLASS_NAMES).each { |n| h[n] = "Petra::Proxies::#{c}" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# @see #available_class_proxies
|
66
|
+
#
|
67
|
+
# Returns only module proxies
|
68
|
+
#
|
69
|
+
def available_module_proxies
|
70
|
+
@available_module_proxies ||= Petra::Proxies.constants.each_with_object({}) do |c, h|
|
71
|
+
klass = Petra::Proxies.const_get(c)
|
72
|
+
next unless klass.is_a?(Module)
|
73
|
+
next unless klass.included_modules.include?(Petra::Proxies::ModuleProxy)
|
74
|
+
next unless klass.const_defined?(:MODULE_NAMES)
|
75
|
+
|
76
|
+
klass.const_get(:MODULE_NAMES).each { |n| h[n] = "Petra::Proxies::#{c}" }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Retrieves a configuration value with the given name respecting
|
82
|
+
# custom configurations made for its class (or class family)
|
83
|
+
#
|
84
|
+
def inherited_config_for(object, name, *args)
|
85
|
+
# If the proxied object already is a class, we don't use its class (Class)
|
86
|
+
# as there is a high chance nobody will ever use this object proxy on
|
87
|
+
# this level of meta programming
|
88
|
+
klass = object.is_a?(Class) ? object : object.class
|
89
|
+
Petra.configuration.class_configurator(klass).__inherited_value(name, *args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# As it might happen that a custom proxy has to be defined for behaviour
|
95
|
+
# introduced to different classes as an included module (an example would be Enumerable),
|
96
|
+
# it has to be possible to define an equivalent to object proxies for them.
|
97
|
+
# This function inspects all modules which were previously included into
|
98
|
+
# the proxied object's singleton class and automatically adds matching module proxies.
|
99
|
+
#
|
100
|
+
# Please take a look at Petra::Proxies::EnumerableProxy for an example module proxy
|
101
|
+
#
|
102
|
+
def mixin_module_proxies!
|
103
|
+
# Neither symbols nor fixnums may have singleton classes, see the corresponding Kernel method
|
104
|
+
return if proxied_object.is_a?(Integer) || proxied_object.is_a?(Symbol)
|
105
|
+
|
106
|
+
# Do not load ModuleProxies if the object's configuration denies it
|
107
|
+
return unless object_config(:mixin_module_proxies)
|
108
|
+
|
109
|
+
proxied_object.singleton_class.included_modules.each do |mod|
|
110
|
+
proxy_module = Petra::Proxies::ObjectProxy.available_module_proxies[mod.to_s].try(:constantize)
|
111
|
+
# Skip all included modules without ModuleProxies
|
112
|
+
next unless proxy_module
|
113
|
+
|
114
|
+
singleton_class.class_eval do
|
115
|
+
# Extend the proxy with the module proxy's class methods
|
116
|
+
extend proxy_module.const_get(:ClassMethods) if proxy_module.const_defined?(:ClassMethods)
|
117
|
+
|
118
|
+
# Include the module proxy's instance methods
|
119
|
+
include proxy_module.const_get(:InstanceMethods) if proxy_module.const_defined?(:InstanceMethods)
|
120
|
+
|
121
|
+
proxy_module.const_get(:INCLUDES).each { |m| include m } if proxy_module.const_defined?(:INCLUDES)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# @return [Petra::Components::Transaction] the currently active transaction
|
128
|
+
#
|
129
|
+
def transaction
|
130
|
+
Petra.transaction_manager.current_transaction
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# @see #inherited_config_for, the proxied object is automatically passed in
|
135
|
+
# as first parameter
|
136
|
+
#
|
137
|
+
def object_config(name, *args)
|
138
|
+
self.class.inherited_config_for(proxied_object, name, *args)
|
139
|
+
end
|
140
|
+
|
141
|
+
delegate :inspect, to: :proxied_object
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def initialize; end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'petra/proxies/module_proxy'
|
4
|
+
|
5
|
+
module Petra
|
6
|
+
module Proxies
|
7
|
+
#
|
8
|
+
# Module Proxy which is used to proxy classes which include Enumerable, such as
|
9
|
+
# Enumerator or Array. It contains wrappers for the default enumerator functions to
|
10
|
+
# ensure that objects yielded to their blocks are correctly wrapped in Petra proxies (if needed)
|
11
|
+
#
|
12
|
+
module EnumerableProxy
|
13
|
+
include ModuleProxy
|
14
|
+
MODULE_NAMES = %w[Enumerable].freeze
|
15
|
+
INCLUDES = [Enumerable].freeze
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
#
|
19
|
+
# We have to define our own #each method for the singleton class' Enumerable
|
20
|
+
# It basically just wraps the original enum's entries in proxies and executes
|
21
|
+
# the "normal" #each
|
22
|
+
#
|
23
|
+
def each(&block)
|
24
|
+
Petra::Proxies::EnumerableProxy.proxy_entries(proxied_object).each(&block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Ensures the the objects yielded to blocks are actually petra proxies.
|
30
|
+
# This is necessary as the internal call to +each+ would be forwarded to the
|
31
|
+
# actual Enumerable object and result in unproxied objects.
|
32
|
+
#
|
33
|
+
# This method will only proxy objects which allow this through the class config
|
34
|
+
# as the enum's entries are seen as inherited objects.
|
35
|
+
# `[]` is used as method causing the proxy creation as it's closest to what's actually happening.
|
36
|
+
#
|
37
|
+
# @return [Array<Petra::Proxies::ObjectProxy>]
|
38
|
+
#
|
39
|
+
def self.proxy_entries(enum, surrogate_method: '[]')
|
40
|
+
enum.entries.map { |o| o.petra(inherited: true, configuration_args: [surrogate_method]) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Petra
|
4
|
+
module Proxies
|
5
|
+
module Handlers
|
6
|
+
class AttributeReadHandler < MissingMethodHandler
|
7
|
+
add_constraint(:before, :object_persistence)
|
8
|
+
|
9
|
+
def self.identifier
|
10
|
+
:attribute_read
|
11
|
+
end
|
12
|
+
|
13
|
+
def applicable?(method_name)
|
14
|
+
proxy.send(:__attribute_reader?, method_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle(method_name, *args)
|
18
|
+
if transaction.attribute_value?(@proxy, attribute: method_name)
|
19
|
+
# As we read this attribute before, we have the value we read back then on record.
|
20
|
+
# Therefore, we may check if the value changed in the mean time which would invalidate
|
21
|
+
# the transaction (most likely).
|
22
|
+
transaction.verify_attribute_integrity!(@proxy, attribute: method_name)
|
23
|
+
|
24
|
+
transaction.attribute_value(@proxy, attribute: method_name).tap do |result|
|
25
|
+
Petra.logger.debug "Served value from write set: #{method_name} => #{result}", :yellow, :bold
|
26
|
+
end
|
27
|
+
elsif transaction.read_attribute_value?(@proxy, attribute: method_name)
|
28
|
+
# If we didn't write the attribute before, we may at least have already read it.
|
29
|
+
# In this case, we don't have to generate a new read log entry
|
30
|
+
transaction.verify_attribute_integrity!(@proxy, attribute: method_name)
|
31
|
+
|
32
|
+
# We also may simply return the last accepted read set value
|
33
|
+
transaction.read_attribute_value(@proxy, attribute: method_name).tap do |result|
|
34
|
+
Petra.logger.debug "Re-read attribute: #{method_name} => #{result}", :yellow, :bold
|
35
|
+
end
|
36
|
+
else
|
37
|
+
proxied_object.send(method_name, *args).tap do |val|
|
38
|
+
transaction.log_attribute_read(@proxy, attribute: method_name, value: val, method: method_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Petra
|
4
|
+
module Proxies
|
5
|
+
module Handlers
|
6
|
+
class MissingMethodHandler
|
7
|
+
def initialize(proxy)
|
8
|
+
@proxy = proxy
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :proxy
|
12
|
+
delegate :transaction, to: :@proxy
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def constraints
|
16
|
+
@constraints ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Adds a constraint to this handler class regarding the position
|
21
|
+
# it will end up in when actually executing the handlers.
|
22
|
+
#
|
23
|
+
# @param [:before, :after, :<, :>] position
|
24
|
+
# @param [String, Symbol] other_handler
|
25
|
+
# The other handler's identifier
|
26
|
+
#
|
27
|
+
def add_constraint(position, other_handler)
|
28
|
+
method = position.to_sym == :before ? :< : :>
|
29
|
+
constraints << [method, other_handler.to_sym]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def queue_constraints
|
34
|
+
not_implemented
|
35
|
+
end
|
36
|
+
|
37
|
+
def applicable?
|
38
|
+
not_implemented
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle(*)
|
42
|
+
not_implemented
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Petra
|
4
|
+
module Proxies
|
5
|
+
#
|
6
|
+
# This class holds method handlers for certain method groups a proxy
|
7
|
+
# may encounter (readers, writers, etc).
|
8
|
+
# They are encapsulated in an own class instead of a module mixin to keep the
|
9
|
+
# proxy objects as small as possible, hopefully avoiding using the same method names
|
10
|
+
# as a proxied object
|
11
|
+
#
|
12
|
+
class MethodHandlers
|
13
|
+
def initialize(proxy, proxy_binding)
|
14
|
+
@proxy = proxy
|
15
|
+
@proxy_binding = proxy_binding
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Helper method to call private (or public) methods on the associated
|
20
|
+
# proxy object. It will define an own method in this class which can be used
|
21
|
+
# as if it would be called directly on the proxy.
|
22
|
+
#
|
23
|
+
# @param [String, Symbol] name
|
24
|
+
# the method name to be called on the proxy
|
25
|
+
#
|
26
|
+
# @param [Boolean] underscore_prefix
|
27
|
+
# If set to +true+, two underscores will be prefixed to the given method name, e.g.
|
28
|
+
# for +__attribute_reader?+
|
29
|
+
#
|
30
|
+
def self.proxy_method(name, underscore_prefix = false)
|
31
|
+
define_method(name) do |*args|
|
32
|
+
if underscore_prefix
|
33
|
+
@proxy.send("__#{name}", *args)
|
34
|
+
else
|
35
|
+
@proxy.send(name, *args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Shortcut function to call `proxy_method` for multiple functions
|
42
|
+
#
|
43
|
+
def self.proxy_methods(*methods, underscore_prefix: false)
|
44
|
+
methods.each { |m| proxy_method(m, underscore_prefix) }
|
45
|
+
end
|
46
|
+
|
47
|
+
proxy_methods :proxied_object, :transaction, :object_config, :class_proxy?
|
48
|
+
proxy_methods :attribute_reader?, :type_cast_attribute_value, underscore_prefix: true
|
49
|
+
|
50
|
+
#
|
51
|
+
# Yields an array and executes the given handlers afterwards.
|
52
|
+
#
|
53
|
+
# @return [Object] the first handler's execution result
|
54
|
+
#
|
55
|
+
# @param [Proc, NilClass] block
|
56
|
+
# As this method itself accepts a block, a proc passed to
|
57
|
+
# method_missing has to be passed in in its normal parameter form
|
58
|
+
#
|
59
|
+
def execute_missing_queue(method_name, *args, block: nil)
|
60
|
+
yield queue = []
|
61
|
+
queue << :handle_missing_method if queue.empty?
|
62
|
+
|
63
|
+
send(queue.first, method_name, *args).tap do
|
64
|
+
queue[1..-1].each do |handler|
|
65
|
+
if block
|
66
|
+
send(handler, method_name, *args, &block)
|
67
|
+
else
|
68
|
+
send(handler, method_name, *args)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Calls the given method on the proxied object and optionally
|
76
|
+
# wraps the result in another petra proxy
|
77
|
+
#
|
78
|
+
def handle_missing_method(method_name, *args, &block)
|
79
|
+
proxied_object
|
80
|
+
.public_send(method_name, *args, &block)
|
81
|
+
.petra(inherited: true, configuration_args: [method_name.to_s])
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# A "dynamic attribute" in this case is a method which usually formats
|
86
|
+
# one or multiple attributes and returns the result. An example would be `#{first_name} #{last_name}`
|
87
|
+
# within a user class.
|
88
|
+
# As methods which are no simple readers/writers are usually forwarded to the proxied
|
89
|
+
# object, we have to make sure that these methods are called in this proxy's context, otherwise
|
90
|
+
# the used attribute readers would return the actual values, not the ones from our write set.
|
91
|
+
#
|
92
|
+
# There is no particularly elegant way to achieve this as all forms of bind or instance_eval/exec would
|
93
|
+
# not set the correct self (or be incompatible), we generate a new proc from the method's source code
|
94
|
+
# and call it within our own context.
|
95
|
+
# This should therefore be only used for dynamic attributes like the above example, more complex
|
96
|
+
# methods might cause serious problems.
|
97
|
+
#
|
98
|
+
def handle_dynamic_attribute_read(method_name, *args)
|
99
|
+
method_source_proc(method_name).call(*args)
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Logs changes made to attributes of the proxied object.
|
104
|
+
# This means that the attribute change is documented within the currently active transaction
|
105
|
+
# section and added to the temporary write set.
|
106
|
+
#
|
107
|
+
def handle_attribute_change(method_name, *args)
|
108
|
+
# Remove a possible "=" at the end of the setter method name
|
109
|
+
attribute_name = method_name
|
110
|
+
attribute_name = method_name[0..-2] if method_name =~ /^.*=$/
|
111
|
+
|
112
|
+
# As there might not be a corresponding getter, our fallback value for
|
113
|
+
# the old attribute value is +nil+. TODO: See if this causes unexpected behaviour
|
114
|
+
old_value = nil
|
115
|
+
# To get the actual old value of an attribute reader, we have to
|
116
|
+
# act as if it was requested externally by either serving it from the object
|
117
|
+
# itself or the transaction's write set.
|
118
|
+
# TODO: (Better) way to determine the reader method name, it might be a different one...
|
119
|
+
old_value = handle_attribute_read(attribute_name) if attribute_reader?(attribute_name)
|
120
|
+
|
121
|
+
# As we currently only handle simple setters, we expect the first given argument
|
122
|
+
# to be the new attribute value.
|
123
|
+
new_value = args.first # type_cast_attribute_value(attribute_name, args.first)
|
124
|
+
|
125
|
+
transaction.log_attribute_change(@proxy,
|
126
|
+
attribute: attribute_name,
|
127
|
+
old_value: old_value,
|
128
|
+
new_value: new_value,
|
129
|
+
method: method_name.to_s)
|
130
|
+
|
131
|
+
new_value
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Handles a getter method for the proxied object.
|
136
|
+
# As attribute changes are not actually forwarded to the actual object,
|
137
|
+
# we have to retrieve them from the transaction's write set.
|
138
|
+
#
|
139
|
+
def handle_attribute_read(method_name, *args)
|
140
|
+
# We wrote this attribute before, so we have to serve its value
|
141
|
+
# from the transaction's write set
|
142
|
+
if transaction.attribute_value?(@proxy, attribute: method_name)
|
143
|
+
# As we wrote this attribute before, we have the value we read back then on record.
|
144
|
+
# Therefore, we may check if the value changed in the mean time which would invalidate
|
145
|
+
# the transaction (most likely).
|
146
|
+
transaction.verify_attribute_integrity!(@proxy, attribute: method_name)
|
147
|
+
|
148
|
+
transaction.attribute_value(@proxy, attribute: method_name).tap do |result|
|
149
|
+
Petra.logger.debug "Served value from write set: #{method_name} => #{result}", :yellow, :bold
|
150
|
+
end
|
151
|
+
elsif transaction.read_attribute_value?(@proxy, attribute: method_name)
|
152
|
+
# If we didn't write the attribute before, we may at least have already read it.
|
153
|
+
# In this case, we don't have to generate a new read log entry
|
154
|
+
transaction.verify_attribute_integrity!(@proxy, attribute: method_name)
|
155
|
+
|
156
|
+
# We also may simply return the last accepted read set value
|
157
|
+
transaction.read_attribute_value(@proxy, attribute: method_name).tap do |result|
|
158
|
+
Petra.logger.debug "Re-read attribute: #{method_name} => #{result}", :yellow, :bold
|
159
|
+
end
|
160
|
+
else
|
161
|
+
proxied_object.send(method_name, *args).tap do |val|
|
162
|
+
transaction.log_attribute_read(@proxy, attribute: method_name, value: val, method: method_name)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Handles calls to a method which persists the proxied object.
|
169
|
+
# As we may not actually call the method on the proxied object, we may only
|
170
|
+
# log the persistence.
|
171
|
+
#
|
172
|
+
# This is a very simple behaviour, so it makes sense to handle persistence methods
|
173
|
+
# differently in specialized object proxies (see ActiveRecordProxy)
|
174
|
+
#
|
175
|
+
# TODO: Log parameters given to the persistence method so they can be used during the commit phase
|
176
|
+
#
|
177
|
+
def handle_object_persistence(method_name, *args)
|
178
|
+
transaction.log_object_persistence(@proxy, method: method_name, args: args)
|
179
|
+
# TODO: Find a better return value for pure persistence calls
|
180
|
+
true
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Handles calls to a method which destroys the proxied object
|
185
|
+
#
|
186
|
+
def handle_object_destruction(method_name, *args)
|
187
|
+
transaction.log_object_destruction(@proxy, method: method_name, args: args)
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
#----------------------------------------------------------------
|
192
|
+
# Helpers
|
193
|
+
#----------------------------------------------------------------
|
194
|
+
|
195
|
+
#
|
196
|
+
# Generates a new Proc object from the source code of a given instance method
|
197
|
+
# of the proxied object.
|
198
|
+
#
|
199
|
+
# TODO: This does not work well with #unloadable, e.g. in Rails development environment
|
200
|
+
# TODO: method.parameters returns the required and optional parameters, these could be handed to the proc
|
201
|
+
# TODO: what happens with dynamically generated methods? is there a practical way to achieve this?
|
202
|
+
#
|
203
|
+
def method_source_proc(method_name)
|
204
|
+
method = proxied_object.method(method_name.to_sym)
|
205
|
+
method_source = method.source.lines[1..-2].join
|
206
|
+
proc do
|
207
|
+
@proxy_binding.eval method_source
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|