bcdd-result 0.4.0 → 0.6.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.
@@ -38,7 +38,15 @@ class BCDD::Result::Error < StandardError
38
38
 
39
39
  class WrongSubjectMethodArity < self
40
40
  def self.build(subject:, method:)
41
- new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0 or 1.")
41
+ new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0, 1 or 2.")
42
+ end
43
+ end
44
+
45
+ class UnhandledTypes < self
46
+ def self.build(types:)
47
+ subject = types.size == 1 ? 'This was' : 'These were'
48
+
49
+ new("You must handle all cases. #{subject} not handled: #{types.map(&:inspect).join(', ')}")
42
50
  end
43
51
  end
44
52
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ module Contract::Disabled
5
+ extend Contract::Interface
6
+
7
+ EMPTY_SET = Set.new.freeze
8
+
9
+ def self.allowed_types
10
+ EMPTY_SET
11
+ end
12
+
13
+ def self.type?(_type)
14
+ true
15
+ end
16
+
17
+ def self.type!(type)
18
+ type
19
+ end
20
+
21
+ def self.type_and_value!(_data); end
22
+
23
+ private_constant :EMPTY_SET
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ class Contract::Evaluator
5
+ include Contract::Interface
6
+
7
+ attr_reader :allowed_types, :success, :failure
8
+
9
+ def initialize(success, failure)
10
+ @success = success
11
+ @failure = failure
12
+
13
+ @allowed_types = (success.allowed_types | failure.allowed_types).freeze
14
+ end
15
+
16
+ def type?(type)
17
+ success_disabled = success == Contract::Disabled
18
+ failure_disabled = failure == Contract::Disabled
19
+
20
+ return Contract::Disabled.type?(type) if success_disabled && failure_disabled
21
+
22
+ (!success_disabled && success.type?(type)) || (!failure_disabled && failure.type?(type))
23
+ end
24
+
25
+ def type!(type)
26
+ return type if type?(type)
27
+
28
+ raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
29
+ end
30
+
31
+ def type_and_value!(data)
32
+ self.for(data).type_and_value!(data)
33
+ end
34
+
35
+ private
36
+
37
+ def for(data)
38
+ case data.name
39
+ when :unknown then Contract::Disabled
40
+ when :success then success
41
+ else failure
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ class Contract::ForTypes
5
+ include Contract::Interface
6
+
7
+ attr_reader :allowed_types
8
+
9
+ def initialize(types)
10
+ @allowed_types = Array(types).map(&:to_sym).to_set.freeze
11
+ end
12
+
13
+ def type?(type)
14
+ allowed_types.member?(type)
15
+ end
16
+
17
+ def type!(type)
18
+ return type if type?(type)
19
+
20
+ raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
21
+ end
22
+
23
+ def type_and_value!(data)
24
+ type!(data.type)
25
+
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ class Contract::ForTypesAndValues
5
+ include Contract::Interface
6
+
7
+ def initialize(types_and_values)
8
+ @types_and_values = types_and_values.transform_keys(&:to_sym)
9
+
10
+ @types_contract = Contract::ForTypes.new(@types_and_values.keys)
11
+ end
12
+
13
+ def allowed_types
14
+ @types_contract.allowed_types
15
+ end
16
+
17
+ def type?(type)
18
+ @types_contract.type?(type)
19
+ end
20
+
21
+ def type!(type)
22
+ @types_contract.type!(type)
23
+ end
24
+
25
+ def type_and_value!(data)
26
+ type = data.type
27
+ value = data.value
28
+ allowed_value = @types_and_values[type!(type)]
29
+
30
+ return value if allowed_value === value
31
+
32
+ raise Error::UnexpectedValue.build(type: type, value: value)
33
+ rescue NoMatchingPatternError
34
+ raise Error::UnexpectedValue.build(type: data.type, value: data.value)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ module Contract::Interface
5
+ def allowed_types
6
+ raise ::BCDD::Result::Error::NotImplemented
7
+ end
8
+
9
+ def type?(_type)
10
+ raise ::BCDD::Result::Error::NotImplemented
11
+ end
12
+
13
+ def type!(_type)
14
+ raise ::BCDD::Result::Error::NotImplemented
15
+ end
16
+
17
+ def type_and_value!(_data)
18
+ raise ::BCDD::Result::Error::NotImplemented
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ module Contract
5
+ require_relative 'contract/interface'
6
+ require_relative 'contract/evaluator'
7
+ require_relative 'contract/disabled'
8
+ require_relative 'contract/for_types'
9
+ require_relative 'contract/for_types_and_values'
10
+
11
+ NONE = Contract::Evaluator.new(Contract::Disabled, Contract::Disabled).freeze
12
+
13
+ ToEnsure = ->(spec) do
14
+ return Contract::Disabled if spec.nil?
15
+
16
+ spec.is_a?(Hash) ? Contract::ForTypesAndValues.new(spec) : Contract::ForTypes.new(Array(spec))
17
+ end
18
+
19
+ def self.new(success:, failure:)
20
+ Contract::Evaluator.new(ToEnsure[success], ToEnsure[failure])
21
+ end
22
+
23
+ private_constant :ToEnsure
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations::Error < BCDD::Result::Error
4
+ class UnexpectedType < self
5
+ def self.build(type:, allowed_types:)
6
+ new("type :#{type} is not allowed. Allowed types: #{allowed_types.map(&:inspect).join(', ')}")
7
+ end
8
+ end
9
+
10
+ class UnexpectedValue < self
11
+ def self.build(type:, value:)
12
+ new("value #{value.inspect} is not allowed for :#{type} type")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ module Mixin
5
+ METHODS = <<~RUBY
6
+ def Success(...)
7
+ __expected::Success(...)
8
+ end
9
+
10
+ def Failure(...)
11
+ __expected::Failure(...)
12
+ end
13
+
14
+ private
15
+
16
+ def __expected
17
+ @__expected ||= Expected.with(subject: self)
18
+ end
19
+ RUBY
20
+
21
+ module Addons
22
+ module Continuable
23
+ private def Continue(value)
24
+ ::BCDD::Result::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
+
36
+ private_constant :Mixin
37
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result::Expectations
4
+ class TypeChecker
5
+ attr_reader :result_type, :expectations
6
+
7
+ def initialize(result_type, expectations:)
8
+ @result_type = result_type
9
+
10
+ @expectations = expectations
11
+ end
12
+
13
+ def allow?(types)
14
+ validate(types, expected: expectations, allow_empty: false)
15
+ end
16
+
17
+ def allow_success?(types)
18
+ validate(types, expected: expectations.success, allow_empty: true)
19
+ end
20
+
21
+ def allow_failure?(types)
22
+ validate(types, expected: expectations.failure, allow_empty: true)
23
+ end
24
+
25
+ private
26
+
27
+ def validate(types, expected:, allow_empty:)
28
+ (allow_empty && types.empty?) || types.any? { |type| expected.type!(type) == result_type }
29
+ end
30
+ end
31
+
32
+ private_constant :TypeChecker
33
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
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
21
+
22
+ expectations.type_and_value!(data)
23
+
24
+ TypeChecker.new(data.type, expectations: expectations)
25
+ end
26
+
27
+ def initialize(subject: nil, success: nil, failure: nil, contract: nil)
28
+ @subject = subject
29
+
30
+ @contract = contract if contract.is_a?(Contract::Evaluator)
31
+
32
+ @contract ||= Contract.new(success: success, failure: failure).freeze
33
+ end
34
+
35
+ def Success(type, value = nil)
36
+ ::BCDD::Result::Success.new(type: type, value: value, subject: subject, expectations: contract)
37
+ end
38
+
39
+ def Failure(type, value = nil)
40
+ ::BCDD::Result::Failure.new(type: type, value: value, subject: subject, expectations: contract)
41
+ end
42
+
43
+ def with(subject:)
44
+ self.class.new(subject: subject, contract: contract)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :subject, :contract
50
+ end
@@ -7,7 +7,7 @@ class BCDD::Result
7
7
  end
