Skip to content

Commit

Permalink
[WIP] Add BCDD::Result#and_then!
Browse files Browse the repository at this point in the history
  • Loading branch information
serradura committed Jan 6, 2024
1 parent 6817fe8 commit 5b11342
Show file tree
Hide file tree
Showing 34 changed files with 1,347 additions and 68 deletions.
21 changes: 18 additions & 3 deletions lib/bcdd/result.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# frozen_string_literal: true

require 'singleton'

require_relative 'result/version'
require_relative 'result/transitions'
require_relative 'result/error'
require_relative 'result/transitions'
require_relative 'result/callable_and_then'
require_relative 'result/data'
require_relative 'result/handler'
require_relative 'result/failure'
Expand Down Expand Up @@ -88,12 +91,20 @@ def on_unknown
tap { yield(value, type) if unknown }
end

def and_then(method_name = nil, context = nil, &block)
def and_then(method_name = nil, injected_value = nil, &block)
return self if terminal?

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

method_name ? call_and_then_source_method(method_name, context) : call_and_then_block(block)
method_name ? call_and_then_source_method(method_name, injected_value) : call_and_then_block(block)
end

def and_then!(source, injected_value = nil, _call: nil)
raise Error::CallableAndThenDisabled unless Config.instance.feature.enabled?(:and_then!)

return self if terminal?

call_and_then_callable!(source, value: value, injected_value: injected_value, method_name: _call)
end

def handle
Expand Down Expand Up @@ -170,6 +181,10 @@ def call_and_then_block!(block)
block.call(value)
end

def call_and_then_callable!(source, value:, injected_value:, method_name:)
CallableAndThen::Caller.call(source, value: value, injected_value: injected_value, method_name: method_name)
end

def ensure_result_object(result, origin:)
raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::BCDD::Result)

Expand Down
9 changes: 9 additions & 0 deletions lib/bcdd/result/callable_and_then.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class BCDD::Result
module CallableAndThen
require_relative 'callable_and_then/error'
require_relative 'callable_and_then/config'
require_relative 'callable_and_then/caller'
end
end
49 changes: 49 additions & 0 deletions lib/bcdd/result/callable_and_then/caller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

class BCDD::Result
class CallableAndThen::Caller
def self.call(source, value:, injected_value:, method_name:)
method = callable_method(source, method_name)

Transitions.tracking.record_and_then(method, injected_value, source) do
result =
if source.is_a?(::Proc)
call_proc!(source, value, injected_value)
else
call_method!(source, method, value, injected_value)
end

ensure_result_object(source, value, result)
end
end

def self.call_proc!(source, value, injected_value)
case source.arity
when 1 then source.call(value)
when 2 then source.call(value, injected_value)
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: :call, arity: '1..2')
end
end

def self.call_method!(source, method, value, injected_value)
case method.arity
when 1 then source.send(method.name, value)
when 2 then source.send(method.name, value, injected_value)
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: method.name, arity: '1..2')
end
end

def self.callable_method(source, method_name)
source.method(method_name || Config.instance.and_then!.default_method_name_to_call)
end

def self.ensure_result_object(source, _value, result)
return result if result.is_a?(::BCDD::Result)

raise Error::UnexpectedOutcome.build(outcome: result, origin: source)
end

private_class_method :new, :allocate
private_class_method :call_proc!, :call_method!, :callable_method, :ensure_result_object
end
end
15 changes: 15 additions & 0 deletions lib/bcdd/result/callable_and_then/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class BCDD::Result
class CallableAndThen::Config
attr_accessor :default_method_name_to_call

def initialize
self.default_method_name_to_call = :call
end

def options
{ default_method_name_to_call: default_method_name_to_call }
end
end
end
11 changes: 11 additions & 0 deletions lib/bcdd/result/callable_and_then/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class BCDD::Result
class CallableAndThen::Error < Error
class InvalidArity < self
def self.build(source:, method:, arity:)
new("Invalid arity for #{source.class}##{method} method. Expected arity: #{arity}")
end
end
end
end
12 changes: 9 additions & 3 deletions lib/bcdd/result/config.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require 'singleton'

require_relative 'config/options'
require_relative 'config/switcher'
require_relative 'config/switchers/addons'
Expand All @@ -20,13 +18,19 @@ def initialize
@feature = Features.switcher
@constant_alias = ConstantAliases.switcher
@pattern_matching = PatternMatching.switcher
@and_then_ = CallableAndThen::Config.new
end

def and_then!
@and_then_
end

def freeze
addon.freeze
feature.freeze
constant_alias.freeze
pattern_matching.freeze
and_then!.freeze

super
end
Expand All @@ -45,7 +49,9 @@ def to_h
end

def inspect
"#<#{self.class.name} options=#{options.keys.sort.inspect}>"
"#<#{self.class.name} " \
"options=#{options.keys.sort.inspect} " \
"and_then!=#{and_then!.options.inspect}>"
end
end
end
6 changes: 5 additions & 1 deletion lib/bcdd/result/config/switchers/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ module Features
},
transitions: {
default: true,
affects: %w[BCDD::Result BCDD::Result::Context]
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
},
and_then!: {
default: false,
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
}
}.transform_values!(&:freeze).freeze

