bcdd-result 0.6.0 → 0.7.0

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +24 -11
  3. data/CHANGELOG.md +28 -0
  4. data/README.md +585 -160
  5. data/lib/bcdd/result/context/expectations/mixin.rb +35 -0
  6. data/lib/bcdd/result/context/expectations.rb +41 -0
  7. data/lib/bcdd/result/context/failure.rb +9 -0
  8. data/lib/bcdd/result/context/mixin.rb +38 -0
  9. data/lib/bcdd/result/context/success.rb +15 -0
  10. data/lib/bcdd/result/context.rb +74 -0
  11. data/lib/bcdd/result/{expectations/contract → contract}/disabled.rb +2 -2
  12. data/lib/bcdd/result/{expectations → contract}/error.rb +5 -3
  13. data/lib/bcdd/result/{expectations/contract → contract}/evaluator.rb +2 -2
  14. data/lib/bcdd/result/{expectations/contract → contract}/for_types.rb +2 -2
  15. data/lib/bcdd/result/{expectations/contract → contract}/for_types_and_values.rb +10 -8
  16. data/lib/bcdd/result/contract/interface.rb +21 -0
  17. data/lib/bcdd/result/{expectations → contract}/type_checker.rb +1 -1
  18. data/lib/bcdd/result/contract.rb +43 -0
  19. data/lib/bcdd/result/error.rb +7 -9
  20. data/lib/bcdd/result/expectations/mixin.rb +14 -9
  21. data/lib/bcdd/result/expectations.rb +28 -37
  22. data/lib/bcdd/result/failure/methods.rb +21 -0
  23. data/lib/bcdd/result/failure.rb +2 -16
  24. data/lib/bcdd/result/mixin.rb +8 -3
  25. data/lib/bcdd/result/success/methods.rb +21 -0
  26. data/lib/bcdd/result/success.rb +2 -16
  27. data/lib/bcdd/result/version.rb +1 -1
  28. data/lib/bcdd/result.rb +6 -4
  29. data/sig/bcdd/result.rbs +227 -83
  30. metadata +18 -10
  31. data/lib/bcdd/result/expectations/contract/interface.rb +0 -21
  32. data/lib/bcdd/result/expectations/contract.rb +0 -25
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ module Expectations::Mixin
5
+ METHODS = <<~RUBY
6
+ def Success(...)
7
+ _Result::Success(...)
8
+ end
9
+
10
+ def Failure(...)
11
+ _Result::Failure(...)
12
+ end
13
+
14
+ private
15
+
16
+ def _Result
17
+ @_Result ||= Result.with(subject: self)
18
+ end
19
+ RUBY
20
+
21
+ module Addons
22
+ module Continuable
23
+ private def Continue(**value)
24
+ Success.new(type: :continued, value: value, subject: self)
25
+ end
26
+ end
27
+
28
+ OPTIONS = { Continue: Continuable }.freeze
29
+
30
+ def self.options(names)
31
+ Array(names).filter_map { |name| OPTIONS[name] }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ class Expectations
5
+ require_relative 'expectations/mixin'
6
+
7
+ def self.mixin(success: nil, failure: nil, with: nil)
8
+ addons = Mixin::Addons.options(with)
9
+
10
+ mod = ::BCDD::Result::Expectations::Mixin.module!
11
+ mod.const_set(:Result, new(success: success, failure: failure).freeze)
12
+ mod.module_eval(Mixin::METHODS)
13
+ mod.send(:include, *addons) unless addons.empty?
14
+ mod
15
+ end
16
+
17
+ def initialize(subject: nil, success: nil, failure: nil, contract: nil)
18
+ @subject = subject
19
+
20
+ @contract = contract if contract.is_a?(::BCDD::Result::Contract::Evaluator)
21
+
22
+ @contract ||= ::BCDD::Result::Contract.new(success: success, failure: failure).freeze
23
+ end
24
+
25
+ def Success(type, **value)
26
+ Success.new(type: type, value: value, subject: subject, expectations: contract)
27
+ end
28
+
29
+ def Failure(type, **value)
30
+ Failure.new(type: type, value: value, subject: subject, expectations: contract)
31
+ end
32
+
33
+ def with(subject:)
34
+ self.class.new(subject: subject, contract: contract)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :subject, :contract
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context::Failure < BCDD::Result::Context
4
+ include BCDD::Result::Failure::Methods
5
+
6
+ def and_expose(_type, _keys)
7
+ self
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context
4
+ module Mixin
5
+ module Methods
6
+ def Success(type, **value)
7
+ Success.new(type: type, value: value, subject: self)
8
+ end
9
+
10
+ def Failure(type, **value)
11
+ Failure.new(type: type, value: value, subject: self)
12
+ end
13
+ end
14
+
15
+ module Addons
16
+ module Continuable
17
+ private def Continue(**value)
18
+ Success.new(type: :continued, value: value, subject: self)
19
+ end
20
+ end
21
+
22
+ OPTIONS = { Continue: Continuable }.freeze
23
+
24
+ def self.options(names)
25
+ Array(names).filter_map { |name| OPTIONS[name] }
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.mixin(with: nil)
31
+ addons = Mixin::Addons.options(with)
32
+
33
+ mod = ::BCDD::Result::Mixin.module!
34
+ mod.send(:include, Mixin::Methods)
35
+ mod.send(:include, *addons) unless addons.empty?
36
+ mod
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Context::Success < BCDD::Result::Context
4
+ include ::BCDD::Result::Success::Methods
5
+
6
+ def and_expose(type, keys)
7
+ unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
8
+ raise ::ArgumentError, 'keys must be an Array of Symbols'
9
+ end
10
+
11
+ exposed_value = acc.merge(value).slice(*keys)
12
+
13
+ self.class.new(type: type, value: exposed_value, subject: subject)
14
+ end
15
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Context < self
5
+ require_relative 'context/failure'
6
+ require_relative 'context/success'
7
+ require_relative 'context/mixin'
8
+ require_relative 'context/expectations'
9
+
10
+ def self.Success(type, **value)
11
+ Success.new(type: type, value: value)
12
+ end
13
+
14
+ def self.Failure(type, **value)
15
+ Failure.new(type: type, value: value)
16
+ end
17
+
18
+ def initialize(type:, value:, subject: nil, expectations: nil)
19
+ value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
20
+
21
+ @acc = {}
22
+
23
+ super
24
+ end
25
+
26
+ def and_then(method_name = nil, **context_data, &block)
27
+ super(method_name, context_data, &block)
28
+ end
29
+
30
+ protected
31
+
32
+ attr_reader :acc
33
+
34
+ private
35
+
36
+ SubjectMethodArity = ->(method) do
37
+ return 0 if method.arity.zero?
38
+ return 1 if method.parameters.map(&:first).all?(/\Akey/)
39
+
40
+ -1
41
+ end
42
+
43
+ def call_subject_method(method_name, context)
44
+ method = subject.method(method_name)
45
+
46
+ acc.merge!(value.merge(context))
47
+
48
+ result =
49
+ case SubjectMethodArity[method]
50
+ when 0 then subject.send(method_name)
51
+ when 1 then subject.send(method_name, **acc)
52
+ else raise Error::InvalidSubjectMethodArity.build(subject: subject, method: method, max_arity: 1)
53
+ end
54
+
55
+ ensure_result_object(result, origin: :method)
56
+ end
57
+
58
+ def ensure_result_object(result, origin:)
59
+ raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)
60
+
61
+ return result.tap { _1.acc.merge!(acc) } if result.subject.equal?(subject)
62
+
63
+ raise Error::InvalidResultSubject.build(given_result: result, expected_subject: subject)
64
+ end
65
+
66
+ EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
67
+
68
+ def raise_unexpected_outcome_error(result, origin)
69
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
70
+ end
71
+
72
+ private_constant :SubjectMethodArity, :EXPECTED_OUTCOME
73
+ end
74
+ end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  module Contract::Disabled
5
5
  extend Contract::Interface