8
8
 
9
9
  def failure?(type = nil)
10
- type.nil? || type == self.type
10
+ type.nil? || type_checker.allow_failure?([type])
11
11
  end
12
12
 
13
13
  def value_or
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Handler
5
+ class AllowedTypes
6
+ attr_reader :unchecked, :type_checker
7
+
8
+ def initialize(type_checker)
9
+ @type_checker = type_checker
10
+
11
+ @expectations = type_checker.expectations
12
+
13
+ @unchecked = @expectations.allowed_types.dup
14
+ end
15
+
16
+ def allow?(types)
17
+ check!(types, type_checker.allow?(types))
18
+ end
19
+
20
+ def allow_success?(types)
21
+ unchecked.subtract(@expectations.success.allowed_types) if types.empty?
22
+
23
+ check!(types, type_checker.allow_success?(types))
24
+ end
25
+
26
+ def allow_failure?(types)
27
+ unchecked.subtract(@expectations.failure.allowed_types) if types.empty?
28
+
29
+ check!(types, type_checker.allow_failure?(types))
30
+ end
31
+
32
+ def all_checked?
33
+ unchecked.empty?
34
+ end
35
+
36
+ private
37
+
38
+ def check!(types, checked)
39
+ unchecked.subtract(types) unless all_checked?
40
+
41
+ checked
42
+ end
43
+ end
44
+ end
45
+ end
@@ -2,27 +2,30 @@
2
2
 
