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,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'petra/proxies/abstract_proxy'
4
+ require 'petra/proxies/method_handlers'
5
+
6
+ module Petra
7
+ module Proxies
8
+ #
9
+ # To avoid messing with the methods defined by ActiveRecord or similar,
10
+ # the programmer should use these proxy objects (object.petra.*) which handle
11
+ # actions on a different level.
12
+ #
13
+ # This class is the base proxy class which can be extended to cover
14
+ # certain behaviours that would be too complex to be put inside the configuration.
15
+ #
16
+ class ObjectProxy < AbstractProxy
17
+ include Comparable
18
+
19
+ CLASS_NAMES = %w[Object].freeze
20
+
21
+ delegate :to_s, to: :proxied_object
22
+
23
+ #
24
+ # Do not create new proxies for already proxied objects.
25
+ # Instead, return the current proxy object
26
+ #
27
+ def petra(*)
28
+ self
29
+ end
30
+
31
+ # Creepy!
32
+ def new(*args)
33
+ class_method!
34
+ proxied_object.new(*args).petra
35
+ end
36
+
37
+ #
38
+ # Access the proxied object publicly from each petra proxy
39
+ # TODO: This should not leave the proxy!
40
+ #
41
+ # @example
42
+ # user = User.petra.first
43
+ # user.unproxied.first_name
44
+ #
45
+ def unproxied
46
+ proxied_object
47
+ end
48
+
49
+ #
50
+ # Checks whether the given attribute was altered during the current transaction.
51
+ # Note that an attribute counts as `altered` even if it was reset to its original
52
+ # value in a later transaction step.
53
+ #
54
+ # @deprecated
55
+ #
56
+ # TODO: Check for dynamic attribute readers?
57
+ #
58
+ def __original_attribute?(attribute_name)
59
+ !transaction.attribute_value?(self, attribute: attribute_name.to_s)
60
+ end
61
+
62
+ #
63
+ # Catch all methods which are not defined on this proxy object as they
64
+ # are most likely meant to go to the proxied object
65
+ #
66
+ # Also checks a few special cases like attribute reads/changes.
67
+ # Please note that a method may be e.g. a persistence method AND an attribute writer
68
+ # (for normal objects, every attribute write would be persisted to memory), so
69
+ # we have to execute all matching handlers in a queue.
70
+ #
71
+ # rubocop:disable Style/MethodMissing
72
+ def method_missing(meth, *args, &block)
73
+ # If no transaction is currently running, we proxy everything
74
+ # to the original object.
75
+ unless Petra.transaction_running?
76
+ Petra.logger.info "No transaction running, proxying #{meth} to original object."
77
+ return unproxied.public_send(meth, *args, &block)
78
+ end
79
+
80
+ # As calling a superclass method in ruby does not cause method calls within this method
81
+ # to be called within the superclass context, the correct (= the child class') attribute
82
+ # detectors are run.
83
+ result = __handlers.execute_missing_queue(meth, *args, block: block) do |queue|
84
+ queue << :handle_attribute_change if __attribute_writer?(meth)
85
+ queue << :handle_attribute_read if __attribute_reader?(meth)
86
+ queue << :handle_dynamic_attribute_read if __dynamic_attribute_reader?(meth)
87
+ queue << :handle_object_persistence if __persistence_method?(meth)
88
+ end
89
+
90
+ Petra.logger.debug "#{object_class_or_self}##{meth}(#{args.map(&:inspect).join(', ')}) => #{result.inspect}"
91
+
92
+ result
93
+ rescue SystemStackError => e
94
+ exception = ArgumentError.new("Method '#{meth}' lead to a SystemStackError due to `method_missing`")
95
+ exception.set_backtrace(e.backtrace.uniq)
96
+ raise exception
97
+ end
98
+ # rubocop:enable Style/MethodMissing
99
+
100
+ #
101
+ # It is necessary to forward #respond_to? queries to
102
+ # the proxied object as otherwise certain calls, especially from
103
+ # the Rails framework itself will fail.
104
+ # Hidden methods are ignored.
105
+ #
106
+ def respond_to_missing?(meth, *)
107
+ proxied_object.respond_to?(meth)
108
+ end
109
+
110
+ #
111
+ # Generates an ID for the proxied object based on the class configuration.
112
+ # New objects (= objects which were generated within this transaction) receive
113
+ # an artificial ID
114
+ #
115
+ def __object_id
116
+ @__object_id ||= if __new?
117
+ transaction.objects.next_id
118
+ else
119
+ object_config(:id_method, proc_expected: true, base: proxied_object)
120
+ end
121
+ end
122
+
123
+ #
124
+ # Generates a unique object key based on the proxied object's class and id
125
+ #
126
+ # @return [String] the generated object key
127
+ #
128
+ def __object_key
129
+ [proxied_object.class, __object_id].map(&:to_s).join('/')
130
+ end
131
+
132
+ #
133
+ # Generates a unique attribute key based on the proxied object's class, id and a given attribute
134
+ #
135
+ # @param [String, Symbol] attribute
136
+ #
137
+ # @return [String] the generated attribute key
138
+ #
139
+ def __attribute_key(attribute)
140
+ [proxied_object.class, __object_id, attribute].map(&:to_s).join('/')
141
+ end
142
+
143
+ #
144
+ # @return [Boolean] +true+ if the proxied object did not exist before the transaction started
145
+ #
146
+ def __new?
147
+ transaction.objects.new?(self)
148
+ end
149
+
150
+ #
151
+ # @return [Boolean] +true+ if the proxied object existed before the transaction started
152
+ #
153
+ def __existing?
154
+ transaction.objects.existing?(self)
155
+ end
156
+
157
+ #
158
+ # @return [Boolean] +true+ if the proxied object was created (= initialized + persisted) during
159
+ # the current transaction
160
+ #
161
+ def __created?
162
+ transaction.objects.created?(self)
163
+ end
164
+
165
+ #
166
+ # @return [Boolean] +true+ if the proxied object was destroyed during the transaction
167
+ #
168
+ def __destroyed?
169
+ transaction.objects.destroyed?(self)
170
+ end
171
+
172
+ #
173
+ # Very simple spaceship operator based on the object key
174
+ # TODO: See if this causes problems when ID-ordering is expected.
175
+ # For existing objects that shouldn't be the case in most situations as
176
+ # a collection mostly contains only objects of one kind
177
+ #
178
+ def <=>(other)
179
+ __object_key <=> other.__object_key
180
+ end
181
+
182
+ protected
183
+
184
+ #----------------------------------------------------------------
185
+ # Method Group Detectors
186
+ #----------------------------------------------------------------
187
+
188
+ #
189
+ # Checks whether the given method name is part of the configured attribute reader
190
+ # methods within the currently proxied class
191
+ #
192
+ def __attribute_reader?(method_name)
193
+ object_config(:attribute_reader?, method_name.to_s)
194
+ end
195
+
196
+ #
197
+ # @see #attribute_reader?
198
+ #
199
+ def __attribute_writer?(method_name)
200
+ object_config(:attribute_writer?, method_name.to_s)
201
+ end
202
+
203
+ #
204
+ # @see #attribute_reader?
205
+ #
206
+ # Currently, classes may not use dynamic attribute readers
207
+ #
208
+ def __dynamic_attribute_reader?(method_name)
209
+ !class_proxy? && object_config(:dynamic_attribute_reader?, method_name.to_s)
210
+ end
211
+
212
+ #
213
+ # @return [Boolean] +true+ if the given method would persist the
214
+ # proxied object
215
+ #
216
+ def __persistence_method?(method_name)
217
+ !class_proxy? && object_config(:persistence_method?, method_name.to_s)
218
+ end
219
+
220
+ #
221
+ # @return [Boolean] +true+ if the given method name is a "destructor" of the
222
+ # proxied object
223
+ #
224
+ def __destruction_method?(method_name)
225
+ !class_proxy? && object_config(:destruction_method?, method_name.to_s)
226
+ end
227
+
228
+ #
229
+ # Sets the given attribute to the given value using the default setter
230
+ # function `name=`. This function is just a convenience method and does not
231
+ # manage the actual write set. Please take a look at #handle_attribute_change instead.
232
+ #
233
+ # @param [String, Symbol] attribute
234
+ # The attribute name. The proxied object is expected to have a corresponding public setter method
235
+ #
236
+ # @param [Object] new_value
237
+ #
238
+ def __set_attribute(attribute, new_value)
239
+ public_send("#{attribute}=", new_value)
240
+ end
241
+
242
+ def __handlers
243
+ @__handlers ||= Petra::Proxies::MethodHandlers.new(self, binding)
244
+ end
245
+
246
+ def initialize(object, inherited = false, object_id: nil)
247
+ @obj = object
248
+ @inherited = inherited
249
+ @__object_id = object_id
250
+ end
251
+
252
+ #
253
+ # @return [Object] the proxied object
254
+ #
255
+ def proxied_object
256
+ @obj
257
+ end
258
+
259
+ #
260
+ # @return [Boolean] +true+ if the proxied object is a class
261
+ #
262
+ def class_proxy?
263
+ proxied_object.is_a?(Class)
264
+ end
265
+
266
+ #
267
+ # @return [Class] the proxied object if it is a class itself, otherwise
268
+ # the proxied object's class.
269
+ #
270
+ def object_class_or_self
271
+ class_proxy? ? proxied_object : proxied_object.class
272
+ end
273
+
274
+ #
275
+ # @return [Boolean] +true+ if the proxied object is a +klass+
276
+ #
277
+ def for_class?(klass)
278
+ proxied_object.is_a?(klass)
279
+ end
280
+
281
+ #
282
+ # Performs possible type casts on a value which is about to be set
283
+ # for an attribute. For general ObjectProxy instances, this is simply the identity
284
+ # function, but it might be overridden in more specialized proxies.
285
+ #
286
+ def __type_cast_attribute_value(_attribute, value)
287
+ value
288
+ end
289
+
290
+ #
291
+ # Raises an exception if proxied object isn't a class.
292
+ # Currently, there is no way to specify which methods are class and instance methods
293
+ # in a specialized proxy, so this at least tells the developer that he did something wrong.
294
+ #
295
+ def class_method!
296
+ return if class_proxy?
297
+ fail Petra::PetraError, 'This method is meant to be used as a singleton method, not an instance method.'
298
+ end
299
+
300
+ #
301
+ # @see #class_method!
302
+ #
303
+ def instance_method!
304
+ return if class_proxy?
305
+ fail Petra::PetraError, 'This method is meant to be used as an instance method only!'
306
+ end
307
+
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Util
5
+ module Debug
6
+ STRING_COLORS = {light_gray: 90,
7
+ yellow: 33,
8
+ green: 32,
9
+ red: 31,
10
+ purple: 35,
11
+ cyan: 36,
12
+ blue: 34}.freeze
13
+
14
+ FORMATS = {default: 0,
15
+ bold: 1,
16
+ underline: 4}.freeze
17
+
18
+ %i[debug info warn error].each do |level|
19
+ define_method level do |message, color = :light_gray, format = :default|
20
+ log(message, level: level, color: color, format: format)
21
+ end
22
+
23
+ module_function level
24
+ end
25
+
26
+ def log(message, level: :debug, color: :light_gray, format: :default)
27
+ logger.send(level, 'Petra :: ' + colored_string(message, color, format))
28
+ end
29
+
30
+ private
31
+
32
+ def logger
33
+ @logger ||= Logger.new(STDOUT).tap do |l|
34
+ l.level = "Logger::#{Petra.configuration.log_level.upcase}".constantize
35
+ end
36
+ end
37
+
38
+ def colored_string(string, color, format)
39
+ "\e[#{Petra::Util::Debug::FORMATS[format]};#{Petra::Util::Debug::STRING_COLORS[color.to_sym]}m#{string}\e[0m"
40
+ end
41
+
42
+ module_function :log, :logger, :colored_string
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Util
5
+ module ExtendedAttributeAccessors
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ # TODO: Keep track of available accessors on class level
14
+ # TODO: Keep track of the actual values on instance (or class instance) level
15
+
16
+ def extended_attr_accessor(name, **options)
17
+ singleton = options.fetch(:singleton, false)
18
+ methods_method, definer_method = :instance_methods, :define_method
19
+ methods_method, definer_method = :singleton_methods, :define_singleton_method if singleton
20
+
21
+ unless send(methods_method).include?(:extended_attribute_accessors)
22
+ send(definer_method, :__extended_attribute_accessors__) do |group = :general|
23
+ (@extended_attribute_accessors ||= {})[group.to_sym] ||= {}
24
+ end
25
+
26
+ send(definer_method, :extended_attribute_accessors) do |group = :general, only: nil|
27
+ result = __extended_attribute_accessors__(group)
28
+ return result unless only
29
+
30
+ result.each_with_object({}) do |(k, v), h|
31
+ h[k] = v.slice(Array(only).map(:to_sym))
32
+ end
33
+ end
34
+ end
35
+
36
+ group = options.fetch(:group, :general)
37
+
38
+ send(definer_method, name) do
39
+ accessor = extended_attribute_accessors(group)[name.to_sym] || {}
40
+ accessor[:value] || accessor[:default]
41
+ end
42
+
43
+ define_method("#{name}=") do |value|
44
+ self[name] = value
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Util
5
+ module FieldAccessors
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def field_accessor(name)
12
+ define_method(name) do
13
+ self[name]
14
+ end
15
+
16
+ define_method("#{name}=") do |value|
17
+ self[name] = value
18
+ end
19
+ end
20
+ end
21
+
22
+ def fields
23
+ @fields ||= {}
24
+ end
25
+
26
+ def [](key)
27
+ fields[key.to_s]
28
+ end
29
+
30
+ def []=(key, value)
31
+ fields[key.to_s] = value
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Util
5
+ #
6
+ # Helper module to add register functionality to a class
7
+ # This means that other classes may register themselves under a certain name
8
+ #
9
+ module Registrable
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ #
16
+ # Generates helper methods from the given name.
17
+ #
18
+ # @example Type register
19
+ # acts_as_register(:type)
20
+ # => registered_types
21
+ # => register_type(type)
22
+ # => registered_type(type) #=> value
23
+ # => registered_type?(type) #=> true/false
24
+ #
25
+ def acts_as_register(name)
26
+ name = name.to_s
27
+
28
+ define_singleton_method("registered_#{name.pluralize}") do
29
+ @registered_components ||= {}
30
+ @registered_components[name.to_s] ||= {}
31
+ end
32
+
33
+ define_singleton_method("registered_#{name}") do |key|
34
+ send("registered_#{name.pluralize}")[key.to_s]
35
+ end
36
+
37
+ define_singleton_method("register_#{name}") do |key, value|
38
+ send("registered_#{name.pluralize}")[key.to_s] = value
39
+ end
40
+
41
+ define_singleton_method("registered_#{name}?") do |key|
42
+ send("registered_#{name.pluralize}").key?(key.to_s)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Util
5
+ module TestHelpers
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ VERSION = '0.0.1'
5
+ end
data/lib/petra.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'method_source'
5
+ require 'pathname'
6
+
7
+ require 'petra/core_ext'
8
+ require 'petra/exceptions'
9
+ require 'petra/configuration/base'
10
+ require 'petra/configuration/class_configurator'
11
+ require 'petra/util/debug'
12
+ require 'petra/persistence_adapters/file_adapter'
13
+
14
+ require 'petra/proxies/enumerable_proxy'
15
+ require 'petra/proxies/object_proxy'
16
+
17
+ require 'petra/components/transaction_manager'
18
+
19
+ require 'petra/components/entries/attribute_change'
20
+ require 'petra/components/entries/attribute_change_veto'
21
+ require 'petra/components/entries/attribute_read'
22
+ require 'petra/components/entries/object_destruction'
23
+ require 'petra/components/entries/object_initialization'
24
+ require 'petra/components/entries/object_persistence'
25
+ require 'petra/components/entries/read_integrity_override'
26
+
27
+ require 'forwardable'
28
+
29
+ module Petra
30
+ extend SingleForwardable
31
+
32
+ def self.root
33
+ Pathname.new(File.dirname(__FILE__))
34
+ end
35
+
36
+ #
37
+ # @return [Petra::Configuration::Base] petra's configuration instance
38
+ #
39
+ def self.configuration
40
+ @configuration ||= Petra::Configuration::Base.new
41
+ end
42
+
43
+ #
44
+ # Executes the given block in the context of petra's configuration instance
45
+ #
46
+ def self.configure(&proc)
47
+ configuration.instance_eval(&proc) if block_given?
48
+ end
49
+
50
+ #
51
+ # Forward transaction handling to the TransactionManager class
52
+ #
53
+ # @see Petra::Components::TransactionManager#with_transaction
54
+ #
55
+ def self.transaction(identifier: nil, &block)
56
+ Petra::Components::TransactionManager.with_transaction(identifier: identifier || SecureRandom.uuid, &block)
57
+ end
58
+
59
+ #
60
+ # @return [Boolean] +true+ if a transaction is currently running
61
+ #
62
+ def self.transaction_running?
63
+ Petra::Components::TransactionManager.instance?
64
+ end
65
+
66
+ #
67
+ # Attempts to commit the currently active transaction
68
+ #
69
+ def self.commit!
70
+ transaction_manager.commit_transaction
71
+ end
72
+
73
+ #
74
+ # @return [Petra::Components::TransactionManager, NilClass]
75
+ #
76
+ def self.transaction_manager
77
+ Petra::Components::TransactionManager.instance
78
+ end
79
+
80
+ def_delegator :transaction_manager, :current_transaction
81
+
82
+ #
83
+ # Logs the given +message+
84
+ #
85
+ def self.logger
86
+ Petra::Util::Debug
87
+ end
88
+
89
+ def self.rails?
90
+ defined?(Rails)
91
+ end
92
+ end
93
+
94
+ # Extend the Object class to add the `petra` proxy generator
95
+ Object.class_eval do
96
+ include Petra::CoreExt::Object
97
+ end
98
+
99
+ # Register Persistence Adapters
100
+ Petra::PersistenceAdapters::Adapter.register_adapter(:file, Petra::PersistenceAdapters::FileAdapter)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :petra do
4
+ # # Task goes here
5
+ # end
data/petra.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'petra/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'petra_core'
9
+ spec.version = Petra::VERSION
10
+ spec.authors = ['Stefan Exner']
11
+ spec.email = ['stex@sterex.de']
12
+
13
+ spec.summary = 'Proof-Of-Concept for temporarily persisted transactions in Ruby'
14
+ spec.homepage = 'https://github.com/stex/petra'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.required_ruby_version = '~> 2.5'
26
+
27
+ spec.add_dependency 'activesupport', '~> 4.2'
28
+ spec.add_dependency 'method_source', '~> 0.9.0'
29
+
30
+ spec.add_development_dependency 'bundler', '~> 1.16'
31
+ spec.add_development_dependency 'faker', '~> 1.8'
32
+ spec.add_development_dependency 'pry', '~> 0.11'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ spec.add_development_dependency 'rubocop', '~> 0.53.0'
36
+ end