6
6
 
7
- EMPTY_SET = Set.new.freeze
7
+ EMPTY_SET = ::Set.new.freeze
8
8
 
9
9
  def self.allowed_types
10
10
  EMPTY_SET
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations::Error < BCDD::Result::Error
3
+ class BCDD::Result::Contract::Error < BCDD::Result::Error
4
4
  class UnexpectedType < self
5
5
  def self.build(type:, allowed_types:)
6
6
  new("type :#{type} is not allowed. Allowed types: #{allowed_types.map(&:inspect).join(', ')}")
@@ -8,8 +8,10 @@ class BCDD::Result::Expectations::Error < BCDD::Result::Error
8
8
  end
9
9
 
10
10
  class UnexpectedValue < self
11
- def self.build(type:, value:)
12
- new("value #{value.inspect} is not allowed for :#{type} type")
11
+ def self.build(type:, value:, cause: nil)
12
+ cause_message = " (#{cause.message})" if cause
13
+
14
+ new("value #{value.inspect} is not allowed for :#{type} type#{cause_message}")
13
15
  end
14
16
  end
15
17
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  class Contract::Evaluator
5
5
  include Contract::Interface
6
6
 
@@ -25,7 +25,7 @@ class BCDD::Result::Expectations
25
25
  def type!(type)
