dry-transaction 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +20 -14
  4. data/lib/dry/transaction/builder.rb +2 -4
  5. data/lib/dry/transaction/callable.rb +38 -0
  6. data/lib/dry/transaction/errors.rb +27 -0
  7. data/lib/dry/transaction/instance_methods.rb +22 -5
  8. data/lib/dry/transaction/operation.rb +4 -4
  9. data/lib/dry/transaction/operation_resolver.rb +5 -1
  10. data/lib/dry/transaction/result_matcher.rb +3 -3
  11. data/lib/dry/transaction/stack.rb +24 -0
  12. data/lib/dry/transaction/step.rb +37 -21
  13. data/lib/dry/transaction/step_adapter.rb +49 -0
  14. data/lib/dry/transaction/step_adapters/around.rb +25 -0
  15. data/lib/dry/transaction/step_adapters/check.rb +18 -0
  16. data/lib/dry/transaction/step_adapters/map.rb +3 -3
  17. data/lib/dry/transaction/step_adapters/raw.rb +6 -12
  18. data/lib/dry/transaction/step_adapters/tee.rb +4 -4
  19. data/lib/dry/transaction/step_adapters/try.rb +11 -8
  20. data/lib/dry/transaction/step_adapters.rb +2 -0
  21. data/lib/dry/transaction/version.rb +1 -1
  22. data/lib/dry/transaction.rb +14 -11
  23. data/spec/examples.txt +81 -65
  24. data/spec/integration/around_spec.rb +81 -0
  25. data/spec/integration/custom_step_adapters_spec.rb +6 -4
  26. data/spec/integration/operation_spec.rb +3 -3
  27. data/spec/integration/passing_step_arguments_spec.rb +1 -1
  28. data/spec/integration/publishing_step_events_spec.rb +36 -17
  29. data/spec/integration/transaction_spec.rb +165 -37
  30. data/spec/integration/transaction_without_steps_spec.rb +101 -0
  31. data/spec/spec_helper.rb +14 -5
  32. data/spec/support/container.rb +10 -0
  33. data/spec/support/database.rb +12 -0
  34. data/spec/support/db_transactions.rb +45 -0
  35. data/spec/support/result_mixin.rb +3 -0
  36. data/spec/unit/step_adapters/around_spec.rb +46 -0
  37. data/spec/unit/step_adapters/check_spec.rb +43 -0
  38. data/spec/unit/step_adapters/map_spec.rb +5 -12
  39. data/spec/unit/step_adapters/raw_spec.rb +16 -32
  40. data/spec/unit/step_adapters/tee_spec.rb +4 -10
  41. data/spec/unit/step_adapters/try_spec.rb +24 -33
  42. data/spec/unit/step_spec.rb +41 -10
  43. metadata +39 -21
  44. data/spec/support/either_mixin.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 679662590f13d07a84ae193679d65f448ddb9e54
4
- data.tar.gz: 5810baddf36d0bfd0691acdcb663c5b866f04576
2
+ SHA256:
3
+ metadata.gz: 6e276d629eb7ee813600d0cfce597d0857370b5ee44472de1495faa16f7db44f
4
+ data.tar.gz: 240ce1181835f188c975415c947b2b10f8f45de45d2ea6e0293af360d680796b
5
5
  SHA512:
6
- metadata.gz: 57726a7d740fa0e5983690fe21956303d2f4db5a62ba16f2c8aa334c7dd40b92aefccdc3e13df2f568eed5cf2ee7b4707d0ad78a771c20c89652f29f3fea1049
7
- data.tar.gz: 3c9de9200a701e364c397279c3a277c5f575b725afa62c87b7102f6c31ae747b9b3f6f93b5798a99bd026062bb932aeac8ea78925d8147b678897fe0f8cd90b1
6
+ metadata.gz: b6d04f7008727c783260df83e97221a5ebecc9b703da004d860bc040ed5334869d4844d2977cc5575e7a59ebd4a5ed95a6745aaa50981f7fb97aa777511265c2
7
+ data.tar.gz: b242f6281051877a0bc85f962e8655f3c92439388fa5790ad793ad34afbaa15a628f6f0aa8bc3484d250dd8be515232d18accca218a39a51a88d3c86d1f4bfe2
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ gemspec
5
5
  group :test do