Expand Down
26 changes: 22 additions & 4 deletions lib/bcdd/result/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class Context < self
require_relative 'context/success'
require_relative 'context/mixin'
require_relative 'context/expectations'
require_relative 'context/callable_and_then'

EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'

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

def and_then!(source, **injected_value)
_call = injected_value.delete(:_call)

acc.merge!(injected_value)

super(source, injected_value, _call: _call)
end

protected

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

SourceMethodArity = ->(method) do
return 0 if method.arity.zero?
return 1 if method.parameters.map(&:first).all?(/\Akey/)

parameters = method.parameters.map(&:first)

return 1 if !parameters.empty? && parameters.all?(/\Akey/)

-1
end
Expand All @@ -56,6 +70,12 @@ def call_and_then_block!(block)
block.call(acc)
end

def call_and_then_callable!(source, value:, injected_value:, method_name:)
acc.merge!(value.merge(injected_value))

CallableAndThen::Caller.call(source, value: acc, injected_value: injected_value, method_name: method_name)
end

def ensure_result_object(result, origin:)
raise_unexpected_outcome_error(result, origin) unless result.is_a?(Context)

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

EXPECTED_OUTCOME = 'BCDD::Result::Context::Success or BCDD::Result::Context::Failure'

def raise_unexpected_outcome_error(result, origin)
raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
end

private_constant :SourceMethodArity, :EXPECTED_OUTCOME
private_constant :SourceMethodArity
end
end
39 changes: 39 additions & 0 deletions lib/bcdd/result/context/callable_and_then.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

class BCDD::Result
module Context::CallableAndThen
class Caller < CallableAndThen::Caller
module KeyArgs
def self.parameters?(source)
parameters = source.parameters.map(&:first)

!parameters.empty? && parameters.all?(/\Akey/)
end

def self.invalid_arity(source, method)
CallableAndThen::Error::InvalidArity.build(source: source, method: method, arity: 'only keyword args')
end
end

def self.call_proc!(source, value, _injected_value)
return source.call(**value) if KeyArgs.parameters?(source)

raise KeyArgs.invalid_arity(source, :call)
end

def self.call_method!(source, method, value, _injected_value)
return source.send(method.name, **value) if KeyArgs.parameters?(method)

raise KeyArgs.invalid_arity(source, method.name)
end

def self.ensure_result_object(source, value, result)
return result.tap { result.send(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)

raise Error::UnexpectedOutcome.build(outcome: result, origin: source, expected: Context::EXPECTED_OUTCOME)
end

private_class_method :call_proc!, :call_method!
end
end
end
20 changes: 12 additions & 8 deletions lib/bcdd/result/context/success.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# frozen_string_literal: true

class BCDD::Result::Context::Success < BCDD::Result::Context
include ::BCDD::Result::Success::Methods
class BCDD::Result
class Context::Success < Context
include ::BCDD::Result::Success::Methods

def and_expose(type, keys, terminal: true)
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
raise ::ArgumentError, 'keys must be an Array of Symbols'
end
def and_expose(type, keys, terminal: true)
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
raise ::ArgumentError, 'keys must be an Array of Symbols'
end

Transitions.tracking.reset_and_then!

exposed_value = acc.merge(value).slice(*keys)
exposed_value = acc.merge(value).slice(*keys)

self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
self.class.new(type: type, value: exposed_value, source: source, terminal: terminal)
end
end
end
11 changes: 10 additions & 1 deletion lib/bcdd/result/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class NotImplemented < self
end

class MissingTypeArgument < self
def initialize(_arg = nil)
def initialize(_message = nil)
super('A type (argument) is required to invoke the #on/#on_type method')
end
end
Expand Down Expand Up @@ -47,4 +47,13 @@ def self.build(types:)
new("You must handle all cases. #{source} not handled: #{types.map(&:inspect).join(', ')}")
end
end

class CallableAndThenDisabled < self
def initialize(_message = nil)
super(
'You cannot use #and_then! as the feature is disabled. ' \
'Please use BCDD::Result.config.feature.enable!(:and_then!) to enable it.'
)
end
end
end
18 changes: 8 additions & 10 deletions lib/bcdd/result/transitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@ module Transitions

THREAD_VAR_NAME = :bcdd_result_transitions_tracking

EnsureResult = ->(result) do
return result if result.is_a?(::BCDD::Result)

raise Error::UnexpectedOutcome.build(outcome: result, origin: :transitions)
end

def self.tracking
Thread.current[THREAD_VAR_NAME] ||= Tracking.instance
end
end

def self.transitions(name: nil, desc: nil)
Transitions.tracking.start(name: name, desc: desc)

result = yield

result.is_a?(::BCDD::Result) or raise Error::UnexpectedOutcome.build(outcome: result, origin: :transitions)

Transitions.tracking.finish(result: result)

result
def self.transitions(name: nil, desc: nil, &block)
Transitions.tracking.exec(name, desc, &block)
rescue ::Exception => e
Transitions.tracking.reset!

Expand Down
Loading

0 comments on commit 5b11342

Please sign in to comment.