26
26
  return type if type?(type)
27
27
 
28
- raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
28
+ raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
29
29
  end
30
30
 
31
31
  def type_and_value!(data)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  class Contract::ForTypes
5
5
  include Contract::Interface
6
6
 
@@ -17,7 +17,7 @@ class BCDD::Result::Expectations
17
17
  def type!(type)
18
18
  return type if type?(type)
19
19
 
20
- raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
20
+ raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
21
21
  end
22
22
 
23
23
  def type_and_value!(data)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ class BCDD::Result
4
4
  class Contract::ForTypesAndValues
5
5
  include Contract::Interface
6
6
 
@@ -23,15 +23,17 @@ class BCDD::Result::Expectations
23
23
  end
24
24
 
25
25
  def type_and_value!(data)
26
- type = data.type
27
- value = data.value
28
- allowed_value = @types_and_values[type!(type)]
26
+ type, value = data.type, data.value
29
27
 
30
- return value if allowed_value === value
28
+ value_checking = @types_and_values[type!(type)]
31
29
 
32
- raise Error::UnexpectedValue.build(type: type, value: value)
33
- rescue NoMatchingPatternError
34
- raise Error::UnexpectedValue.build(type: data.type, value: data.value)
30
+ checking_result = value_checking === value
31
+
32
+ return value if checking_result || (Contract.nil_as_valid_value_checking? && checking_result.nil?)
33
+
34
+ raise Contract::Error::UnexpectedValue.build(type: type, value: value)
35
+ rescue ::NoMatchingPatternError => e
36
+ raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e)
35
37
  end
36
38
  end
37
39
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Contract::Interface
5
+ def allowed_types
6
+ raise Error::NotImplemented
7
+ end
8
+
9
+ def type?(_type)
10
+ raise Error::NotImplemented
11
+ end
12
+
13
+ def type!(_type)
14
+ raise Error::NotImplemented
15
+ end
16
+
17
+ def type_and_value!(_data)
18
+ raise Error::NotImplemented
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
3
+ module BCDD::Result::Contract
4
4
  class TypeChecker
5
5
  attr_reader :result_type, :expectations
6
6
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Contract
4
+ require_relative 'contract/error'
5
+ require_relative 'contract/type_checker'
6
+ require_relative 'contract/interface'
7
+ require_relative 'contract/evaluator'
8
+ require_relative 'contract/disabled'
9
+ require_relative 'contract/for_types'
10
+ require_relative 'contract/for_types_and_values'
11
+
12
+ NONE = Evaluator.new(Disabled, Disabled).freeze
13
+
14
+ def self.evaluate(data, contract)
15
+ contract ||= NONE
16
+
17
+ contract.type_and_value!(data)
18
+
19
+ TypeChecker.new(data.type, expectations: contract)
20
+ end
21
+
22
+ ToEnsure = ->(spec) do
23
+ return Disabled if spec.nil?
24
+
25
+ spec.is_a?(::Hash) ? ForTypesAndValues.new(spec) : ForTypes.new(Array(spec))
26
+ end
27
+
28
+ def self.new(success:, failure:)
29
+ Evaluator.new(ToEnsure[success], ToEnsure[failure])
30
+ end
31
+
32
+ @nil_as_valid_value_checking = false
33
+
34
+ def self.nil_as_valid_value_checking!(enabled: true)
35
+ @nil_as_valid_value_checking = enabled
36
+ end
37
+
38
+ def self.nil_as_valid_value_checking?
39
+ @nil_as_valid_value_checking
40
+ end
41
+
42
+ private_constant :ToEnsure
43
+ end
@@ -15,16 +15,14 @@ class BCDD::Result::Error < StandardError
15
15
  end
