bcdd-result 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -10
  3. data/CHANGELOG.md +49 -2
  4. data/README.md +709 -127
  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 +42 -0
  21. data/lib/bcdd/result/expectations.rb +27 -48
  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 +36 -4
  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 +11 -6
  29. data/sig/bcdd/result.rbs +245 -71
  30. metadata +19 -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 or 1.")
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
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
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
+
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
42
+ end
@@ -1,62 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Expectations
4
- require_relative 'expectations/error'
5
- require_relative 'expectations/contract'
6
- require_relative 'expectations/type_checker'
7
-
8
- MIXIN_METHODS = <<~RUBY
9
- def Success(...)
10
- _expected_result::Success(...)
3
+ class BCDD::Result
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 = 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
11
15
  end
12
16
 
13
- def Failure(...)
14
- _expected_result::Failure(...)
15
- end
17
+ def initialize(subject: nil, success: nil, failure: nil, contract: nil)
18
+ @subject = subject
16
19
 
17
- private
20
+ @contract = contract if contract.is_a?(Contract::Evaluator)
18
21
 
19
- def _expected_result
20
- @_expected_result ||= Expected.with(subject: self)
22
+ @contract ||= Contract.new(success: success, failure: failure).freeze
21
23
  end
22
- RUBY
23
-
24
- def self.mixin(success: nil, failure: nil)
25
- mod = Module.new
26
- mod.const_set(:Expected, new(success: success, failure: failure).freeze)
27
- mod.module_eval(MIXIN_METHODS)
28
- mod
29
- end
30
-
31
- def self.evaluate(data, expectations)
32
- expectations ||= Contract::NONE
33
-
34
- expectations.type_and_value!(data)
35
-
36
- TypeChecker.new(data.type, expectations: expectations)
37
- end
38
-
39
- def initialize(subject: nil, success: nil, failure: nil, contract: nil)
40
- @subject = subject
41
24
 
42
- @contract = contract if contract.is_a?(Contract::Evaluator)
25
+ def Success(type, value = nil)
26
+ Success.new(type: type, value: value, subject: subject, expectations: contract)
27
+ end
43
28
 
44
- @contract ||= Contract.new(success: success, failure: failure).freeze
45
- end
29
+ def Failure(type, value = nil)
30
+ Failure.new(type: type, value: value, subject: subject, expectations: contract)
31
+ end
46
32
 
47
- def Success(type, value = nil)
48
- ::BCDD::Result::Success.new(type: type, value: value, subject: subject, expectations: contract)
49
- end
33
+ def with(subject:)
34
+ self.class.new(subject: subject, contract: contract)
35
+ end
50
36
 
51
- def Failure(type, value = nil)
52
- ::BCDD::Result::Failure.new(type: type, value: value, subject: subject, expectations: contract)
53
- end
37
+ private
54
38
 
55
- def with(subject:)
56
- self.class.new(subject: subject, contract: contract)
39
+ attr_reader :subject, :contract
57
40
  end
58
-
59
- private
60
-
61
- attr_reader :subject, :contract
62
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)
@@ -2,12 +2,44 @@
2
2
 
3
3
  class BCDD::Result
4
4
  module Mixin
5
- def Success(type, value = nil)
6
- Success.new(type: type, value: value, subject: self)
5
+ module Methods
6
+ def Success(type, value = nil)
7
+ Success.new(type: type, value: value, subject: self)
8
+ end
9
+
10
+ def Failure(type, value = nil)
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(:continued, value)
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
7
27
  end
8
28
 
9
- def Failure(type, value = nil)
10
- Failure.new(type: type, value: value, subject: self)
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
11
34
  end
12
35
  end
36
+
37
+ def self.mixin(with: nil)
38
+ addons = Mixin::Addons.options(with)
39
+
40
+ mod = Mixin.module!
41
+ mod.send(:include, Mixin::Methods)
42
+ mod.send(:include, *addons) unless addons.empty?
43
+ mod
44
+ end
13
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