6
6
  gem "simplecov"
7
7
  gem "codeclimate-test-reporter"
8
- gem "byebug", platform: :mri
8
+ gem "pry-byebug", platform: :mri
9
9
  gem "dry-container"
10
10
  end
11
11
 
data/Gemfile.lock CHANGED
@@ -1,17 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dry-transaction (0.10.2)
4
+ dry-transaction (0.11.0)
5
5
  dry-container (>= 0.2.8)
6
- dry-matcher (>= 0.5.0)
7
- dry-monads (>= 0.0.1)
8
- wisper (>= 1.6.0)
6
+ dry-events (>= 0.1.0)
7
+ dry-matcher (>= 0.7.0)
8
+ dry-monads (>= 0.4.0)
9
9
 
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
13
  ast (2.3.0)
14
- byebug (8.2.2)
14
+ byebug (9.1.0)
15
15
  codeclimate-test-reporter (0.5.0)
16
16
  simplecov (>= 0.7.1, < 1.0.0)
17
17
  coderay (1.1.1)
@@ -23,12 +23,16 @@ GEM
23
23
  dry-container (0.6.0)
24
24
  concurrent-ruby (~> 1.0)
25
25
  dry-configurable (~> 0.1, >= 0.1.3)
26
- dry-core (0.3.1)
26
+ dry-core (0.4.2)
27
27
  concurrent-ruby (~> 1.0)
28
28
  dry-equalizer (0.2.0)
29
- dry-matcher (0.6.0)
30
- dry-monads (0.3.1)
31
- dry-core
29
+ dry-events (0.1.0)
30
+ concurrent-ruby (~> 1.0)
31
+ dry-core (~> 0.4)
32
+ dry-equalizer (~> 0.2)
33
+ dry-matcher (0.7.0)
34
+ dry-monads (0.4.0)
35
+ dry-core (~> 0.3, >= 0.3.3)
32
36
  dry-equalizer
33
37
  json (1.8.6)
34
38
  method_source (0.8.2)
@@ -39,6 +43,9 @@ GEM
39
43
  coderay (~> 1.1.0)
40
44
  method_source (~> 0.8.1)
41
45
  slop (~> 3.4)
46
+ pry-byebug (3.5.0)
47
+ byebug (~> 9.1)
48
+ pry (~> 0.10)
42
49
  rainbow (2.1.0)
43
50
  rake (11.2.2)
44
51
  rspec (3.3.0)
@@ -68,7 +75,6 @@ GEM
68
75
  simplecov-html (0.10.0)
69
76
  slop (3.6.0)
70
77
  unicode-display_width (1.1.1)
71
- wisper (2.0.0)
72
78
  yard (0.8.7.6)
73
79
 
74
80
  PLATFORMS
@@ -76,16 +82,16 @@ PLATFORMS
76
82
 
77
83
  DEPENDENCIES
78
84
  bundler (~> 1.15)
79
- byebug
80
85
  codeclimate-test-reporter
81
86
  dry-container
82
87
  dry-transaction!
83
88
  pry
84
- rake (~> 11.2.2)
85
- rspec (~> 3.3.0)
89
+ pry-byebug
90
+ rake (~> 11.2, >= 11.2.2)
91
+ rspec (~> 3.3)
86
92
  rubocop
87
93
  simplecov
88
94
  yard
89
95
 
90
96
  BUNDLED WITH
91
- 1.15.1
97
+ 1.16.1
@@ -1,4 +1,3 @@
1
- require "dry/monads/either"
2
1
  require "dry/transaction/step"
3
2
  require "dry/transaction/dsl"
4
3
  require "dry/transaction/instance_methods"
@@ -17,9 +16,8 @@ module Dry
17
16
 
18
17
  def included(klass)
19
18
  klass.extend(dsl_mod)
