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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Configuration
5
+ class Base
6
+
7
+ DEFAULTS = {
8
+ persistence_adapter_name: 'file',
9
+ log_level: 'debug',
10
+ instant_read_integrity_fail: true
11
+ }.freeze
12
+
13
+ #----------------------------------------------------------------
14
+ # Configuration Keys
15
+ #----------------------------------------------------------------
16
+
17
+ #
18
+ # Configures whether a read integrity error will be automatically
19
+ # detected whenever an attribute is read.
20
+ # If this is set to +false+, the read values will only be checked during
21
+ # the commit phase.
22
+ #
23
+ def instant_read_integrity_fail(new_value = nil)
24
+ if !new_value.nil?
25
+ __configuration_hash[:instant_read_integrity_fail] = new_value
26
+ else
27
+ __config_or_default(:instant_read_integrity_fail)
28
+ end
29
+ end
30
+
31
+ alias instantly_fail_on_read_integrity_errors instant_read_integrity_fail
32
+
33
+ #
34
+ # Sets the adapter to be used as transaction persistence adapter.
35
+ # An adapter has to be registered before it may be used (see Adapter)
36
+ #
37
+ # @return [Class] the persistence adapter class used for storing transaction values.
38
+ # Defaults to use to the cache adapter
39
+ #
40
+ def persistence_adapter(name = nil)
41
+ if name
42
+ unless Petra::PersistenceAdapters::Adapter.registered_adapter?(name)
43
+ fail Petra::ConfigurationError,
44
+ "The given adapter `#{name}` hasn't been registered. " \
45
+ "Valid adapters are: #{Petra::PersistenceAdapters::Adapter.registered_adapters.keys.inspect}"
46
+ end
47
+ __configuration_hash[:persistence_adapter_name] = name
48
+ else
49
+ Petra::PersistenceAdapters::Adapter[__config_or_default(:persistence_adapter_name)]
50
+ end
51
+ end
52
+
53
+ #
54
+ # The log level for petra. Only messages which are greater or equal to this level
55
+ # will be shown in the output
56
+ #
57
+ def log_level(new_value = nil)
58
+ if new_value
59
+ __configuration_hash[:log_level] = new_value.to_s
60
+ else
61
+ __config_or_default(:log_level).to_sym
62
+ end
63
+ end
64
+
65
+ #----------------------------------------------------------------
66
+ # Helper Methods
67
+ #----------------------------------------------------------------
68
+
69
+ #
70
+ # A shortcut method to set +proxy_instances+ for multiple classes at once
71
+ # without having to +configure_class+ for each one.
72
+ #
73
+ # @example
74
+ # proxy_class_instances 'Array', 'Enumerator', Hash
75
+ #
76
+ def proxy_class_instances(*class_names)
77
+ class_names.each do |klass|
78
+ configure_class(klass) do
79
+ proxy_instances true
80
+ end
81
+ end
82
+ end
83
+
84
+ #
85
+ # Executes the given block in the context of a ClassConfigurator to
86
+ # configure petra's behaviour for a certain model/class
87
+ #
88
+ def configure_class(class_name, &proc)
89
+ configurator = class_configurator(class_name)
90
+ configurator.instance_eval(&proc)
91
+ configurator.__persist!
92
+ end
93
+
94
+ #
95
+ # Builds a ClassConfigurator for the given class or class name.
96
+ #
97
+ # @example Request the configuration for a certain model
98
+ # Notifications::Notificator.configuration.model_configurator(Subscription).__value(:recipients)
99
+ #
100
+ def class_configurator(class_name)
101
+ ClassConfigurator.for_class(class_name)
102
+ end
103
+
104
+ alias [] class_configurator
105
+
106
+ #
107
+ # @return [Hash] the complete configuration or one of its sub-namespaces.
108
+ # If a namespace does not exists yet, it will be initialized with an empty hash
109
+ #
110
+ # @example Retrieve the {:something => {:completely => {:different => 5}}} namespace
111
+ # __configuration_hash(:something, :completely)
112
+ # #=> {:different => 5}
113
+ #
114
+ def __configuration_hash(*sub_keys)
115
+ sub_keys.inject(@configuration ||= {}) do |h, k|
116
+ h[k] ||= {}
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ #
123
+ # @return [Object] a base configuration value (non-namespaced) or
124
+ # the default value (see DEFAULTS) if none was set yet.
125
+ #
126
+ def __config_or_default(name)
127
+ __configuration_hash.fetch(name.to_sym, DEFAULTS[name.to_sym])
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'petra/configuration/configurator'
4
+
5
+ module Petra
6
+ module Configuration
7
+ class ClassConfigurator < Configurator
8
+
9
+ DEFAULTS = {
10
+ proxy_instances: false,
11
+ mixin_module_proxies: true,
12
+ use_specialized_proxy: true,
13
+ id_method: :object_id,
14
+ lookup_method: ->(id) { ObjectSpace._id2ref(id.to_i) },
15
+ init_method: :new,
16
+ attribute_reader?: false,
17
+ attribute_writer?: ->(name) { /=$/.match(name) },
18
+ dynamic_attribute_reader?: false,
19
+ persistence_method?: false,
20
+ destruction_method?: false
21
+ }.freeze
22
+
23
+ #
24
+ # @param [String] class_name
25
+ # The name of the class to be configured.
26
+ # .new() should not be called manually, use #for_class instead
27
+ # which accepts different input types.
28
+ #
29
+ def initialize(class_name)
30
+ @class_name = class_name
31
+ super()
32
+ end
33
+
34
+ #----------------------------------------------------------------
35
+ # Configuration Keys
36
+ #----------------------------------------------------------------
37
+
38
+ # TODO: LIST:
39
+ # - Rails' routes like configurators for method handlers:
40
+ # - Allow attribute writers without parameters. These will need a value the
41
+ # write set entry is set to. An example here would be `lock!` which sets `locked = true` internally
42
+ # - Methods should get an option which will set their generated log entries
43
+ # to not be executed. This is necessary if e.g. a setter method is also a persistence method
44
+ # and would re-set the attribute - fine in many places, but think about a mutex.
45
+ # example:
46
+ # attribute_writers do
47
+ # lock! :locked => true
48
+ # - Usage of method_missing with (**options)?
49
+ #
50
+ # Sets whether instances of this class should be wrapped in an ObjectProxy
51
+ # if this is not directly done by the programmer, e.g. as return value
52
+ # from an already proxied object.
53
+ #
54
+ base_config :proxy_instances
55
+
56
+ #
57
+ # Sets whether ObjectProxies should be extended with possibly existing
58
+ # ModuleProxy modules. This is used mainly for +Enumerable+, but you may want
59
+ # to define your own helper modules.
60
+ #
61
+ base_config :mixin_module_proxies
62
+
63
+ #
64
+ # Some classes have specialized proxy classes.
65
+ # If this setting is set to +false+, they will not be used in favour of ObjectProxy
66
+ #
67
+ base_config :use_specialized_proxy
68
+
69
+ #
70
+ # Sets the method to be used to determine the unique ID of an Object.
71
+ # The ID is needed to identify an object when reloading it within a transaction,
72
+ # so basically a key for our read set.
73
+ #
74
+ # If a block is given, the (:base) object is yielded to it, otherwise,
75
+ # the given method name is assumed to be an instance method in the configured class
76
+ #
77
+ base_config :id_method
78
+
79
+ #
80
+ # Sets the method to be used to load an object with a certain unique ID
81
+ # (see +:id_method+).
82
+ #
83
+ # If a block is given, the identifier is yielded to it, otherwise,
84
+ # the given method name is assumed to be a class method accepting
85
+ # a string identifier in the configured class
86
+ #
87
+ base_config :lookup_method
88
+
89
+ #
90
+ # Method to initialize a new instance of the proxied class, e.g. `:new` for basic objects
91
+ #
92
+ base_config :init_method
93
+
94
+ #
95
+ # Expects the value (or return value of a block) to be a boolean value
96
+ # depending on whether a method name given as argument is an attribute reader
97
+ #
98
+ base_config :attribute_reader?
99
+
100
+ #
101
+ # Expects the value (or return value of a block) to be a boolean value
102
+ # depending on whether a method name given as argument is an attribute reader
103
+ #
104
+ base_config :attribute_writer?
105
+
106
+ #
107
+ # Sometimes it might be necessary to use helper methods to combine multiple attributes,
108
+ # e.g. `#name` for `"#{first_name} #{last_name}"`.
109
+ # As calling `#name` would usually be passed to the proxied objects and
110
+ # executed within the object's context instead of the proxy, these methods
111
+ # can be flagged as combined/dynamic attribute readers and will be executed within
112
+ # the proxy's binding.
113
+ # The function is expected to return a boolean value.
114
+ #
115
+ base_config :dynamic_attribute_reader?
116
+
117
+ #
118
+ # Expects the value (or return value of a block) to be a boolean value
119
+ # depending on whether a method name given as argument is a method that will persist
120
+ # the current instance.
121
+ # For normal ruby objects this would be every attribute setter (as it would be persisted in
122
+ # the process memory), for e.g. ActiveRecord::Base instances, this is only done by update/save/...
123
+ #
124
+ base_config :persistence_method?
125
+
126
+ #
127
+ # Expects the value (or return value of a block) to be a boolean value and
128
+ # be +true+ if the given method is a "destructor" of the configured class.
129
+ # This can't be easily said for plain ruby objects, but some classes
130
+ # may implement an own destruction behaviour (e.g. ActiveRecord)
131
+ #
132
+ base_config :destruction_method?
133
+
134
+ #----------------------------------------------------------------
135
+ # Helper Methods
136
+ #----------------------------------------------------------------
137
+
138
+ #
139
+ # Builds a new instance for the given class name.
140
+ # If a configuration for this class already exists, it is loaded and
141
+ # can be retrieved through the corresponding getter methods
142
+ #
143
+ # @param [String, Symbol, Class] klass
144
+ # The class (name) which will be used to initialize the configurator
145
+ # and load a possibly already existing configuration
146
+ #
147
+ def self.for_class(klass)
148
+ new(klass.to_s)
149
+ end
150
+
151
+ #
152
+ # Returns the value for a certain configuration key.
153
+ # If the configuration value is a proc, it will be called
154
+ # with the given +*args+.
155
+ #
156
+ # If no custom configuration was set for the given +name+, the default
157
+ # value is returned instead.
158
+ #
159
+ # @param [Boolean] proc_expected
160
+ # If set to +true+, the value is expected to be either a Proc object
161
+ # or a String/Symbol which is assumed to be a method name.
162
+ # If the value is something else, an Exception is thrown
163
+ #
164
+ # @param [Object] base
165
+ # The base object which is used in case +mandatory_proc+ is set to +true+.
166
+ # If the fetched value is a String or Symbol, it will be used as method
167
+ # name in a call based on the +base+ object with +*args* as actual parameters, e.g.
168
+ # base.send(:some_fetched_value, *args)
169
+ #
170
+ def __value(key, *args, proc_expected: false, base: nil)
171
+ v = __configuration.fetch(key.to_sym, DEFAULTS[key.to_sym])
172
+
173
+ # As the setting blocks are saved as Proc objects (which are run
174
+ # in their textual scope) and not lambdas (which are run in their caller's scope),
175
+ # Ruby does not allow using the `return` keyword while being inside the
176
+ # block as method the proc was defined in might have already been returned.
177
+ #
178
+ # When configuring petra using blocks, it is advised to use `next`
179
+ # instead of `return` (which will jump back to the correct position),
180
+ # a workaround is to rescue from possible LocalJumpErrors and simply
181
+ # use their exit value.
182
+ begin
183
+ case v
184
+ when Proc
185
+ # see #__send_to_base
186
+ return v.call(*[*args, base][0, v.arity]) if proc_expected
187
+ v.call(*(args[0, v.arity]))
188
+ when String, Symbol
189
+ return __send_to_base(base, method: v, args: args, key: key) if proc_expected
190
+ v
191
+ else
192
+ __fail_for_key key, 'Value has to be either a Proc or a method name (Symbol/String)' if proc_expected
193
+ v
194
+ end
195
+ rescue LocalJumpError => e
196
+ e.exit_value
197
+ end
198
+ end
199
+
200
+ #
201
+ # Tests whether this class configuration has a custom setting for the given key.
202
+ #
203
+ # @return [TrueClass, FalseClass] +true+ if there is a custom setting
204
+ #
205
+ def __value?(key)
206
+ __configuration.key?(key.to_sym)
207
+ end
208
+
209
+ #
210
+ # Much like #__value, but it searches for settings
211
+ # with the given name in the current class' ancestors if
212
+ # itself does not have a custom value set.
213
+ #
214
+ def __inherited_value(key, *args)
215
+ configurator = self
216
+
217
+ # Search for a custom configuration in the current class and its superclasses
218
+ # until we either reach Object (the lowest level ignoring BasicObject) or
219
+ # found a custom setting.
220
+ until (klass = configurator.send(:configured_class)) == Object || configurator.__value?(key)
221
+ configurator = Petra.configuration.class_configurator(klass.superclass)
222
+ end
223
+
224
+ # By now, we have either reached the Object level or found a value.
225
+ # In either case, we are save to retrieve it.
226
+ configurator.__value(key, *args)
227
+ end
228
+
229
+ private
230
+
231
+ #
232
+ # Raises a Petra::ConfigurationError with information about the key that caused it and a message
233
+ #
234
+ def __fail_for_key(key, message)
235
+ fail Petra::ConfigurationError,
236
+ "The configuration '#{key}' for class '#{@class_name}' seems to be incorrect: #{message}"
237
+ end
238
+
239
+ #
240
+ # Tries to .send() the given +method+ to the +base+ object.
241
+ # Exceptions are raised when no base was given or the given base does not respond to the given method.
242
+ #
243
+ def __send_to_base(base, method:, key:, args: [])
244
+ fail ArgumentError, "No base object to send ':#{method}' to was given" unless base
245
+
246
+ unless base.respond_to?(method.to_sym)
247
+ if base.is_a?(Class)
248
+ __fail_for_key key, ":#{method} was expected to be a class method in #{base}"
249
+ else
250
+ __fail_for_key key, ":#{method} was expected to be an instance method in #{base.class}"
251
+ end
252
+ end
253
+
254
+ # It might happen that the given method name does not accept all of the given
255
+ # arguments, most likely because they are not needed to make the necessary
256
+ # decisions anyway.
257
+ # Therefore, only the correct amount of arguments is passed to the function, e.g.
258
+ # args[0,2] for a method with arity 2
259
+ base.send(method.to_sym, *__args_for_arity(base, method, args))
260
+ end
261
+
262
+ #
263
+ # Takes as many elements from +args+ as the given method accepts
264
+ # If a method with variable arguments is given (def something(*args)),
265
+ # all arguments are returned
266
+ #
267
+ def __args_for_arity(base, method, args)
268
+ arity = base.method(method.to_sym).arity
269
+ arity >= 0 ? args[0, arity] : args
270
+ end
271
+
272
+ #
273
+ # @return [Class] the class which is configured by this ClassConfigurator
274
+ #
275
+ # Even though ruby class should only contain module separators (::) and camel case words,
276
+ # there might be (framework) class names which do not comply to this.
277
+ # An example would be ActiveRecord's Relation class which seems to be specific
278
+ # for each model class it is used on.
279
+ #
280
+ # Example: User.all #=> <User::ActiveRecord_Relation...>
281
+ #
282
+ # Therefore, we first try to camelize the given class name and if that
283
+ # does not lead us to a valid constant name, we try to pass in the
284
+ # @class_name as is and raise possible errors.
285
+ #
286
+ def configured_class
287
+ @class_name.camelize.safe_constantize || @class_name.constantize
288
+ end
289
+
290
+ #
291
+ # @return [Array<Symbol, String>] the namespaces which will be used
292
+ # when merging this class configuration into the main configuration hash
293
+ #
294
+ def __namespaces
295
+ [:models, @class_name]
296
+ end
297
+
298
+ #
299
+ # Removes options from the given arguments if the last element is a Hash
300
+ #
301
+ # @return [Hash] the options extracted from the given arguments or an empty
302
+ # hash if there were no options given
303
+ #
304
+ def extract_options!(args)
305
+ args.last.is_a?(Hash) ? args.pop : {}
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Configuration
5
+ class Configurator
6
+
7
+ #
8
+ # Generates a very basic configuration method which accepts a
9
+ # block or input value (see +options+)
10
+ #
11
+ # @param [Object] name
12
+ # The configuration's name which will become the method name
13
+ #
14
+ # @param [Boolean] accept_value
15
+ # If set to +true+, the resulting method accepts not only a block,
16
+ # but also a direct value.
17
+ # If both, a value and a block are given, the block takes precedence
18
+ #
19
+ def self.base_config(name, accept_value: true)
20
+ if accept_value
21
+ define_method name do |value = nil, &proc|
22
+ if proc
23
+ __configuration[name.to_sym] = proc
24
+ elsif !value.nil?
25
+ __configuration[name.to_sym] = value
26
+ else
27
+ fail ArgumentError, 'Either a value or a configuration block have to be given.'
28
+ end
29
+ end
30
+ else
31
+ define_method name do |&proc|
32
+ fail(ArgumentError, 'A configuration block has to be given.') unless proc
33
+ __configuration[name.to_sym] = proc
34
+ end
35
+ end
36
+ end
37
+
38
+ def initialize
39
+ @options = Petra.configuration.__configuration_hash(*__namespaces).deep_dup
40
+ end
41
+
42
+ #
43
+ # Persists the new configuration values in the global configuration,
44
+ # meaning that it merges its options into the specific configuration hash
45
+ # under a certain key
46
+ #
47
+ def __persist!
48
+ Petra.configuration.__configuration_hash(*__namespaces).deep_merge!(__configuration)
49
+ end
50
+
51
+ protected
52
+
53
+ #
54
+ # @return [Array<Symbol>] the current configuration options within an
55
+ # optional namespace chain, mainly to be merged into a global
56
+ #
57
+ def __namespaces
58
+ not_implemented
59
+ end
60
+
61
+ def __configuration
62
+ @options
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module CoreExt
5
+ module Object
6
+ #
7
+ # @return [Petra::ObjectProxy, Object] A proxy object to be used instead of the
8
+ # actual object in the transactions' contexts.
9
+ #
10
+ # Some objects are frozen by default (e.g. +nil+ or the shared instances of TrueClass and FalseClass),
11
+ # for these, the resulting object proxy is not cached
12
+ #
13
+ def petra(inherited: false, configuration_args: [])
14
+ # Do not proxy inherited objects if their configuration prohibits it.
15
+ if inherited && !Petra::Proxies::ObjectProxy.inherited_config_for(self, :proxy_instances, *configuration_args)
16
+ return self
17
+ end
18
+
19
+ if frozen?
20
+ Petra::Proxies::ObjectProxy.for(self, inherited: inherited)
21
+ else
22
+ @__petra_proxy ||= Petra::Proxies::ObjectProxy.for(self, inherited: inherited)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end