3
3
  class BCDD::Result
4
4
  class Handler
5
+ require_relative 'handler/allowed_types'
6
+
5
7
  UNDEFINED = ::Object.new
6
8
 
7
- def initialize(result)
9
+ def initialize(result, type_checker:)
10
+ @allowed_types = AllowedTypes.new(type_checker)
11
+
8
12
  @outcome = UNDEFINED
9
13
 
10
- @_type = result._type
11
14
  @result = result
12
15
  end
13
16
 
14
17
  def [](*types, &block)
15
18
  raise Error::MissingTypeArgument if types.empty?
16
19
 
17
- self.outcome = block if _type.in?(types, allow_empty: false)
20
+ self.outcome = block if allowed_types.allow?(types)
18
21
  end
19
22
 
20
- def failure(*types, &block)
21
- self.outcome = block if result.failure? && _type.in?(types, allow_empty: true)
23
+ def success(*types, &block)
24
+ self.outcome = block if allowed_types.allow_success?(types) && result.success?
22
25
  end
23
26
 
24
- def success(*types, &block)
25
- self.outcome = block if result.success? && _type.in?(types, allow_empty: true)
27
+ def failure(*types, &block)
28
+ self.outcome = block if allowed_types.allow_failure?(types) && result.failure?
26
29
  end
27
30
 
28
31
  def unknown(&block)
@@ -33,7 +36,7 @@ class BCDD::Result
33
36
 
34
37
  private
35
38
 
36
- attr_reader :_type, :result
39
+ attr_reader :result, :allowed_types
37
40
 
38
41
  def outcome?
39
42
  @outcome != UNDEFINED
@@ -44,6 +47,8 @@ class BCDD::Result
44
47
  end
45
48
 
46
49
  def outcome
50
+ allowed_types.all_checked? or raise Error::UnhandledTypes.build(types: allowed_types.unchecked)
51
+
47
52
  @outcome if outcome?
48
53
  end
49
54
  end
@@ -2,12 +2,39 @@
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
7
13
  end
8
14
 
9
- def Failure(type, value = nil)
10
- Failure.new(type: type, value: value, subject: self)
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
11
27
  end
12
28
  end
29
+
30
+ def self.mixin(with: nil)
31
+ addons = Mixin::Addons.options(with)
32
+
33
+ mod = Module.new
34
+ mod.send(:include, Mixin::Methods)
35
+ mod.send(:include, *addons) unless addons.empty?
36
+ mod
37
+ end
38
+
39
+ private_constant :Mixin
13
40
  end
@@ -3,7 +3,7 @@
3
3
  class BCDD::Result
4
4
  class Success < self
5
5
  def success?(type = nil)
6
- type.nil? || type == self.type
6
+ type.nil? || type_checker.allow_success?([type])
7
7
  end
8
8
 
9
9
  def failure?(_type = nil)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BCDD
4
4
  class Result
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
data/lib/bcdd/result.rb CHANGED
@@ -2,32 +2,38 @@
2
2
 