20
- klass.send(:include, InstanceMethods)
21
- klass.send(:prepend, resolver_mod)
22
- klass.send(:include, Dry::Monads::Either::Mixin)
19
+ klass.include(InstanceMethods)
20
+ klass.prepend(resolver_mod)
23
21
  end
24
22
  end
25
23
  end
@@ -0,0 +1,38 @@
1
+ module Dry
2
+ module Transaction
3
+ # @api private
4
+ class Callable
5
+ def self.[](callable)
6
+ if callable.is_a?(self)
7
+ callable
8
+ elsif callable.nil?
9
+ nil
10
+ else
11
+ new(callable)
12
+ end
13
+ end
14
+
15
+ attr_reader :operation
16
+ attr_reader :arity
17
+
18
+ def initialize(operation)
19
+ @operation = case operation
20
+ when Proc, Method
21
+ operation
22
+ else
23
+ operation.method(:call)
24
+ end
25
+
26
+ @arity = @operation.arity
27
+ end
28
+
29
+ def call(*args, &block)
30
+ if arity.zero?
31
+ operation.(&block)
32
+ else
33
+ operation.(*args, &block)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module Dry
2
+ module Transaction
3
+ class InvalidStepError < ArgumentError
4
+ def initialize(step_name)
5
+ super("step +`#{step_name}`+ must respond to `#call`")
6
+ end
7
+ end
8
+
9
+ class MissingStepError < ArgumentError
10
+ def initialize(step_name)
11
+ super("Definition for step +`#{step_name}`+ is missing")
12
+ end
13
+ end
14
+
15
+ class InvalidResultError < ArgumentError
16
+ def initialize(step_name)
17
+ super("step +#{step_name}+ must return a Result object")
18
+ end
19
+ end
20
+
21
+ class MissingCatchListError < ArgumentError
22
+ def initialize(step_name)
23
+ super("step +#{step_name}+ requires one or more exception classes provided via +catch:+")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,33 +1,38 @@
1
- require "dry/monads"
1
+ require "dry/monads/result"
2
2
  require "dry/transaction/result_matcher"
3
+ require "dry/transaction/stack"
3
4
 
4
5
  module Dry
5
6
  module Transaction
6
7
  module InstanceMethods
8
+ include Dry::Monads::Result::Mixin
9
+
7
10
  attr_reader :steps
8
11
  attr_reader :operations
9
12
  attr_reader :listeners
13
+ attr_reader :stack
10
14
 
11
15
  def initialize(steps: (self.class.steps), listeners: nil, **operations)
12
16
  @steps = steps.map { |step|
13
- operation = methods.include?(step.step_name) ? method(step.step_name) : operations[step.step_name]
17
+ operation = resolve_operation(step, operations)
14
18
  step.with(operation: operation)
15
19
  }
16
20
  @operations = operations
21
+ @stack = Stack.new(@steps)
17
22
  subscribe(listeners) unless listeners.nil?
18
23
  end
19
24
 
20
- def call(input, &block)
25
+ def call(input = nil, &block)
21
26
  assert_step_arity
22
27
 
23
- result = steps.inject(Dry::Monads.Right(input), :bind)
28
+ result = stack.(Success(input))
24
29
 
25
30
  if block
26
31
  ResultMatcher.(result, &block)
27
32
  else
28
33
  result.or { |step_failure|
29
34
  # Unwrap the value from the StepFailure and return it directly
30
- Dry::Monads.Left(step_failure.value)
35
+ Failure(step_failure.value)
31
36
  }
32
37
  end
33
38
  end
@@ -76,6 +81,18 @@ module Dry
76
81
  operation.(*args, &block)
77
82
  end
78
83
 
84
+ def resolve_operation(step, **operations)
85
+ if methods.include?(step.step_name) || private_methods.include?(step.step_name)
86
+ method(step.step_name)
87
+ elsif operations[step.step_name].nil?
88
+ raise MissingStepError.new(step.step_name)
89
+ elsif operations[step.step_name].respond_to?(:call)
90
+ operations[step.step_name]
91
+ else
92
+ raise InvalidStepError.new(step.step_name)
93
+ end
94
+ end
95
+
79
96
  def assert_valid_step_args(step_args)
