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,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