bcdd-result 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -2
  3. data/CHANGELOG.md +99 -16
  4. data/README.md +618 -247
  5. data/Rakefile +1 -1
  6. data/Steepfile +4 -4
  7. data/examples/multiple_listeners/Rakefile +55 -0
  8. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  9. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  10. data/examples/multiple_listeners/app/models/account.rb +11 -0
  11. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  12. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  13. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  14. data/examples/multiple_listeners/app/models/user.rb +15 -0
  15. data/examples/multiple_listeners/config/boot.rb +16 -0
  16. data/examples/multiple_listeners/config/initializers/bcdd.rb +9 -0
  17. data/examples/multiple_listeners/config.rb +27 -0
  18. data/examples/multiple_listeners/db/setup.rb +60 -0
  19. data/examples/multiple_listeners/lib/bcdd/result/event_logs_record.rb +27 -0
  20. data/examples/multiple_listeners/lib/bcdd/result/rollback_on_failure.rb +15 -0
  21. data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
  22. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  23. data/examples/service_objects/Rakefile +36 -0
  24. data/examples/service_objects/app/models/account/member.rb +10 -0
  25. data/examples/service_objects/app/models/account.rb +11 -0
  26. data/examples/service_objects/app/models/user/token.rb +7 -0
  27. data/examples/service_objects/app/models/user.rb +15 -0
  28. data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
  29. data/examples/service_objects/app/services/application_service.rb +79 -0
  30. data/examples/service_objects/app/services/user/creation.rb +56 -0
  31. data/examples/service_objects/app/services/user/token/creation.rb +37 -0
  32. data/examples/service_objects/config/boot.rb +17 -0
  33. data/examples/service_objects/config/initializers/bcdd.rb +9 -0
  34. data/examples/service_objects/config.rb +20 -0
  35. data/examples/service_objects/db/setup.rb +49 -0
  36. data/examples/single_listener/Rakefile +92 -0
  37. data/examples/single_listener/app/models/account/member.rb +10 -0
  38. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  39. data/examples/single_listener/app/models/account.rb +11 -0
  40. data/examples/single_listener/app/models/user/creation.rb +67 -0
  41. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  42. data/examples/single_listener/app/models/user/token.rb +7 -0
  43. data/examples/single_listener/app/models/user.rb +15 -0
  44. data/examples/single_listener/config/boot.rb +16 -0
  45. data/examples/single_listener/config/initializers/bcdd.rb +9 -0
  46. data/examples/single_listener/config.rb +23 -0
  47. data/examples/single_listener/db/setup.rb +49 -0
  48. data/examples/single_listener/lib/bcdd/result/rollback_on_failure.rb +15 -0
  49. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  50. data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
  51. data/lib/bcdd/{result/context → context}/callable_and_then.rb +5 -4
  52. data/lib/bcdd/{result/context → context}/expectations/mixin.rb +3 -3
  53. data/lib/bcdd/{result/context → context}/expectations.rb +2 -2
  54. data/lib/bcdd/context/failure.rb +9 -0
  55. data/lib/bcdd/{result/context → context}/mixin.rb +4 -4
  56. data/lib/bcdd/context/success.rb +37 -0
  57. data/lib/bcdd/context.rb +91 -0
  58. data/lib/bcdd/failure.rb +23 -0
  59. data/lib/bcdd/result/_self.rb +198 -0
  60. data/lib/bcdd/result/callable_and_then/caller.rb +1 -1
  61. data/lib/bcdd/result/config/switchers/addons.rb +2 -2
  62. data/lib/bcdd/result/config/switchers/constant_aliases.rb +1 -3
  63. data/lib/bcdd/result/config/switchers/features.rb +5 -5
  64. data/lib/bcdd/result/config/switchers/pattern_matching.rb +1 -1
  65. data/lib/bcdd/result/config.rb +9 -2
  66. data/lib/bcdd/result/contract/for_types.rb +1 -1
  67. data/lib/bcdd/result/contract/for_types_and_values.rb +2 -0
  68. data/lib/bcdd/result/contract/type_checker.rb +4 -0
  69. data/lib/bcdd/result/event_logs/config.rb +28 -0
  70. data/lib/bcdd/result/event_logs/listener.rb +51 -0
  71. data/lib/bcdd/result/event_logs/listeners.rb +87 -0
  72. data/lib/bcdd/result/event_logs/tracking/disabled.rb +15 -0
  73. data/lib/bcdd/result/event_logs/tracking/enabled.rb +161 -0
  74. data/lib/bcdd/result/event_logs/tracking.rb +26 -0
  75. data/lib/bcdd/result/{transitions → event_logs}/tree.rb +46 -4
  76. data/lib/bcdd/result/event_logs.rb +27 -0
  77. data/lib/bcdd/result/expectations/mixin.rb +2 -2
  78. data/lib/bcdd/result/failure.rb +1 -3
  79. data/lib/bcdd/result/ignored_types.rb +14 -0
  80. data/lib/bcdd/result/mixin.rb +2 -2
  81. data/lib/bcdd/result/success.rb +1 -3
  82. data/lib/bcdd/result/version.rb +1 -1
  83. data/lib/bcdd/result.rb +25 -191
  84. data/lib/bcdd/success.rb +23 -0
  85. data/sig/bcdd/context.rbs +175 -0
  86. data/sig/bcdd/failure.rbs +13 -0
  87. data/sig/bcdd/result/config.rbs +1 -2
  88. data/sig/bcdd/result/context.rbs +2 -165
  89. data/sig/bcdd/result/contract.rbs +1 -0
  90. data/sig/bcdd/result/event_logs.rbs +189 -0
  91. data/sig/bcdd/result/ignored_types.rbs +9 -0
  92. data/sig/bcdd/result.rbs +14 -32
  93. data/sig/bcdd/success.rbs +13 -0
  94. metadata +75 -22
  95. data/lib/bcdd/result/context/failure.rb +0 -9
  96. data/lib/bcdd/result/context/success.rb +0 -19
  97. data/lib/bcdd/result/context.rb +0 -93
  98. data/lib/bcdd/result/failure/methods.rb +0 -21
  99. data/lib/bcdd/result/success/methods.rb +0 -21
  100. data/lib/bcdd/result/transitions/tracking/disabled.rb +0 -27
  101. data/lib/bcdd/result/transitions/tracking/enabled.rb +0 -100
  102. data/lib/bcdd/result/transitions/tracking.rb +0 -20
  103. data/lib/bcdd/result/transitions.rb +0 -28
  104. data/sig/bcdd/result/transitions.rbs +0 -100
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ $LOAD_PATH.unshift(__dir__)
6
+
7
+ require_relative 'config/boot'
8
+ require_relative 'config/initializers/bcdd'
9
+
10
+ require 'db/setup'
11
+
12
+ require 'lib/bcdd/result/rollback_on_failure'
13
+ require 'lib/single_event_logs_listener'
14
+ require 'lib/runtime_breaker'
15
+
16
+ require 'app/models/account'
17
+ require 'app/models/account/member'
18
+ require 'app/models/user'
19
+ require 'app/models/user/token'
20
+
21
+ require 'app/models/account/owner_creation'
22
+ require 'app/models/user/token/creation'
23
+ require 'app/models/user/creation'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+
5
+ ActiveRecord::Base.establish_connection(
6
+ host: 'localhost',
7
+ adapter: 'sqlite3',
8
+ database: ':memory:'
9
+ )
10
+
11
+ ActiveRecord::Schema.define do
12
+ suppress_messages do
13
+ create_table :accounts do |t|
14
+ t.string :uuid, null: false, index: {unique: true}
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ create_table :users do |t|
20
+ t.string :uuid, null: false, index: {unique: true}
21
+ t.string :name, null: false
22
+ t.string :email, null: false, index: {unique: true}
23
+ t.string :password_digest, null: false
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :user_tokens do |t|
29
+ t.belongs_to :user, null: false, foreign_key: true, index: true
30
+ t.string :access_token, null: false
31
+ t.string :refresh_token, null: false
32
+ t.datetime :access_token_expires_at, null: false
33
+ t.datetime :refresh_token_expires_at, null: false
34
+
35
+ t.timestamps
36
+ end
37
+
38
+ create_table :account_members do |t|
39
+ t.integer :role, null: false, default: 0
40
+ t.belongs_to :user, null: false, foreign_key: true, index: true
41
+ t.belongs_to :account, null: false, foreign_key: true, index: true
42
+
43
+ t.timestamps
44
+
45
+ t.index %i[account_id role], unique: true, where: "(role = 0)"
46
+ t.index %i[account_id user_id], unique: true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD::Result::RollbackOnFailure
4
+ def rollback_on_failure(model: ::ActiveRecord::Base)
5
+ result = nil
6
+
7
+ model.transaction do
8
+ result = yield
9
+
10
+ raise ::ActiveRecord::Rollback if result.failure?
11
+ end
12
+
13
+ result
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuntimeBreaker
4
+ Interruption = Class.new(StandardError)
5
+
6
+ def self.try_to_interrupt(env:)
7
+ return unless String(ENV[env]).strip.start_with?(/1|t/)
8
+
9
+ raise Interruption, "Runtime breaker activated (#{env})"
10
+ end
11
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SingleEventLogsListener
4
+ include BCDD::Result::EventLogs::Listener
5
+
6
+ # A listener will be initialized before the first event log, and it is discarded after the last one.
7
+ def initialize
8
+ @buffer = []
9
+ end
10
+
11
+ # This method will be called before each event log block.
12
+ # The parent event log block will be called first in the case of nested blocks.
13
+ #
14
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
15
+ def on_start(scope:)
16
+ scope => { id:, name:, desc: }
17
+
18
+ @buffer << [id, "##{id} #{name} - #{desc}".chomp('- ')]
19
+ end
20
+
21
+ # This method will wrap all the event_logs in the same block.
22
+ # It can be used to perform an instrumentation (measure/report) of the event_logs.
23
+ #
24
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
25
+ def around_event_logs(scope:)
26
+ yield
27
+ end
28
+
29
+ # This method will wrap each and_then call.
30
+ # It can be used to perform an instrumentation (measure/report) of the and_then calls.
31
+ #
32
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
33
+ # @param and_then:
34
+ # {:type=>:block, :arg=>:some_injected_value}
35
+ # {:type=>:method, :arg=>:some_injected_value, :method_name=>:some_method_name}
36
+ def around_and_then(scope:, and_then:)
37
+ yield
38
+ end
39
+
40
+ # This method will be called after each result recording/tracking.
41
+ #
42
+ # @param record:
43
+ # {
44
+ # :root => {:id=>0, :name=>"RootOperation", :desc=>nil},
45
+ # :parent => {:id=>0, :name=>"RootOperation", :desc=>nil},
46
+ # :current => {:id=>1, :name=>"SomeOperation", :desc=>nil},
47
+ # :result => {:kind=>:success, :type=>:_continue_, :value=>{some: :thing}, :source=><MyProcess:0x0000000102fd6378>},
48
+ # :and_then => {:type=>:method, :arg=>nil, :method_name=>:some_method},
49
+ # :time => 2024-01-26 02:53:11.310431 UTC
50
+ # }
51
+ def on_record(record:)
52
+ record => { current: { id: }, result: { kind:, type: } }
53
+
54
+ method_name = record.dig(:and_then, :method_name)
55
+
56
+ @buffer << [id, " * #{kind}(#{type}) from method: #{method_name}".chomp('from method: ')]
57
+ end
58
+
59
+ MapNestedMessages = ->(event_logs, buffer, hide_given_and_continue) do
60
+ ids_level_parent = event_logs.dig(:metadata, :ids, :level_parent)
61
+
62
+ messages = buffer.filter_map { |(id, msg)| "#{' ' * ids_level_parent[id].first}#{msg}" if ids_level_parent[id] }
63
+
64
+ messages.reject! { _1.match?(/\(_(given|continue)_\)/) } if hide_given_and_continue
65
+
66
+ messages
67
+ end
68
+
69
+ # This method will be called at the end of the event_logs tracking.
70
+ #
71
+ # @param event_logs:
72
+ # {
73
+ # :version => 1,
74
+ # :metadata => {
75
+ # :duration => 0,
76
+ # :trace_id => nil,
77
+ # :ids => {
78
+ # :tree => [0, [[1, []], [2, []]]],
79
+ # :matrix => { 0 => [0, 0], 1 => [1, 1], 2 => [2, 1]},
80
+ # :level_parent => { 0 => [0, 0], 1 => [1, 0], 2 => [1, 0]}
81
+ # }
82
+ # },
83
+ # :records => [
84
+ # # ...
85
+ # ]
86
+ # }
87
+ def on_finish(event_logs:)
88
+ messages = MapNestedMessages[event_logs, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
89
+
90
+ puts messages.join("\n")
91
+ end
92
+
93
+ # This method will be called when an exception is raised during the event_logs tracking.
94
+ #
95
+ # @param exception: Exception
96
+ # @param event_logs: Hash
97
+ def before_interruption(exception:, event_logs:)
98
+ messages = MapNestedMessages[event_logs, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
99
+
100
+ puts messages.join("\n")
101
+
102
+ bc = ::ActiveSupport::BacktraceCleaner.new
103
+ bc.add_filter { |line| line.gsub(__dir__.sub('/lib', ''), '').sub(/\A\//, '')}
104
+ bc.add_silencer { |line| /lib\/bcdd\/result/.match?(line) }
105
+ bc.add_silencer { |line| line.include?(RUBY_VERSION) }
106
+
107
+ dir = "#{FileUtils.pwd[1..]}/"
108
+
109
+ listener_filename = File.basename(__FILE__).chomp('.rb')
110
+
111
+ cb = bc.clean(exception.backtrace)
112
+ cb.each { _1.sub!(dir, '') }
113
+ cb.reject! { _1.match?(/block \(\d levels?\) in|in `block in|internal:kernel|#{listener_filename}/) }
114
+
115
+ puts "\nException:\n #{exception.message} (#{exception.class})\n\nBacktrace:\n #{cb.join("\n ")}"
116
+ end
117
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result
3
+ module BCDD
4
4
  module Context::CallableAndThen
5
- class Caller < CallableAndThen::Caller
5
+ class Caller < Result::CallableAndThen::Caller
6
6
  module KeyArgs
7
7
  def self.parameters?(source)
8
8
  parameters = source.parameters.map(&:first)
@@ -11,7 +11,7 @@ class BCDD::Result
11
11
  end
12
12
 
13
13
  def self.invalid_arity(source, method)
14
- CallableAndThen::Error::InvalidArity.build(source: source, method: method, arity: 'only keyword args')
14
+ Result::CallableAndThen::Error::InvalidArity.build(source: source, method: method, arity: 'only keyword args')
15
15
  end
16
16
  end
17
17
 
@@ -30,7 +30,8 @@ class BCDD::Result
30
30
  def self.ensure_result_object(source, value, result)
31
31
  return result.tap { result.send(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)
32
32
 
33
- raise Error::UnexpectedOutcome.build(outcome: result, origin: source, expected: Context::EXPECTED_OUTCOME)
33
+ raise Result::Error::UnexpectedOutcome.build(outcome: result, origin: source,
34
+ expected: Context::EXPECTED_OUTCOME)
34
35
  end
35
36
 
36
37
  private_class_method :call_proc!, :call_method!
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Context
3
+ class BCDD::Context
4
4
  module Expectations::Mixin
5
5
  Factory = BCDD::Result::Expectations::Mixin::Factory
6
6
 
@@ -9,7 +9,7 @@ class BCDD::Result::Context
9
9
  module Addons
10
10
  module Continue
11
11
  private def Continue(**value)
12
- Success.new(type: :continued, value: value, source: self)
12
+ Success(::BCDD::Result::IgnoredTypes::CONTINUE, **value)
13
13
  end
14
14
  end
15
15
 
@@ -17,7 +17,7 @@ class BCDD::Result::Context
17
17
  private def Given(*values)
18
18
  value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
19
19
 
20
- Success.new(type: :given, value: value, source: self)
20
+ Success(::BCDD::Result::IgnoredTypes::GIVEN, **value)
21
21
  end
22
22
  end
23
23
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Context
3
+ class BCDD::Context
4
4
  class Expectations < BCDD::Result::Expectations
5
5
  require_relative 'expectations/mixin'
6
6
 
@@ -9,7 +9,7 @@ class BCDD::Result::Context
9
9
  end
10
10
 
11
11
  def self.result_factory_without_expectations
12
- ::BCDD::Result::Context
12
+ ::BCDD::Context
13
13
  end
14
14
 
15
15
  private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Context::Failure < BCDD::Context
4
+ include ::BCDD::Failure
5
+
6
+ def and_expose(_type, _keys, **_options)
7
+ self
8
+ end
9
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class BCDD::Result::Context
3
+ class BCDD::Context
4
4
  module Mixin
5
5
  Factory = BCDD::Result::Mixin::Factory
6
6
 
@@ -25,7 +25,7 @@ class BCDD::Result::Context
25
25
  end
26
26
 
27
27
  private def Continue(**value)
28
- _ResultAs(Success, :continued, value)
28
+ _ResultAs(Success, ::BCDD::Result::IgnoredTypes::CONTINUE, value)
29
29
  end
30
30
  end
31
31
 
@@ -33,7 +33,7 @@ class BCDD::Result::Context
33
33
  private def Given(*values)
34
34
  value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
35
35
 
36
- _ResultAs(Success, :given, value)
36
+ _ResultAs(Success, ::BCDD::Result::IgnoredTypes::GIVEN, value)
37
37
  end
38
38
  end
39
39
 
@@ -50,7 +50,7 @@ class BCDD::Result::Context
50
50
  end
51
51
 
52
52
  def self.result_factory
53
- ::BCDD::Result::Context
53
+ ::BCDD::Context
54
54
  end
55
55
 
56
56
  private_class_method :mixin_module, :result_factory
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Context
4
+ class Error < BCDD::Result::Error
5
+ InvalidExposure = ::Class.new(self)
6
+ end
7
+
8
+ class Success < self
9
+ include ::BCDD::Success
10
+
11
+ FetchValues = ->(acc_values, keys) do
12
+ fetched_values = acc_values.fetch_values(*keys)
13
+
14
+ keys.zip(fetched_values).to_h
15
+ rescue ::KeyError => e
16
+ message = "#{e.message}. Available to expose: #{acc_values.keys.map(&:inspect).join(', ')}"
17
+
18
+ raise Error::InvalidExposure, message
19
+ end
20
+
21
+ def and_expose(type, keys, terminal: true)
22
+ unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
23
+ raise ::ArgumentError, 'keys must be an Array of Symbols'
24
+ end
25
+
26
+ EventLogs.tracking.reset_and_then!
27
+
28
+ acc_values = acc.merge(value)
29
+
30
+ value_to_expose = FetchValues.call(acc_values, keys)
31
+
32
+ expectations = type_checker.expectations
33
+
34
+ self.class.new(type: type, value: value_to_expose, source: source, terminal: terminal, expectations: expectations)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Context < BCDD::Result
4
+ require_relative 'context/failure'
5
+ require_relative 'context/success'
6
+ require_relative 'context/mixin'
7
+ require_relative 'context/expectations'
8
+ require_relative 'context/callable_and_then'
9
+
10
+ EXPECTED_OUTCOME = 'BCDD::Context::Success or BCDD::Context::Failure'
11
+
12
+ def self.Success(type, **value)
13
+ Success.new(type: type, value: value)
14
+ end
15
+
16
+ def self.Failure(type, **value)
17
+ Failure.new(type: type, value: value)
18
+ end
19
+
20
+ def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
21
+ value.is_a?(::Hash) or raise ::ArgumentError, 'value must be a Hash'
22
+
23
+ @acc = {}
24
+
25
+ super
26
+ end
27
+
28
+ def and_then(method_name = nil, **injected_value, &block)
29
+ super(method_name, injected_value, &block)
30
+ end
31
+
32
+ def and_then!(source, **injected_value)
33
+ _call = injected_value.delete(:_call)
34
+
35
+ acc.merge!(injected_value)
36
+
37
+ super(source, injected_value, _call: _call)
38
+ end
39
+
40
+ protected
41
+
42
+ attr_reader :acc
43
+
44
+ private
45
+
46
+ SourceMethodArity = ->(method) do
47
+ return 0 if method.arity.zero?
48
+
49
+ parameters = method.parameters.map(&:first)
50
+
51
+ return 1 if !parameters.empty? && parameters.all?(/\Akey/)
52
+
53
+ -1
54
+ end
55
+
56
+ def call_and_then_source_method!(method, injected_value)
57
+ acc.merge!(value.merge(injected_value))
58
+
59
+ case SourceMethodArity[method]
60
+ when 0 then source.send(method.name)
61
+ when 1 then source.send(method.name, **acc)
62
+ else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 1)
63
+ end
64
+ end
65
+
66
+ def call_and_then_block!(block)
67
+ acc.merge!(value)
68
+
69
+ block.call(acc)
70
+ end
71
+
72
+ def call_and_then_callable!(source, value:, injected_value:, method_name:)
73
+ acc.merge!(value.merge(injected_value))
74
+
75
+ CallableAndThen::Caller.call(source, value: acc, injected_value: injected_value, method_name: method_name)
76
+ end
77
+
78
+ def ensure_result_object(result, origin:)
79
+ raise_unexpected_outcome_error(result, origin) unless result.is_a?(BCDD::Context)
80
+
81
+ return result.tap { _1.acc.merge!(acc) } if result.source.equal?(source)
82
+
83
+ raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
84
+ end
85
+
86
+ def raise_unexpected_outcome_error(result, origin)
87
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
88
+ end
89
+
90
+ private_constant :SourceMethodArity
91
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BCDD
4
+ module Failure
5
+ def success?(_type = nil)
6
+ false
7
+ end
8
+
9
+ def failure?(type = nil)
10
+ type.nil? || type_checker.allow_failure?([type])
11
+ end
12
+
13
+ def value_or
14
+ yield(value)
15
+ end
16
+
17
+ private
18
+
19
+ def kind
20
+ :failure
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ attr_accessor :unknown, :event_logs
5
+
6
+ attr_reader :source, :data, :type_checker, :terminal
7
+
8
+ protected :source
9
+
10
+ private :unknown, :unknown=, :type_checker, :event_logs=
11
+
12
+ def self.config
13
+ Config.instance
14
+ end
15
+
16
+ def self.configuration(freeze: true)
17
+ yield(config)
18
+
19
+ freeze and config.freeze
20
+ end
21
+
22
+ def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
23
+ data = Data.new(kind, type, value)
24
+
25
+ @type_checker = Contract.evaluate(data, expectations)
26
+ @source = source
27
+ @terminal = kind == :failure || (terminal && !IgnoredTypes.include?(type))
28
+ @data = data
29
+
30
+ self.unknown = true
31
+ self.event_logs = EventLogs::Tracking::EMPTY
32
+
33
+ EventLogs.tracking.record(self)
34
+ end
35
+
36
+ def terminal?
37
+ terminal
38
+ end
39
+
40
+ def type
41
+ data.type
42
+ end
43
+
44
+ def value
45
+ data.value
46
+ end
47
+
48
+ def type?(arg)
49
+ type_checker.allow!(arg.to_sym) == type
50
+ end
51
+
52
+ def success?(_type = nil)
53
+ raise Error::NotImplemented
54
+ end
55
+
56
+ def failure?(_type = nil)
57
+ raise Error::NotImplemented
58
+ end
59
+
60
+ def value_or(&_block)
61
+ raise Error::NotImplemented
62
+ end
63
+
64
+ def on(*types, &block)
65
+ raise Error::MissingTypeArgument if types.empty?
66
+
67
+ tap { known(block) if type_checker.allow?(types) }
68
+ end
69
+
70
+ def on_success(*types, &block)
71
+ tap { known(block) if type_checker.allow_success?(types) && success? }
72
+ end
73
+
74
+ def on_failure(*types, &block)
75
+ tap { known(block) if type_checker.allow_failure?(types) && failure? }
76
+ end
77
+
78
+ def on_unknown
79
+ tap { yield(value, type) if unknown }
80
+ end
81
+
82
+ def and_then(method_name = nil, injected_value = nil, &block)
83
+ return self if terminal?
84
+
85
+ method_name && block and raise ::ArgumentError, 'method_name and block are mutually exclusive'
86
+
87
+ method_name ? call_and_then_source_method(method_name, injected_value) : call_and_then_block(block)
88
+ end
89
+
90
+ def and_then!(source, injected_value = nil, _call: nil)
91
+ raise Error::CallableAndThenDisabled unless Config.instance.feature.enabled?(:and_then!)
92
+
93
+ return self if terminal?
94
+
95
+ call_and_then_callable!(source, value: value, injected_value: injected_value, method_name: _call)
96
+ end
97
+
98
+ def handle
99
+ handler = Handler.new(self, type_checker: type_checker)
100
+
101
+ yield handler
102
+
103
+ handler.send(:outcome)
104
+ end
105
+
106
+ def ==(other)
107
+ self.class == other.class && type == other.type && value == other.value
108
+ end
109
+
110
+ def hash
111
+ [self.class, type, value].hash
112
+ end
113
+
114
+ def inspect
115
+ format('#<%<class_name>s type=%<type>p value=%<value>p>', class_name: self.class.name, type: type, value: value)
116
+ end
117
+
118
+ def deconstruct
119
+ [type, value]
120
+ end
121
+
122
+ TYPE_AND_VALUE = %i[type value].freeze
123
+
124
+ def deconstruct_keys(keys)
125
+ output = TYPE_AND_VALUE.each_with_object({}) do |key, hash|
126
+ hash[key] = send(key) if keys.include?(key)
127
+ end
128
+
129
+ output.empty? ? value : output
130
+ end
131
+
132
+ def method_missing(name, *args, &block)
133
+ name.end_with?('?') ? is?(name.to_s.chomp('?')) : super
134
+ end
135
+
136
+ def respond_to_missing?(name, include_private = false)
137
+ name.end_with?('?') || super
138
+ end
139
+
140
+ alias is? type?
141
+ alias eql? ==
142
+ alias on_type on
143
+
144
+ private
145
+
146
+ def kind
147
+ :unknown
148
+ end
149
+
150
+ def known(block)
151
+ self.unknown = false
152
+
153
+ block.call(value, type)
154
+ end
155
+
156
+ def call_and_then_source_method(method_name, injected_value)
157
+ method = source.method(method_name)
158
+
159
+ EventLogs.tracking.record_and_then(method, injected_value) do
160
+ result = call_and_then_source_method!(method, injected_value)
161
+
162
+ ensure_result_object(result, origin: :method)
163
+ end
164
+ end
165
+
166
+ def call_and_then_source_method!(method, injected_value)
167
+ case method.arity
168
+ when 0 then source.send(method.name)
169
+ when 1 then source.send(method.name, value)
170
+ when 2 then source.send(method.name, value, injected_value)
171
+ else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 2)
172
+ end
173
+ end
174
+
175
+ def call_and_then_block(block)
176
+ EventLogs.tracking.record_and_then(:block, nil) do
177
+ result = call_and_then_block!(block)
178
+
179
+ ensure_result_object(result, origin: :block)
180
+ end
181
+ end
182
+
183
+ def call_and_then_block!(block)
184
+ block.call(value)
185
+ end
186
+
187
+ def call_and_then_callable!(source, value:, injected_value:, method_name:)
188
+ CallableAndThen::Caller.call(source, value: value, injected_value: injected_value, method_name: method_name)
189
+ end
190
+
191
+ def ensure_result_object(result, origin:)
192
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::BCDD::Result)
193
+
194
+ return result if result.source.equal?(source)
195
+
196
+ raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
197
+ end
198
+ end
@@ -5,7 +5,7 @@ class BCDD::Result
5
5
  def self.call(source, value:, injected_value:, method_name:)
6
6
  method = callable_method(source, method_name)
7
7
 
8
- Transitions.tracking.record_and_then(method, injected_value, source) do
8
+ EventLogs.tracking.record_and_then(method, injected_value) do
9
9
  result =
10
10
  if source.is_a?(::Proc)
11
11
  call_proc!(source, value, injected_value)