3
3
  require_relative 'result/version'
4
4
  require_relative 'result/error'
5
- require_relative 'result/type'
6
5
  require_relative 'result/data'
7
6
  require_relative 'result/handler'
8
7
  require_relative 'result/failure'
9
8
  require_relative 'result/success'
10
9
  require_relative 'result/mixin'
10
+ require_relative 'result/expectations'
11
11
 
12
12
  class BCDD::Result
13
13
  attr_accessor :unknown
14
14
 
15
- attr_reader :_type, :value, :subject
15
+ attr_reader :subject, :data, :type_checker
16
16
 
17
17
  protected :subject
18
18
 
19
- private :unknown, :unknown=
19
+ private :unknown, :unknown=, :type_checker
20
20
 
21
- def initialize(type:, value:, subject: nil)
22
- @_type = Type.new(type)
23
- @value = value
21
+ def initialize(type:, value:, subject: nil, expectations: nil)
22
+ data = Data.new(name, type, value)
23
+
24
+ @type_checker = Expectations.evaluate(data, expectations)
24
25
  @subject = subject
26
+ @data = data
25
27
 
26
28
  self.unknown = true
27
29
  end
28
30
 
29
31
  def type
30
- _type.to_sym
32
+ data.type
33
+ end
34
+
35
+ def value
36
+ data.value
31
37
  end
32
38
 
33
39
  def success?(_type = nil)
@@ -45,25 +51,27 @@ class BCDD::Result
45
51
  def on(*types, &block)
46
52
  raise Error::MissingTypeArgument if types.empty?
47
53
 
48
- tap { known(block) if _type.in?(types, allow_empty: false) }
54
+ tap { known(block) if type_checker.allow?(types) }
49
55
  end
50
56
 
51
57
  def on_success(*types, &block)
52
- tap { known(block) if success? && _type.in?(types, allow_empty: true) }
58
+ tap { known(block) if type_checker.allow_success?(types) && success? }
53
59
  end
54
60
 
55
61
  def on_failure(*types, &block)
56
- tap { known(block) if failure? && _type.in?(types, allow_empty: true) }
62
+ tap { known(block) if type_checker.allow_failure?(types) && failure? }
57
63
  end
58
64
 
59
65
  def on_unknown
60
66
  tap { yield(value, type) if unknown }
61
67
  end
62
68
 
63
- def and_then(method_name = nil)
69
+ def and_then(method_name = nil, context = nil)
64
70
  return self if failure?
65
71
 
66
- return call_subject_method(method_name) if method_name
72
+ method_name && block_given? and raise ArgumentError, 'method_name and block are mutually exclusive'
73
+
74
+ return call_subject_method(method_name, context) if method_name
67
75
 
68
76
  result = yield(value)
69
77
 
@@ -71,17 +79,13 @@ class BCDD::Result
71
79
  end
72
80
 
73
81
  def handle
74
- handler = Handler.new(self)
82
+ handler = Handler.new(self, type_checker: type_checker)
75
83
 
76
84
  yield handler
77
85
 
78
86
  handler.send(:outcome)
79
87
  end
80
88
 
81
- def data
82
- Data.new(self)
83
- end
84
-
85
89
  def ==(other)
86
90
  self.class == other.class && type == other.type && value == other.value
87
91
  end
@@ -108,7 +112,7 @@ class BCDD::Result
108
112
  private
109
113
 
110
114
  def name
111
- raise Error::NotImplemented
115
+ :unknown
112
116
  end
113
117
 
114
118
  def known(block)
@@ -117,13 +121,14 @@ class BCDD::Result
117
121
  block.call(value, type)
118
122
  end
119
123
 
120
- def call_subject_method(method_name)
124
+ def call_subject_method(method_name, context)
121
125
  method = subject.method(method_name)
122
126
 
123
127
  result =
124
128
  case method.arity
125
129
  when 0 then subject.send(method_name)
126
130
  when 1 then subject.send(method_name, value)
131
+ when 2 then subject.send(method_name, value, context)
127
132
  else raise Error::WrongSubjectMethodArity.build(subject: subject, method: method)
128
133
  end
129
134