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.
- 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)
|