bcdd-result 0.6.0 → 0.8.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 +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
|
|