80
97
  step_args.each_key do |step_name|
81
98
  unless steps.any? { |step| step.step_name == step_name }
@@ -1,14 +1,14 @@
1
- require "dry/monads/either"
1
+ require "dry/monads/result"
2
2
  require "dry/matcher"
3
- require "dry/matcher/either_matcher"
3
+ require "dry/matcher/result_matcher"
4
4
 
5
5
  module Dry
6
6
  module Transaction
7
7
  module Operation
8
8
  def self.included(klass)
9
9
  klass.class_eval do
10
- include Dry::Monads::Either::Mixin
11
- include Dry::Matcher.for(:call, with: Dry::Matcher::EitherMatcher)
10
+ include Dry::Monads::Result::Mixin
11
+ include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
12
12
  end
13
13
  end
14
14
  end
@@ -6,7 +6,11 @@ module Dry
6
6
  define_method :initialize do |**kwargs|
7
7
  operation_kwargs = self.class.steps.select(&:operation_name).map { |step|
8
8
  operation = kwargs.fetch(step.step_name) {
9
- ops_container and ops_container.key?(step.operation_name) and ops_container[step.operation_name]
9
+ if ops_container && ops_container.key?(step.operation_name)
10
+ ops_container[step.operation_name]
11
+ else
12
+ nil
13
+ end
10
14
  }
11
15
 
12
16
  [step.step_name, operation]
@@ -5,17 +5,17 @@ module Dry
5
5
  ResultMatcher = Dry::Matcher.new(
6
6
  success: Dry::Matcher::Case.new(
7
7
  match: -> result { result.right? },
8
- resolve: -> result { result.value }
8
+ resolve: -> result { result.value! }
9
9
  ),
10
10
  failure: Dry::Matcher::Case.new(
11
11
  match: -> result, step_name = nil {
12
12
  if step_name
13
- result.left? && result.value.step.step_name == step_name
13
+ result.left? && result.left.step.step_name == step_name
14
14
  else
15
15
  result.left?
16
16
  end
17
17
  },
18
- resolve: -> result { result.value.value }
18
+ resolve: -> result { result.left.value }
19
19
  )
20
20
  )
21
21
  end
@@ -0,0 +1,24 @@
1
+ module Dry
2
+ module Transaction
3
+ # @api private
4
+ class Stack
5
+ RETURN = -> x { x }
6
+
7
+ def initialize(steps)
8
+ @stack = compile(steps)
9
+ end
10
+
11
+ def call(m)
12
+ @stack.(m)
13
+ end
14
+
15
+ private
16
+
17
+ def compile(steps)
18
+ steps.reverse.reduce(RETURN) do |next_step, step|
19
+ proc { |m| m.bind { |value| step.(value, next_step) } }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,62 +1,78 @@
1
- require "dry/monads/either"
2
- require "wisper"
1
+ require "dry/monads/result"
2
+ require 'dry/events/publisher'
3
3
  require "dry/transaction/step_failure"
4
+ require "dry/transaction/step_adapter"
4
5
 
5
6
  module Dry
6
7
  module Transaction
7
8
  # @api private
8
9
  class Step
9
10
  UNDEFINED = Object.new.freeze
11
+ RETURN = -> x { x }
10
12
 
11
- include Wisper::Publisher
12
- include Dry::Monads::Either::Mixin
13
+ include Dry::Events::Publisher[name || object_id]
14
+ include Dry::Monads::Result::Mixin
15
+
16
+ register_event(:step)
17
+ register_event(:step_succeeded)
18
+ register_event(:step_failed)
13
19
 
14
20
  attr_reader :step_adapter
15
21
  attr_reader :step_name
16
22
  attr_reader :operation_name
17
- attr_reader :operation
18
- attr_reader :options
19
23
  attr_reader :call_args
20
24
 
21
25
  def initialize(step_adapter, step_name, operation_name, operation, options, call_args = [])
22
- @step_adapter = step_adapter
26
+ @step_adapter = StepAdapter[step_adapter, operation, **options, step_name: step_name]
23
27
  @step_name = step_name
24
28
  @operation_name = operation_name
25
- @operation = operation
26
- @options = options
27
29
  @call_args = call_args
28
30
  end
29
31
 
30
32
  def with(operation: UNDEFINED, call_args: UNDEFINED)
31
33
  return self if operation == UNDEFINED && call_args == UNDEFINED
32
- new_operation = operation == UNDEFINED ? self.operation : operation
33
- new_call_args = call_args == UNDEFINED ? self.call_args : call_args
34
+
35
+ new_operation = operation == UNDEFINED ? step_adapter.operation : operation
36
+ new_call_args = call_args == UNDEFINED ? self.call_args : Array(call_args)
34
37
 
35
38
  self.class.new(
36
39
  step_adapter,
37
40
  step_name,
38
41
  operation_name,
39
42
  new_operation,
40
- options,
41
- new_call_args,
43
+ step_adapter.options,
44
+ new_call_args
42
45
  )
43
46
  end
44
47
 
45
- def call(input)
46
- args = [input] + Array(call_args)
47
- result = step_adapter.call(self, *args)
48
+ def call(input, continue = RETURN)
49
+ args = [input, *call_args]
50
+
51
+ if step_adapter.yields?
52
+ with_broadcast(args) { step_adapter.(args, &continue) }
53
+ else
54
+ continue.(with_broadcast(args) { step_adapter.(args) })
55
+ end
56
+ end
48
57
 
49
- result.fmap { |value|
50
- broadcast :"#{step_name}_success", value
58
+ def with_broadcast(args)
59
+ publish(:step, step_name: step_name, args: args)
60
+
61
+ yield.fmap { |value|
62
+ publish(:step_succeeded, step_name: step_name, args: args, value: value)
51
63
  value
52
64
  }.or { |value|
53
- broadcast :"#{step_name}_failure", *args, value
54
- Left(StepFailure.new(self, value))
65
+ publish(:step_failed, step_name: step_name, args: args, value: value)
66
+ Failure(StepFailure.new(self, value))
55
67
  }
56
68
  end
57
69
 
58
70
  def arity
59
- operation.is_a?(Proc) ? operation.arity : operation.method(:call).arity
71
+ step_adapter.operation.arity
72
+ end
73
+
74
+ def operation
75
+ step_adapter.operation
60
76
  end
61
77
  end
62
78
  end
@@ -0,0 +1,49 @@
1
+ require "dry/transaction/callable"
2
+
3
+ module Dry
4
+ module Transaction
5
+ # @api private
6
+ class StepAdapter
7
+ def self.[](adapter, operation, options)
8
+ if adapter.is_a?(self)
9
+ adapter.with(operation, options)
10
+ else
11
+ new(adapter, operation, options)
12
+ end
13
+ end
14
+
15
+ attr_reader :adapter
16
+ attr_reader :operation
17
+ attr_reader :options
18
+
19
+ def initialize(adapter, operation, options)
20
+ @adapter = case adapter
21
+ when Proc, Method
22
+ adapter
23
+ else
24
+ adapter.method(:call)
25
+ end
26
+
27
+ @operation = Callable[operation]
28
+
29
+ @options = options
30
+
31
+ @yields = @adapter.
32
+ parameters.
33
+ any? { |type, _| type == :block }
34
+ end
35
+
36
+ def yields?
37
+ @yields
38
+ end
39
+
40
+ def call(args, &block)
41
+ adapter.(operation, options, args, &block)
42
+ end
43
+
44
+ def with(operation = self.operation, new_options = {})
45
+ self.class.new(adapter, operation, options.merge(new_options))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ require "dry/monads/result"
2
+ require "dry/transaction/errors"
3
+
4
+ module Dry
5
+ module Transaction
6
+ class StepAdapters
7
+ # @api private
8
+ class Around
9
+ include Dry::Monads::Result::Mixin
10
+
11
+ def call(operation, options, args, &block)
12
+ result = operation.(*args, &block)
13
+
14
+ unless result.is_a?(Dry::Monads::Result)
15
+ raise InvalidResultError.new(options[:step_name])
16
+ end
17
+
18
+ result
19
+ end
20
+ end
21
+
22
+ register :around, Around.new
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ module Dry
2
+ module Transaction
3
+ class StepAdapters
4
+ # @api private
5
+ class Check
6
+ include Dry::Monads::Either::Mixin
7
+
8
+ def call(operation, _options, args)
9
+ input = args[0]
10
+ res = operation.(*args)
11
+ res == true || res.is_a?(Success) ? Success(input) : Failure(input)
12
+ end
13
+ end
14
+
15
+ register :check, Check.new
16
+ end
17
+ end
18
+ end
@@ -3,10 +3,10 @@ module Dry
3
3
  class StepAdapters
4
4
  # @api private
5
5
  class Map
6
- include Dry::Monads::Either::Mixin
6
+ include Dry::Monads::Result::Mixin
7
7
 
8
- def call(step, input, *args)
9
- Right(step.operation.call(input, *args))
8
+ def call(operation, _options, args)
9
+ Success(operation.(*args))
10
10
  end
11
11
  end
12
12
 
@@ -1,20 +1,14 @@
1
- require "dry/monads/either"
1
+ require "dry/monads/result"
2
+ require "dry/transaction/errors"
3
+ require "dry/transaction/step_adapters/around"
2
4
 
3
5
  module Dry
4
6
  module Transaction
5
7
  class StepAdapters
6
8
  # @api private
7
- class Raw
8
- include Dry::Monads::Either::Mixin
9
-
10
- def call(step, input, *args)
11
- result = step.operation.call(input, *args)
12
-
13
- unless result.is_a?(Dry::Monads::Either)
14
- raise ArgumentError, "step +#{step.step_name}+ must return an Either object"
15
- end
16
-
17
- result
9
+ class Raw < Around
10
+ def call(operation, options, args)
11
+ super(operation, options, args, &nil)
18
12
  end
19
13
  end
20
14
 
@@ -3,11 +3,11 @@ module Dry
3
3
  class StepAdapters
4
4
  # @api private
5
5
  class Tee
6
- include Dry::Monads::Either::Mixin
6
+ include Dry::Monads::Result::Mixin
7
7
 
8
- def call(step, input, *args)
9
- step.operation.call(input, *args)
10
- Right(input)
8
+ def call(operation, _options, args)
9
+ operation.(*args)
10
+ Success(args[0])
11
11
  end
12
12
  end
13
13
 
@@ -1,19 +1,22 @@
1
+ require "dry/transaction/errors"
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class StepAdapters
4
6
  # @api private
5
7
  class Try
6
- include Dry::Monads::Either::Mixin
8
+ include Dry::Monads::Result::Mixin
7
9
 
8
- def call(step, input, *args)
9
- unless step.options[:catch]
10
- raise ArgumentError, "+try+ steps require one or more exception classes provided via +catch:+"
10
+ def call(operation, options, args)
11
+ unless options[:catch]
12
+ raise MissingCatchListError.new(options[:step_name])
11
13
  end
12
14
 
13
- Right(step.operation.call(input, *args))
14
- rescue *Array(step.options[:catch]) => e
15
- e = step.options[:raise].new(e.message) if step.options[:raise]
16
- Left(e)
15
+ result = operation.(*args)
16
+ Success(result)
17
+ rescue *Array(options[:catch]) => e
18
+ e = options[:raise].new(e.message) if options[:raise]
19
+ Failure(e)
17
20
  end
18
21
  end
19
22
 
@@ -8,7 +8,9 @@ module Dry
8
8
  end
9
9
  end
10
10
 
11
+ require "dry/transaction/step_adapters/check"
11
12
  require "dry/transaction/step_adapters/map"
12
13
  require "dry/transaction/step_adapters/raw"
13
14
  require "dry/transaction/step_adapters/tee"
14
15
  require "dry/transaction/step_adapters/try"
16
+ require "dry/transaction/step_adapters/around"
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  # Business transaction DSL.
3
3
  module Transaction
4
- VERSION = "0.10.2".freeze
4
+ VERSION = "0.11.0".freeze
5
5
  end
6
6
  end