petra_core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|