bcdd-result 0.6.0 → 0.8.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 +31 -11
- data/CHANGELOG.md +148 -0
- data/README.md +849 -242
- data/Rakefile +9 -3
- data/Steepfile +1 -1
- data/lib/bcdd/result/config/constant_alias.rb +33 -0
- data/lib/bcdd/result/config/options.rb +26 -0
- data/lib/bcdd/result/config/switcher.rb +82 -0
- data/lib/bcdd/result/config.rb +71 -0
- data/lib/bcdd/result/context/expectations/mixin.rb +23 -0
- data/lib/bcdd/result/context/expectations.rb +25 -0
- data/lib/bcdd/result/context/failure.rb +9 -0
- data/lib/bcdd/result/context/mixin.rb +41 -0
- data/lib/bcdd/result/context/success.rb +15 -0
- data/lib/bcdd/result/context.rb +74 -0
- data/lib/bcdd/result/{expectations/contract → contract}/disabled.rb +2 -2
- data/lib/bcdd/result/{expectations → contract}/error.rb +5 -3
- data/lib/bcdd/result/{expectations/contract → contract}/evaluator.rb +2 -2
- data/lib/bcdd/result/{expectations/contract → contract}/for_types.rb +2 -2
- data/lib/bcdd/result/contract/for_types_and_values.rb +44 -0
- data/lib/bcdd/result/contract/interface.rb +21 -0
- data/lib/bcdd/result/{expectations → contract}/type_checker.rb +1 -1
- data/lib/bcdd/result/contract.rb +33 -0
- data/lib/bcdd/result/error.rb +7 -9
- data/lib/bcdd/result/expectations/mixin.rb +19 -12
- data/lib/bcdd/result/expectations.rb +51 -36
- data/lib/bcdd/result/failure/methods.rb +21 -0
- data/lib/bcdd/result/failure.rb +2 -16
- data/lib/bcdd/result/mixin.rb +26 -8
- data/lib/bcdd/result/success/methods.rb +21 -0
- data/lib/bcdd/result/success.rb +2 -16
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +17 -4
- data/lib/bcdd-result.rb +3 -0
- data/sig/bcdd/result.rbs +340 -88
- metadata +27 -16
- data/lib/bcdd/result/expectations/contract/for_types_and_values.rb +0 -37
- data/lib/bcdd/result/expectations/contract/interface.rb +0 -21
- data/lib/bcdd/result/expectations/contract.rb +0 -25
- data/lib/result.rb +0 -5
data/Rakefile
CHANGED
@@ -3,10 +3,16 @@
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rake/testtask'
|
5
5
|
|
6
|
+
Rake::TestTask.new(:test_configuration) do |t|
|
7
|
+
t.libs += %w[lib test]
|
8
|
+
|
9
|
+
t.test_files = FileList.new('test/**/configuration_test.rb')
|
10
|
+
end
|
11
|
+
|
6
12
|
Rake::TestTask.new(:test) do |t|
|
7
|
-
t.libs
|
8
|
-
|
9
|
-
t.test_files = FileList
|
13
|
+
t.libs += %w[lib test]
|
14
|
+
|
15
|
+
t.test_files = FileList.new('test/**/*_test.rb')
|
10
16
|
end
|
11
17
|
|
12
18
|
require 'rubocop/rake_task'
|
data/Steepfile
CHANGED
@@ -10,7 +10,7 @@ target :lib do
|
|
10
10
|
# check 'app/models/**/*.rb' # Glob
|
11
11
|
# ignore 'lib/templates/*.rb'
|
12
12
|
|
13
|
-
|
13
|
+
library 'singleton' # Standard libraries
|
14
14
|
# library 'strong_json' # Gems
|
15
15
|
|
16
16
|
# configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class Config
|
5
|
+
module ConstantAlias
|
6
|
+
RESULT = 'Result'
|
7
|
+
|
8
|
+
OPTIONS = {
|
9
|
+
RESULT => { default: false, affects: %w[Object] }
|
10
|
+
}.transform_values!(&:freeze).freeze
|
11
|
+
|
12
|
+
MAPPING = {
|
13
|
+
RESULT => { target: ::Object, name: :Result, value: ::BCDD::Result }
|
14
|
+
}.transform_values!(&:freeze).freeze
|
15
|
+
|
16
|
+
Listener = ->(option_name, boolean) do
|
17
|
+
mapping = MAPPING.fetch(option_name)
|
18
|
+
|
19
|
+
target, name, value = mapping.fetch_values(:target, :name, :value)
|
20
|
+
|
21
|
+
defined = target.const_defined?(name, false)
|
22
|
+
|
23
|
+
boolean ? defined || target.const_set(name, value) : defined && target.send(:remove_const, name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.switcher
|
27
|
+
Switcher.new(options: OPTIONS, listener: Listener)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private_constant :ConstantAlias
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class Config
|
5
|
+
module Options
|
6
|
+
def self.with_defaults(all_flags, config)
|
7
|
+
all_flags ||= {}
|
8
|
+
|
9
|
+
default_flags = Config.instance.to_h.fetch(config)
|
10
|
+
|
11
|
+
config_flags = all_flags.fetch(config, {})
|
12
|
+
|
13
|
+
default_flags.merge(config_flags).slice(*default_flags.keys)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.filter_map(all_flags, config:, from:)
|
17
|
+
with_defaults(all_flags, config)
|
18
|
+
.filter_map { |name, truthy| from[name] if truthy }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.addon(map:, from:)
|
22
|
+
filter_map(map, config: :addon, from: from)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class Config
|
5
|
+
class Switcher
|
6
|
+
attr_reader :_options, :_affects, :listener
|
7
|
+
|
8
|
+
private :_options, :_affects, :listener
|
9
|
+
|
10
|
+
def initialize(options:, listener: nil)
|
11
|
+
@_options = options.transform_values { _1.fetch(:default) }
|
12
|
+
@_affects = options.transform_values { _1.fetch(:affects) }
|
13
|
+
@listener = listener
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"#<#{self.class.name} options=#{_options.inspect}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
def freeze
|
21
|
+
_options.freeze
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
_options.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
def options
|
30
|
+
_affects.to_h { |name, affects| [name, { enabled: _options[name], affects: affects }] }
|
31
|
+
end
|
32
|
+
|
33
|
+
def enabled?(name)
|
34
|
+
_options[name] || false
|
35
|
+
end
|
36
|
+
|
37
|
+
def enable!(*names)
|
38
|
+
set_many(names, to: true)
|
39
|
+
end
|
40
|
+
|
41
|
+
def disable!(*names)
|
42
|
+
set_many(names, to: false)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def set_many(names, to:)
|
48
|
+
require_option!(names)
|
49
|
+
|
50
|
+
names.each do |name|
|
51
|
+
set_one(name, to)
|
52
|
+
|
53
|
+
listener&.call(name, to)
|
54
|
+
end
|
55
|
+
|
56
|
+
options.slice(*names)
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_one(name, boolean)
|
60
|
+
validate_option!(name)
|
61
|
+
|
62
|
+
_options[name] = boolean
|
63
|
+
end
|
64
|
+
|
65
|
+
def require_option!(names)
|
66
|
+
raise ::ArgumentError, "One or more options required. #{available_options_message}" if names.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_option!(name)
|
70
|
+
return if _options.key?(name)
|
71
|
+
|
72
|
+
raise ::ArgumentError, "Invalid option: #{name.inspect}. #{available_options_message}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def available_options_message
|
76
|
+
"Available options: #{_options.keys.map(&:inspect).join(', ')}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private_constant :Switcher
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
require_relative 'config/options'
|
6
|
+
require_relative 'config/switcher'
|
7
|
+
require_relative 'config/constant_alias'
|
8
|
+
|
9
|
+
class BCDD::Result
|
10
|
+
class Config
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
ADDON = {
|
14
|
+
continue: {
|
15
|
+
default: false,
|
16
|
+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
17
|
+
}
|
18
|
+
}.transform_values!(&:freeze).freeze
|
19
|
+
|
20
|
+
FEATURE = {
|
21
|
+
expectations: {
|
22
|
+
default: true,
|
23
|
+
affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
24
|
+
}
|
25
|
+
}.transform_values!(&:freeze).freeze
|
26
|
+
|
27
|
+
PATTERN_MATCHING = {
|
28
|
+
nil_as_valid_value_checking: {
|
29
|
+
default: false,
|
30
|
+
affects: %w[BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
31
|
+
}
|
32
|
+
}.transform_values!(&:freeze).freeze
|
33
|
+
|
34
|
+
attr_reader :addon, :feature, :constant_alias, :pattern_matching
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@addon = Switcher.new(options: ADDON)
|
38
|
+
@feature = Switcher.new(options: FEATURE)
|
39
|
+
@constant_alias = ConstantAlias.switcher
|
40
|
+
@pattern_matching = Switcher.new(options: PATTERN_MATCHING)
|
41
|
+
end
|
42
|
+
|
43
|
+
def freeze
|
44
|
+
addon.freeze
|
45
|
+
feature.freeze
|
46
|
+
constant_alias.freeze
|
47
|
+
pattern_matching.freeze
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def options
|
53
|
+
{
|
54
|
+
addon: addon,
|
55
|
+
feature: feature,
|
56
|
+
constant_alias: constant_alias,
|
57
|
+
pattern_matching: pattern_matching
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_h
|
62
|
+
options.transform_values(&:to_h)
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
"#<#{self.class.name} options=#{options.keys.sort.inspect}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
private_constant :ADDON, :FEATURE, :PATTERN_MATCHING
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result::Context
|
4
|
+
module Expectations::Mixin
|
5
|
+
Factory = BCDD::Result::Expectations::Mixin::Factory
|
6
|
+
|
7
|
+
METHODS = BCDD::Result::Expectations::Mixin::METHODS
|
8
|
+
|
9
|
+
module Addons
|
10
|
+
module Continuable
|
11
|
+
private def Continue(**value)
|
12
|
+
Success.new(type: :continued, value: value, subject: self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
OPTIONS = { continue: Continuable }.freeze
|
17
|
+
|
18
|
+
def self.options(config_flags)
|
19
|
+
::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result::Context
|
4
|
+
class Expectations < BCDD::Result::Expectations
|
5
|
+
require_relative 'expectations/mixin'
|
6
|
+
|
7
|
+
def self.mixin_module
|
8
|
+
Mixin
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.result_factory_without_expectations
|
12
|
+
::BCDD::Result::Context
|
13
|
+
end
|
14
|
+
|
15
|
+
private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
|
16
|
+
|
17
|
+
def Success(type, **value)
|
18
|
+
Success.new(type: type, value: value, subject: subject, expectations: contract)
|
19
|
+
end
|
20
|
+
|
21
|
+
def Failure(type, **value)
|
22
|
+
Failure.new(type: type, value: value, subject: subject, expectations: contract)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result::Context
|
4
|
+
module Mixin
|
5
|
+
Factory = BCDD::Result::Mixin::Factory
|
6
|
+
|
7
|
+
module Methods
|
8
|
+
def Success(type, **value)
|
9
|
+
Success.new(type: type, value: value, subject: self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def Failure(type, **value)
|
13
|
+
Failure.new(type: type, value: value, subject: self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Addons
|
18
|
+
module Continuable
|
19
|
+
private def Continue(**value)
|
20
|
+
Success.new(type: :continued, value: value, subject: self)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
OPTIONS = { continue: Continuable }.freeze
|
25
|
+
|
26
|
+
def self.options(config_flags)
|
27
|
+
::BCDD::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.mixin_module
|
33
|
+
Mixin
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.result_factory
|
37
|
+
::BCDD::Result::Context
|
38
|
+
end
|
39
|
+
|
40
|
+
private_class_method :mixin_module, :result_factory
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result::Context::Success < BCDD::Result::Context
|
4
|
+
include ::BCDD::Result::Success::Methods
|
5
|
+
|
6
|
+
def and_expose(type, keys)
|
7
|
+
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
|
8
|
+
raise ::ArgumentError, 'keys must be an Array of Symbols'
|
9
|
+
end
|
10
|
+
|
11
|
+
exposed_value = acc.merge(value).slice(*keys)
|
12
|
+
|
13
|
+
self.class.new(type: type, value: exposed_value, subject: subject)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class Context < self
|
5
|
+
require_relative 'context/failure'
|
6
|
+
require_relative 'context/success'
|
7
|
+
require_relative 'context/mixin'
|
8
|
+
require_relative 'context/expectations'
|
9
|
+
|
10
|
+
def self.Success(type, **value)
|
11
|
+
Success.new(type: type, value: value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.Failure(type, **value)
|
15
|
+
Failure.new(type: type, value: value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(type:, value:, subject: nil, expectations: nil)
|
19
|
+
value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
|
20
|
+
|
21
|
+
@acc = {}
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def and_then(method_name = nil, **context_data, &block)
|
27
|
+
super(method_name, context_data, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
attr_reader :acc
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
SubjectMethodArity = ->(method) do
|
37
|
+
return 0 if method.arity.zero?
|
38
|
+
return 1 if method.parameters.map(&:first).all?(/\Akey/)
|
39
|
+
|
40
|
+
-1
|
41
|
+
end
|
42
|
+
|
43
|
+
def call_subject_method(method_name, context)
|
44
|
+
method = subject.method(method_name)
|
45
|
+
|
46
|
+
acc.merge!(value.merge(context))
|
47
|
+
|
48
|
+
result =
|
49
|
+
case SubjectMethodArity[method]
|
50
|
+
when 0 then subject.send(method_name)
|
51
|
+
when 1 then subject.send(method_name, **acc)
|
52
|
+
else raise Error::InvalidSubjectMethodArity.build(subject: subject, method: method, max_arity: 1)
|
53
|
+
end
|
54
|
+
|
55
|
+
ensure_result_object(result, origin: :method)
|
56
|
+
end
|
57
|
+
|
58
|
+
def ensure_result_object(result, origin:)
|
59
|
+
raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)
|
60
|
+
|
61
|
+
return result.tap { _1.acc.merge!(acc) } if result.subject.equal?(subject)
|
62
|
+
|
63
|
+
raise Error::InvalidResultSubject.build(given_result: result, expected_subject: subject)
|
64
|
+
end
|
65
|
+
|
66
|
+
EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
|
67
|
+
|
68
|
+
def raise_unexpected_outcome_error(result, origin)
|
69
|
+
raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
|
70
|
+
end
|
71
|
+
|
72
|
+
private_constant :SubjectMethodArity, :EXPECTED_OUTCOME
|
73
|
+
end
|
74
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class BCDD::Result::
|
3
|
+
class BCDD::Result::Contract::Error < BCDD::Result::Error
|
4
4
|
class UnexpectedType < self
|
5
5
|
def self.build(type:, allowed_types:)
|
6
6
|
new("type :#{type} is not allowed. Allowed types: #{allowed_types.map(&:inspect).join(', ')}")
|
@@ -8,8 +8,10 @@ class BCDD::Result::Expectations::Error < BCDD::Result::Error
|
|
8
8
|
end
|
9
9
|
|
10
10
|
class UnexpectedValue < self
|
11
|
-
def self.build(type:, value:)
|
12
|
-
|
11
|
+
def self.build(type:, value:, cause: nil)
|
12
|
+
cause_message = " (#{cause.message})" if cause
|
13
|
+
|
14
|
+
new("value #{value.inspect} is not allowed for :#{type} type#{cause_message}")
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class BCDD::Result
|
3
|
+
class BCDD::Result
|
4
4
|
class Contract::Evaluator
|
5
5
|
include Contract::Interface
|
6
6
|
|
@@ -25,7 +25,7 @@ class BCDD::Result::Expectations
|
|
25
25
|
def type!(type)
|
26
26
|
return type if type?(type)
|
27
27
|
|
28
|
-
raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
|
28
|
+
raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
|
29
29
|
end
|
30
30
|
|
31
31
|
def type_and_value!(data)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class BCDD::Result
|
3
|
+
class BCDD::Result
|
4
4
|
class Contract::ForTypes
|
5
5
|
include Contract::Interface
|
6
6
|
|
@@ -17,7 +17,7 @@ class BCDD::Result::Expectations
|
|
17
17
|
def type!(type)
|
18
18
|
return type if type?(type)
|
19
19
|
|
20
|
-
raise Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
|
20
|
+
raise Contract::Error::UnexpectedType.build(type: type, allowed_types: allowed_types)
|
21
21
|
end
|
22
22
|
|
23
23
|
def type_and_value!(data)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class Contract::ForTypesAndValues
|
5
|
+
include Contract::Interface
|
6
|
+
|
7
|
+
def initialize(types_and_values, config)
|
8
|
+
@nil_as_valid_value_checking =
|
9
|
+
Config::Options
|
10
|
+
.with_defaults(config, :pattern_matching)
|
11
|
+
.fetch(:nil_as_valid_value_checking)
|
12
|
+
|
13
|
+
@types_and_values = types_and_values.transform_keys(&:to_sym)
|
14
|
+
|
15
|
+
@types_contract = Contract::ForTypes.new(@types_and_values.keys)
|
16
|
+
end
|
17
|
+
|
18
|
+
def allowed_types
|
19
|
+
@types_contract.allowed_types
|
20
|
+
end
|
21
|
+
|
22
|
+
def type?(type)
|
23
|
+
@types_contract.type?(type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def type!(type)
|
27
|
+
@types_contract.type!(type)
|
28
|
+
end
|
29
|
+
|
30
|
+
def type_and_value!(data)
|
31
|
+
type, value = data.type, data.value
|
32
|
+
|
33
|
+
value_checking = @types_and_values[type!(type)]
|
34
|
+
|
35
|
+
checking_result = value_checking === value
|
36
|
+
|
37
|
+
return value if checking_result || (checking_result.nil? && @nil_as_valid_value_checking)
|
38
|
+
|
39
|
+
raise Contract::Error::UnexpectedValue.build(type: type, value: value)
|
40
|
+
rescue ::NoMatchingPatternError => e
|
41
|
+
raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
module Contract::Interface
|
5
|
+
def allowed_types
|
6
|
+
raise Error::NotImplemented
|
7
|
+
end
|
8
|
+
|
9
|
+
def type?(_type)
|
10
|
+
raise Error::NotImplemented
|
11
|
+
end
|
12
|
+
|
13
|
+
def type!(_type)
|
14
|
+
raise Error::NotImplemented
|
15
|
+
end
|
16
|
+
|
17
|
+
def type_and_value!(_data)
|
18
|
+
raise Error::NotImplemented
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BCDD::Result::Contract
|
4
|
+
require_relative 'contract/error'
|
5
|
+
require_relative 'contract/type_checker'
|
6
|
+
require_relative 'contract/interface'
|
7
|
+
require_relative 'contract/evaluator'
|
8
|
+
require_relative 'contract/disabled'
|
9
|
+
require_relative 'contract/for_types'
|
10
|
+
require_relative 'contract/for_types_and_values'
|
11
|
+
|
12
|
+
NONE = Evaluator.new(Disabled, Disabled).freeze
|
13
|
+
|
14
|
+
def self.evaluate(data, contract)
|
15
|
+
contract ||= NONE
|
16
|
+
|
17
|
+
contract.type_and_value!(data)
|
18
|
+
|
19
|
+
TypeChecker.new(data.type, expectations: contract)
|
20
|
+
end
|
21
|
+
|
22
|
+
ToEnsure = ->(spec, config) do
|
23
|
+
return Disabled if spec.nil?
|
24
|
+
|
25
|
+
spec.is_a?(::Hash) ? ForTypesAndValues.new(spec, config) : ForTypes.new(Array(spec))
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.new(success:, failure:, config:)
|
29
|
+
Evaluator.new(ToEnsure[success, config], ToEnsure[failure, config])
|
30
|
+
end
|
31
|
+
|
32
|
+
private_constant :ToEnsure
|
33
|
+
end
|
data/lib/bcdd/result/error.rb
CHANGED
@@ -15,16 +15,14 @@ class BCDD::Result::Error < StandardError
|
|
15
15
|
end
|
16
16
|
|
17
17
|
class UnexpectedOutcome < self
|
18
|
-
def self.build(outcome:, origin:)
|
19
|
-
|
20
|
-
"Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by " \
|
21
|
-
'BCDD::Result::Success or BCDD::Result::Failure'
|
18
|
+
def self.build(outcome:, origin:, expected: nil)
|
19
|
+
expected ||= 'BCDD::Result::Success or BCDD::Result::Failure'
|
22
20
|
|
23
|
-
new(
|
21
|
+
new("Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by #{expected}")
|
24
22
|
end
|
25
23
|
end
|
26
24
|
|
27
|
-
class
|
25
|
+
class InvalidResultSubject < self
|
28
26
|
def self.build(given_result:, expected_subject:)
|
29
27
|
message =
|
30
28
|
"You cannot call #and_then and return a result that does not belong to the subject!\n" \
|
@@ -36,9 +34,9 @@ class BCDD::Result::Error < StandardError
|
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
39
|
-
class
|
40
|
-
def self.build(subject:, method:)
|
41
|
-
new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0
|
37
|
+
class InvalidSubjectMethodArity < self
|
38
|
+
def self.build(subject:, method:, max_arity:)
|
39
|
+
new("#{subject.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0..#{max_arity}")
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|