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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +83 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +74 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +726 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +8 -0
  14. data/bin/setup +8 -0
  15. data/examples/continuation_error.rb +125 -0
  16. data/examples/dining_philosophers.rb +138 -0
  17. data/examples/showcase.rb +54 -0
  18. data/lib/petra/components/entries/attribute_change.rb +29 -0
  19. data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
  20. data/lib/petra/components/entries/attribute_read.rb +20 -0
  21. data/lib/petra/components/entries/object_destruction.rb +22 -0
  22. data/lib/petra/components/entries/object_initialization.rb +19 -0
  23. data/lib/petra/components/entries/object_persistence.rb +26 -0
  24. data/lib/petra/components/entries/read_integrity_override.rb +42 -0
  25. data/lib/petra/components/entry_set.rb +87 -0
  26. data/lib/petra/components/log_entry.rb +342 -0
  27. data/lib/petra/components/proxy_cache.rb +209 -0
  28. data/lib/petra/components/section.rb +543 -0
  29. data/lib/petra/components/transaction.rb +405 -0
  30. data/lib/petra/components/transaction_manager.rb +214 -0
  31. data/lib/petra/configuration/base.rb +132 -0
  32. data/lib/petra/configuration/class_configurator.rb +309 -0
  33. data/lib/petra/configuration/configurator.rb +67 -0
  34. data/lib/petra/core_ext.rb +27 -0
  35. data/lib/petra/exceptions.rb +181 -0
  36. data/lib/petra/persistence_adapters/adapter.rb +154 -0
  37. data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
  38. data/lib/petra/proxies/abstract_proxy.rb +149 -0
  39. data/lib/petra/proxies/enumerable_proxy.rb +44 -0
  40. data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
  41. data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
  42. data/lib/petra/proxies/method_handlers.rb +213 -0
  43. data/lib/petra/proxies/module_proxy.rb +12 -0
  44. data/lib/petra/proxies/object_proxy.rb +310 -0
  45. data/lib/petra/util/debug.rb +45 -0
  46. data/lib/petra/util/extended_attribute_accessors.rb +51 -0
  47. data/lib/petra/util/field_accessors.rb +35 -0
  48. data/lib/petra/util/registrable.rb +48 -0
  49. data/lib/petra/util/test_helpers.rb +9 -0
  50. data/lib/petra/version.rb +5 -0
  51. data/lib/petra.rb +100 -0
  52. data/lib/tasks/petra_tasks.rake +5 -0
  53. data/petra.gemspec +36 -0
  54. 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Proxies
5
+ #
6
+ # Helper to mark a module inside ::Proxies as ModuleProxy.
7
+ # Each ModuleProxy has to include this module.
8
+ #
9
+ module ModuleProxy
10
+ end
11
+ end
12
+ end