petra_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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