bcdd-result 0.3.0 → 0.5.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.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ class Data
5
+ attr_reader :name, :type, :value
6
+
7
+ def initialize(name, type, value)
8
+ @name = name
9
+ @type = type.to_sym
10
+ @value = value
11
+ end
12
+
13
+ def to_h
14
+ { name: name, type: type, value: value }
15
+ end
16
+
17
+ def to_a
18
+ [name, type, value]
19
+ end
20
+
21
+ def inspect
22
+ format(
23
+ '#<%<class_name>s name=%<name>p type=%<type>p value=%<value>p>',
24
+ class_name: self.class.name, name: name, type: type, value: value
25
+ )
26
+ end
27
+
28
+ alias to_ary to_a
29
+ alias to_hash to_h
30
+ end
31
+
32
+ private_constant :Data
33
+ end
@@ -1,46 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result
4
- class Error < ::StandardError
5
- def self.build(**_kargs)
6
- new
7
- end
3
+ class BCDD::Result::Error < StandardError
4
+ def self.build(**_kargs)
5
+ new
6
+ end
8
7
 
9
- class NotImplemented < self
10
- end
8
+ class NotImplemented < self
9
+ end
11
10
 
12
- class MissingTypeArgument < self
13
- def initialize(_arg = nil)
14
- super('A type (argument) is required to invoke the #on/#on_type method')
15
- end
11
+ class MissingTypeArgument < self
12
+ def initialize(_arg = nil)
13
+ super('A type (argument) is required to invoke the #on/#on_type method')
16
14
  end
15
+ end
17
16
 
18
- class UnexpectedOutcome < self
19
- def self.build(outcome:, origin:)
20
- message =
21
- "Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by " \
22
- 'BCDD::Result::Success or BCDD::Result::Failure'
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'
23
22
 
24
- new(message)
25
- end
23
+ new(message)
26
24
  end
25
+ end
27
26
 
28
- class WrongResultSubject < self
29
- def self.build(given_result:, expected_subject:)
30
- message =
31
- "You cannot call #and_then and return a result that does not belong to the subject!\n" \
32
- "Expected subject: #{expected_subject.inspect}\n" \
33
- "Given subject: #{given_result.send(:subject).inspect}\n" \
34
- "Given result: #{given_result.inspect}"
27
+ class WrongResultSubject < self
28
+ def self.build(given_result:, expected_subject:)
29
+ message =
30
+ "You cannot call #and_then and return a result that does not belong to the subject!\n" \
31
+ "Expected subject: #{expected_subject.inspect}\n" \
32
+ "Given subject: #{given_result.send(:subject).inspect}\n" \
33
+ "Given result: #{given_result.inspect}"
35
34
 
36
- new(message)
37
- end
35
+ new(message)
38
36
  end
37
+ end
38
+
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.")
42
+ end
43
+ end
44
+
45
+ class UnhandledTypes < self
46
+ def self.build(types:)
47
+ subject = types.size == 1 ? 'This was' : 'These were'
39
48
 
40
- class WrongSubjectMethodArity < self
41
- def self.build(subject:, method:)
42
- new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0 or 1.")
43
- end
49
+ new("You must handle all cases. #{subject} not handled: #{types.map(&:inspect).join(', ')}")
44
50
  end
45
51
  end
46
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,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,62 @@
1
+ # frozen_string_literal: true
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(...)
11
+ end
12
+
13
+ def Failure(...)
14
+ _expected_result::Failure(...)
15
+ end
16
+
17
+ private
18
+
19
+ def _expected_result
20
+ @_expected_result ||= Expected.with(subject: self)
21
+ 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
+
42
+ @contract = contract if contract.is_a?(Contract::Evaluator)
43
+
44
+ @contract ||= Contract.new(success: success, failure: failure).freeze
45
+ end
46
+
47
+ def Success(type, value = nil)
48
+ ::BCDD::Result::Success.new(type: type, value: value, subject: subject, expectations: contract)
49
+ end
50
+
51
+ def Failure(type, value = nil)
52
+ ::BCDD::Result::Failure.new(type: type, value: value, subject: subject, expectations: contract)
53
+ end
54
+
55
+ def with(subject:)
56
+ self.class.new(subject: subject, contract: contract)
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :subject, :contract
62
+ end
@@ -7,14 +7,18 @@ 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
14
14
  yield
15
15
  end
16
16
 
17
- alias data_or value_or
17
+ private
18
+
19
+ def name
20
+ :failure
21
+ end
18
22
  end
19
23
 
20
24
  def self.Failure(type, value = nil)
@@ -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,46 +2,55 @@
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)
21
+ end
22
+
23
+ def success(*types, &block)
24
+ self.outcome = block if allowed_types.allow_success?(types) && result.success?
18
25
  end
19
26
 
20
27
  def failure(*types, &block)
21
- self.outcome = block if result.failure? && _type.in?(types, allow_empty: true)
28
+ self.outcome = block if allowed_types.allow_failure?(types) && result.failure?
22
29
  end
23
30
 
24
- def success(*types, &block)
25
- self.outcome = block if result.success? && _type.in?(types, allow_empty: true)
31
+ def unknown(&block)
32
+ self.outcome = block unless outcome?
26
33
  end
27
34
 
28
35
  alias type []
29
36
 
30
37
  private
31
38
 
32
- attr_reader :_type, :result
39
+ attr_reader :result, :allowed_types
33
40
 
34
41
  def outcome?
35
42
  @outcome != UNDEFINED
36
43
  end
37
44
 
38
- def outcome
39
- @outcome if outcome?
40
- end
41
-
42
45
  def outcome=(block)
43
46
  @outcome = block.call(result.value, result.type) unless outcome?
44
47
  end
48
+
49
+ def outcome
50
+ allowed_types.all_checked? or raise Error::UnhandledTypes.build(types: allowed_types.unchecked)
51
+
52
+ @outcome if outcome?
53
+ end
45
54
  end
46
55
 
47
56
  private_constant :Handler
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module Mixin
5
+ def Success(type, value = nil)
6
+ Success.new(type: type, value: value, subject: self)
7
+ end
8
+
9
+ def Failure(type, value = nil)
10
+ Failure.new(type: type, value: value, subject: self)
11
+ end
12
+ end
13
+ 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)
@@ -14,7 +14,11 @@ class BCDD::Result
14
14
  value
15
15
  end
16
16
 
17
- alias data_or value_or
17
+ private
18
+
19
+ def name
20
+ :success
21
+ end
18
22
  end
19
23
 
20
24
  def self.Success(type, value = nil)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BCDD
4
4
  class Result
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end