16
16
 
17
17
  class UnexpectedOutcome < self
18
- def self.build(outcome:, origin:)
19
- message =
20
- "Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by " \
21
- 'BCDD::Result::Success or BCDD::Result::Failure'
18
+ def self.build(outcome:, origin:, expected: nil)
19
+ expected ||= 'BCDD::Result::Success or BCDD::Result::Failure'
22
20
 
23
- new(message)
21
+ new("Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by #{expected}")
24
22
  end
25
23
  end
26
24
 
27
- class WrongResultSubject < self
25
+ class InvalidResultSubject < self
28
26
  def self.build(given_result:, expected_subject:)
29
27
  message =
30
28
  "You cannot call #and_then and return a result that does not belong to the subject!\n" \
@@ -36,9 +34,9 @@ class BCDD::Result::Error < StandardError
36
34
  end
37
35
  end
38
36
 
39
- class WrongSubjectMethodArity < self
40
- def self.build(subject:, method:)
41
- new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0, 1 or 2.")
37
+ class InvalidSubjectMethodArity < self
38
+ def self.build(subject:, method:, max_arity:)
39
+ new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0..#{max_arity}")
42
40
  end
43
41
  end
44
42
 
@@ -1,27 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
4
- module Mixin
3
+ class BCDD::Result
4
+ module Expectations::Mixin
5
5
  METHODS = <<~RUBY
6
6
  def Success(...)
7
- __expected::Success(...)
7
+ _Result.Success(...)
8
8
  end
9
9
 
10
10
  def Failure(...)
11
- __expected::Failure(...)
11
+ _Result.Failure(...)
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- def __expected
17
- @__expected ||= Expected.with(subject: self)
16
+ def _Result
17
+ @_Result ||= Result.with(subject: self)
18
18
  end
19
19
  RUBY
20
20
 
21
21
  module Addons
22
22
  module Continuable
23
23
  private def Continue(value)
24
- ::BCDD::Result::Success.new(type: :continued, value: value, subject: self)
24
+ Success.new(type: :continued, value: value, subject: self)
25
25
  end
26
26
  end
27
27
 
@@ -31,7 +31,12 @@ class BCDD::Result::Expectations
31
31
  Array(names).filter_map { |name| OPTIONS[name] }
32
32
  end
33
33
  end
34
- end
35
34
 
36
- private_constant :Mixin
35
+ def self.module!
36
+ ::Module.new do
37
+ def self.included(base); base.const_set(:ResultExpectationsMixin, self); end
38
+ def self.extended(base); base.const_set(:ResultExpectationsMixin, self); end
39
+ end
40
+ end
41
+ end
37
42
  end
@@ -1,50 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
4
- require_relative 'expectations/mixin'
5
- require_relative 'expectations/error'
6
- require_relative 'expectations/contract'
7
- require_relative 'expectations/type_checker'
8
-
9
- def self.mixin(success: nil, failure: nil, with: nil)
10
- addons = Mixin::Addons.options(with)
11
-
12
- mod = Module.new
13
- mod.const_set(:Expected, new(success: success, failure: failure).freeze)
14
- mod.module_eval(Mixin::METHODS)
15
- mod.send(:include, *addons) unless addons.empty?
16
- mod
17
- end
18
-
19
- def self.evaluate(data, expectations)
20
- expectations ||= Contract::NONE
3
+ class BCDD::Result
4
+ class Expectations
5
+ require_relative 'expectations/mixin'
21
6
 
22
- expectations.type_and_value!(data)
7
+ def self.mixin(success: nil, failure: nil, with: nil)
8
+ addons = Mixin::Addons.options(with)
23
9
 
24
- TypeChecker.new(data.type, expectations: expectations)
25
- end
10
+ mod = Mixin.module!
11
+ mod.const_set(:Result, new(success: success, failure: failure).freeze)
12
+ mod.module_eval(Mixin::METHODS)
13
+ mod.send(:include, *addons) unless addons.empty?
14
+ mod
15
+ end
26
16
 
