Skip to content

Commit 7f4f317

Browse files
committed
[WIP] Add BCDD::Result#and_then!
1 parent 6817fe8 commit 7f4f317

34 files changed

+1347
-68
lines changed

lib/bcdd/result.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# frozen_string_literal: true
22

3+
require 'singleton'
4+
35
require_relative 'result/version'
4-
require_relative 'result/transitions'
56
require_relative 'result/error'
7+
require_relative 'result/transitions'
8+
require_relative 'result/callable_and_then'
69
require_relative 'result/data'
710
require_relative 'result/handler'
811
require_relative 'result/failure'
@@ -88,12 +91,20 @@ def on_unknown
8891
tap { yield(value, type) if unknown }
8992
end
9093

91-
def and_then(method_name = nil, context = nil, &block)
94+
def and_then(method_name = nil, injected_value = nil, &block)
9295
return self if terminal?
9396

9497
method_name && block and raise ::ArgumentError, 'method_name and block are mutually exclusive'
9598

96-
method_name ? call_and_then_source_method(method_name, context) : call_and_then_block(block)
99+
method_name ? call_and_then_source_method(method_name, injected_value) : call_and_then_block(block)
100+
end
101+
102+
def and_then!(source, injected_value = nil, _call: nil)
103+
raise Error::CallableAndThenDisabled unless Config.instance.feature.enabled?(:and_then!)
104+
105+
return self if terminal?
106+
107+
call_and_then_callable!(source, value: value, injected_value: injected_value, method_name: _call)
97108
end
98109

99110
def handle
@@ -170,6 +181,10 @@ def call_and_then_block!(block)
170181
block.call(value)
171182
end
172183

184+
def call_and_then_callable!(source, value:, injected_value:, method_name:)
185+
CallableAndThen::Caller.call(source, value: value, injected_value: injected_value, method_name: method_name)
186+
end
187+
173188
def ensure_result_object(result, origin:)
174189
raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::BCDD::Result)
175190

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class BCDD::Result
4+
module CallableAndThen
5+
require_relative 'callable_and_then/error'
6+
require_relative 'callable_and_then/config'
7+
require_relative 'callable_and_then/caller'
8+
end
9+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
class BCDD::Result
4+
class CallableAndThen::Caller
5+
def self.call(source, value:, injected_value:, method_name:)
6+
method = callable_method(source, method_name)
7+
8+
Transitions.tracking.record_and_then(method, injected_value, source) do
9+
result =
10+
if source.is_a?(::Proc)
11+
call_proc!(source, value, injected_value)
12+
else
13+
call_method!(source, method, value, injected_value)
14+
end
15+
16+
ensure_result_object(source, value, result)
17+
end
18+
end
19+
20+
def self.call_proc!(source, value, injected_value)
21+
case source.arity
22+
when 1 then source.call(value)
23+
when 2 then source.call(value, injected_value)
24+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: :call, arity: '1..2')
25+
end
26+
end
27+
28+
def self.call_method!(source, method, value, injected_value)
29+
case method.arity
30+
when 1 then source.send(method.name, value)
31+
when 2 then source.send(method.name, value, injected_value)
32+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: method.name, arity: '1..2')
33+
end
34+
end
35+
36+
def self.callable_method(source, method_name)
37+
source.method(method_name || Config.instance.and_then!.default_method_name_to_call)
38+
end
39+
40+
def self.ensure_result_object(source, _value, result)
41+
return result if result.is_a?(::BCDD::Result)
42+
43+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source)
44+
end
45+
46+
private_class_method :new, :allocate
47+
private_class_method :call_proc!, :call_method!, :callable_method, :ensure_result_object
48+
end
49+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
class BCDD::Result
4+
class CallableAndThen::Config
5+
attr_accessor :default_method_name_to_call
6+
7+
def initialize
8+
self.default_method_name_to_call = :call
9+
end
10+
11+
def options
12+
{ default_method_name_to_call: default_method_name_to_call }
13+
end
14+
end
15+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class BCDD::Result
4+
class CallableAndThen::Error < Error
5+
class InvalidArity < self
6+
def self.build(source:, method:, arity:)
7+
new("Invalid arity for #{source.class}##{method} method. Expected arity: #{arity}")
8+
end
9+
end
10+
end
11+
end

lib/bcdd/result/config.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'singleton'
4-
53
require_relative 'config/options'
64
require_relative 'config/switcher'
75
require_relative 'config/switchers/addons'
@@ -20,13 +18,19 @@ def initialize
2018
@feature = Features.switcher
2119
@constant_alias = ConstantAliases.switcher
2220
@pattern_matching = PatternMatching.switcher
21+
@and_then_ = CallableAndThen::Config.new
22+
end
23+
24+
def and_then!
25+
@and_then_
2326
end
2427

2528
def freeze
2629
addon.freeze
2730
feature.freeze
2831
constant_alias.freeze
2932
pattern_matching.freeze
33+
and_then!.freeze
3034

