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.
- checksums.yaml +4 -4
- data/.rubocop.yml +22 -1
- data/.rubocop_todo.yml +3 -12
- data/CHANGELOG.md +96 -0
- data/README.md +583 -36
- data/lib/bcdd/result/data.rb +33 -0
- data/lib/bcdd/result/error.rb +37 -31
- data/lib/bcdd/result/expectations/contract/disabled.rb +25 -0
- data/lib/bcdd/result/expectations/contract/evaluator.rb +45 -0
- data/lib/bcdd/result/expectations/contract/for_types.rb +29 -0
- data/lib/bcdd/result/expectations/contract/for_types_and_values.rb +37 -0
- data/lib/bcdd/result/expectations/contract/interface.rb +21 -0
- data/lib/bcdd/result/expectations/contract.rb +25 -0
- data/lib/bcdd/result/expectations/error.rb +15 -0
- data/lib/bcdd/result/expectations/type_checker.rb +33 -0
- data/lib/bcdd/result/expectations.rb +62 -0
- data/lib/bcdd/result/failure.rb +6 -2
- data/lib/bcdd/result/handler/allowed_types.rb +45 -0
- data/lib/bcdd/result/handler.rb +20 -11
- data/lib/bcdd/result/mixin.rb +13 -0
- data/lib/bcdd/result/success.rb +6 -2
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +61 -29
- data/lib/result.rb +5 -0
- data/sig/bcdd/result.rbs +224 -43
- metadata +20 -7
- data/lib/bcdd/result/type.rb +0 -17
- data/lib/bcdd/resultable.rb +0 -11
@@ -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
|
data/lib/bcdd/result/error.rb
CHANGED
@@ -1,46 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class BCDD::Result
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
class BCDD::Result::Error < StandardError
|
4
|
+
def self.build(**_kargs)
|
5
|
+
new
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
class NotImplemented < self
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
end
|
23
|
+
new(message)
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/bcdd/result/failure.rb
CHANGED
@@ -7,14 +7,18 @@ class BCDD::Result
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def failure?(type = nil)
|
10
|
-
type.nil? ||
|
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
|
-
|
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
|
data/lib/bcdd/result/handler.rb
CHANGED
@@ -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
|
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
|
28
|
+
self.outcome = block if allowed_types.allow_failure?(types) && result.failure?
|
22
29
|
end
|
23
30
|
|
24
|
-
def
|
25
|
-
self.outcome = block
|
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 :
|
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
|
data/lib/bcdd/result/success.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
class BCDD::Result
|
4
4
|
class Success < self
|
5
5
|
def success?(type = nil)
|
6
|
-
type.nil? ||
|
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
|
-
|
17
|
+
private
|
18
|
+
|
19
|
+
def name
|
20
|
+
:success
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
def self.Success(type, value = nil)
|