bcdd-result 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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