3135
super
3236
end
@@ -45,7 +49,9 @@ def to_h
4549
end
4650

4751
def inspect
48-
"#<#{self.class.name} options=#{options.keys.sort.inspect}>"
52+
"#<#{self.class.name} " \
53+
"options=#{options.keys.sort.inspect} " \
54+
"and_then!=#{and_then!.options.inspect}>"
4955
end
5056
end
5157
end

lib/bcdd/result/config/switchers/features.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ module Features
1010
},
1111
transitions: {
1212
default: true,
13-
affects: %w[BCDD::Result BCDD::Result::Context]
13+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
14+
},
15+
and_then!: {
16+
default: false,
17+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
1418
}
1519
}.transform_values!(&:freeze).freeze
1620

lib/bcdd/result/context.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ class Context < self
66
require_relative 'context/success'
77
require_relative 'context/mixin'
88
require_relative 'context/expectations'
9+
require_relative 'context/callable_and_then'
10+
11+
EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
912

1013
def self.Success(type, **value)
1114
Success.new(type: type, value: value)
@@ -27,6 +30,14 @@ def and_then(method_name = nil, **injected_value, &block)
2730
super(method_name, injected_value, &block)
2831
end
2932

33+
def and_then!(source, **injected_value)
34+
_call = injected_value.delete(:_call)
35+
36+
acc.merge!(injected_value)
37+
38+
super(source, injected_value, _call: _call)
39+
end
40+
3041
protected
3142

3243
attr_reader :acc
@@ -35,7 +46,10 @@ def and_then(method_name = nil, **injected_value, &block)
3546

3647
SourceMethodArity = ->(method) do
3748
return 0 if method.arity.zero?
38-
return 1 if method.parameters.map(&:first).all?(/\Akey/)
49+
50+
parameters = method.parameters.map(&:first)
51+
52+
return 1 if !parameters.empty? && parameters.all?(/\Akey/)
3953

4054
-1
4155
end
@@ -56,6 +70,12 @@ def call_and_then_block!(block)
5670
block.call(acc)
5771
end
5872

73+
def call_and_then_callable!(source, value:, injected_value:, method_name:)
74+
acc.merge!(value.merge(injected_value))
75+
76+
CallableAndThen::Caller.call(source, value: acc, injected_value: injected_value, method_name: method_name)
77+
end
78+
5979
def ensure_result_object(result, origin:)
6080
raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)
6181

@@ -64,12 +84,10 @@ def ensure_result_object(result, origin:)
6484
raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
6585
end
6686

67-
EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'
68-
6987
def raise_unexpected_outcome_error(result, origin)
7088
raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
7189
end
7290

73-
private_constant :SourceMethodArity, :EXPECTED_OUTCOME
91+
private_constant :SourceMethodArity
7492
end
7593
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
class BCDD::Result
4+
module Context::CallableAndThen
5+
class Caller < CallableAndThen::Caller
6+
module KeyArgs
7+
def self.parameters?(source)
8+
parameters = source.parameters.map(&:first)
9+
10+
!parameters.empty? && parameters.all?(/\Akey/)
11+
end
12+
13+
def self.invalid_arity(source, method)
14+
CallableAndThen::Error::InvalidArity.build(source: source, method: method, arity: 'only keyword args')
15+
end
16+
end
17+
18+
def self.call_proc!(source, value, _injected_value)
19+
return source.call(**value) if KeyArgs.parameters?(source)
20+
21+
raise KeyArgs.invalid_arity(source, :call)
22+
end
23+
24+
def self.call_method!(source, method, value, _injected_value)
25+
return source.send(method.name, **value) if KeyArgs.parameters?(method)
26+
27+
raise KeyArgs.invalid_arity(source, method.name)
28+
end
29+
30+
def self.ensure_result_object(source, value, result)
31+
return result.tap { result.send(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)
32+
33+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source, expected: Context::EXPECTED_OUTCOME)
34+
end
35+
36+
private_class_method :call_proc!, :call_method!
37+
end
38+
end
39+
end

lib/bcdd/result/context/success.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
# frozen_string_literal: true
22

3-
class BCDD::Result::Context::Success < BCDD::Result::Context
4-
include ::BCDD::Result::Success::Methods
3+
class BCDD::Result
4+
class Context::Success < Context
5+
include ::BCDD::Result::Success::Methods
56

6-
def and_expose(type, keys, terminal: true)
7-
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
8-
raise ::ArgumentError, 'keys must be an Array of Symbols'
9-
end
7+
def and_expose(type, keys, terminal: true)
8+
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
9+
raise ::ArgumentError, 'keys must be an Array of Symbols'
10+
end
11+
12+
Transitions.tracking.reset_and_then!
1013

11-
exposed_value = acc.merge(value).slice(*keys)
14+
exposed_value = acc.merge(value).slice(*keys)
1215

13-
self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
16+
self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
17+
end
1418
end
1519
end

0 commit comments

Comments
 (0)