27
- def initialize(subject: nil, success: nil, failure: nil, contract: nil)
28
- @subject = subject
17
+ def initialize(subject: nil, success: nil, failure: nil, contract: nil)
18
+ @subject = subject
29
19
 
30
- @contract = contract if contract.is_a?(Contract::Evaluator)
20
+ @contract = contract if contract.is_a?(Contract::Evaluator)
31
21
 
32
- @contract ||= Contract.new(success: success, failure: failure).freeze
33
- end
22
+ @contract ||= Contract.new(success: success, failure: failure).freeze
23
+ end
34
24
 
35
- def Success(type, value = nil)
36
- ::BCDD::Result::Success.new(type: type, value: value, subject: subject, expectations: contract)
37
- end
25
+ def Success(type, value = nil)
26
+ Success.new(type: type, value: value, subject: subject, expectations: contract)
27
+ end
38
28
 
39
- def Failure(type, value = nil)
40
- ::BCDD::Result::Failure.new(type: type, value: value, subject: subject, expectations: contract)
41
- end
29
+ def Failure(type, value = nil)
30
+ Failure.new(type: type, value: value, subject: subject, expectations: contract)
31
+ end
42
32
 
43
- def with(subject:)
44
- self.class.new(subject: subject, contract: contract)
45
- end
33
+ def with(subject:)
34
+ self.class.new(subject: subject, contract: contract)
35
+ end
46
36
 
47
- private
37
+ private
48
38
 
49
- attr_reader :subject, :contract
39
+ attr_reader :subject, :contract
40
+ end
50
41
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Failure::Methods
4
+ def success?(_type = nil)
5
+ false
6
+ end
7
+
8
+ def failure?(type = nil)
9
+ type.nil? || type_checker.allow_failure?([type])
10
+ end
11
+
12
+ def value_or
13
+ yield(value)
14
+ end
15
+
16
+ private
17
+
18
+ def name
19
+ :failure
20
+ end
21
+ end
@@ -2,23 +2,9 @@
2
2
 
3
3
  class BCDD::Result
4
4
  class Failure < self
5
- def success?(_type = nil)
6
- false
7
- end
5
+ require_relative 'failure/methods'
8
6
 
9
- def failure?(type = nil)
10
- type.nil? || type_checker.allow_failure?([type])
11
- end
12
-
13
- def value_or
14
- yield
15
- end
16
-
17
- private
18
-
19
- def name
20
- :failure
21
- end
7
+ include Methods
22
8
  end
23
9
 
24
10
  def self.Failure(type, value = nil)
@@ -25,16 +25,21 @@ class BCDD::Result
25
25
  Array(names).filter_map { |name| OPTIONS[name] }
26
26
  end
27
27
  end
28
+
29
+ def self.module!
30
+ ::Module.new do
31
+ def self.included(base); base.const_set(:ResultMixin, self); end
32
+ def self.extended(base); base.const_set(:ResultMixin, self); end
33
+ end
34
+ end
28
35
  end
29
36
 
30
37
  def self.mixin(with: nil)
31
38
  addons = Mixin::Addons.options(with)
32
39
 
33
- mod = Module.new
40
+ mod = Mixin.module!
34
41
  mod.send(:include, Mixin::Methods)
35
42
  mod.send(:include, *addons) unless addons.empty?
36
43
  mod
37
44
  end
38
-
39
- private_constant :Mixin
40
45
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::Success::Methods
4
+ def success?(type = nil)
5
+ type.nil? || type_checker.allow_success?([type])
6
+ end
7
+
8
+ def failure?(_type = nil)
9
+ false
10
+ end
11
+
12
+ def value_or
13
+ value
14
+ end
15
+
16
+ private
17
+
18
+ def name
19
+ :success
20
+ end
21
+ end
@@ -2,23 +2,9 @@
2
2
 
3
3
  class BCDD::Result
4
4
  class Success < self
5
- def success?(type = nil)
6
- type.nil? || type_checker.allow_success?([type])
7
- end
5
+ require_relative 'success/methods'
8
6
 
9
- def failure?(_type = nil)
10
- false
11
- end
12
-
13
- def value_or
14
- value
15
- end
16
-
17
- private
18
-
19
- def name
20
- :success
21
- end
7
+ include Methods
22
8
  end
23
9
 
24
10
  def self.Success(type, value = nil)