solid-result 2.0.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.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +98 -0
  3. data/.rubocop_todo.yml +12 -0
  4. data/CHANGELOG.md +600 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +2691 -0
  8. data/Rakefile +28 -0
  9. data/Steepfile +31 -0
  10. data/examples/multiple_listeners/Rakefile +55 -0
  11. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  12. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  13. data/examples/multiple_listeners/app/models/account.rb +11 -0
  14. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  15. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  16. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  17. data/examples/multiple_listeners/app/models/user.rb +15 -0
  18. data/examples/multiple_listeners/config/boot.rb +16 -0
  19. data/examples/multiple_listeners/config/initializers/solid_result.rb +9 -0
  20. data/examples/multiple_listeners/config.rb +27 -0
  21. data/examples/multiple_listeners/db/setup.rb +60 -0
  22. data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
  23. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  24. data/examples/multiple_listeners/lib/solid/result/event_logs_record.rb +27 -0
  25. data/examples/multiple_listeners/lib/solid/result/rollback_on_failure.rb +15 -0
  26. data/examples/service_objects/Rakefile +36 -0
  27. data/examples/service_objects/app/models/account/member.rb +10 -0
  28. data/examples/service_objects/app/models/account.rb +11 -0
  29. data/examples/service_objects/app/models/user/token.rb +7 -0
  30. data/examples/service_objects/app/models/user.rb +15 -0
  31. data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
  32. data/examples/service_objects/app/services/application_service.rb +79 -0
  33. data/examples/service_objects/app/services/user/creation.rb +56 -0
  34. data/examples/service_objects/app/services/user/token/creation.rb +37 -0
  35. data/examples/service_objects/config/boot.rb +17 -0
  36. data/examples/service_objects/config/initializers/solid_result.rb +9 -0
  37. data/examples/service_objects/config.rb +20 -0
  38. data/examples/service_objects/db/setup.rb +49 -0
  39. data/examples/single_listener/Rakefile +92 -0
  40. data/examples/single_listener/app/models/account/member.rb +10 -0
  41. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  42. data/examples/single_listener/app/models/account.rb +11 -0
  43. data/examples/single_listener/app/models/user/creation.rb +67 -0
  44. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  45. data/examples/single_listener/app/models/user/token.rb +7 -0
  46. data/examples/single_listener/app/models/user.rb +15 -0
  47. data/examples/single_listener/config/boot.rb +16 -0
  48. data/examples/single_listener/config/initializers/solid_result.rb +9 -0
  49. data/examples/single_listener/config.rb +23 -0
  50. data/examples/single_listener/db/setup.rb +49 -0
  51. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  52. data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
  53. data/examples/single_listener/lib/solid/result/rollback_on_failure.rb +15 -0
  54. data/lib/solid/failure.rb +23 -0
  55. data/lib/solid/output/callable_and_then.rb +40 -0
  56. data/lib/solid/output/expectations/mixin.rb +31 -0
  57. data/lib/solid/output/expectations.rb +25 -0
  58. data/lib/solid/output/failure.rb +9 -0
  59. data/lib/solid/output/mixin.rb +57 -0
  60. data/lib/solid/output/success.rb +37 -0
  61. data/lib/solid/output.rb +115 -0
  62. data/lib/solid/result/_self.rb +198 -0
  63. data/lib/solid/result/callable_and_then/caller.rb +49 -0
  64. data/lib/solid/result/callable_and_then/config.rb +15 -0
  65. data/lib/solid/result/callable_and_then/error.rb +11 -0
  66. data/lib/solid/result/callable_and_then.rb +9 -0
  67. data/lib/solid/result/config/options.rb +27 -0
  68. data/lib/solid/result/config/switcher.rb +82 -0
  69. data/lib/solid/result/config/switchers/addons.rb +25 -0
  70. data/lib/solid/result/config/switchers/constant_aliases.rb +33 -0
  71. data/lib/solid/result/config/switchers/features.rb +32 -0
  72. data/lib/solid/result/config/switchers/pattern_matching.rb +20 -0
  73. data/lib/solid/result/config.rb +64 -0
  74. data/lib/solid/result/contract/disabled.rb +25 -0
  75. data/lib/solid/result/contract/error.rb +17 -0
  76. data/lib/solid/result/contract/evaluator.rb +45 -0
  77. data/lib/solid/result/contract/for_types.rb +29 -0
  78. data/lib/solid/result/contract/for_types_and_values.rb +46 -0
  79. data/lib/solid/result/contract/interface.rb +21 -0
  80. data/lib/solid/result/contract/type_checker.rb +37 -0
  81. data/lib/solid/result/contract.rb +33 -0
  82. data/lib/solid/result/data.rb +33 -0
  83. data/lib/solid/result/error.rb +59 -0
  84. data/lib/solid/result/event_logs/config.rb +28 -0
  85. data/lib/solid/result/event_logs/listener.rb +51 -0
  86. data/lib/solid/result/event_logs/listeners.rb +87 -0
  87. data/lib/solid/result/event_logs/tracking/disabled.rb +15 -0
  88. data/lib/solid/result/event_logs/tracking/enabled.rb +161 -0
  89. data/lib/solid/result/event_logs/tracking.rb +26 -0
  90. data/lib/solid/result/event_logs/tree.rb +141 -0
  91. data/lib/solid/result/event_logs.rb +27 -0
  92. data/lib/solid/result/expectations/mixin.rb +58 -0
  93. data/lib/solid/result/expectations.rb +75 -0
  94. data/lib/solid/result/failure.rb +11 -0
  95. data/lib/solid/result/handler/allowed_types.rb +45 -0
  96. data/lib/solid/result/handler.rb +57 -0
  97. data/lib/solid/result/ignored_types.rb +14 -0
  98. data/lib/solid/result/mixin.rb +72 -0
  99. data/lib/solid/result/success.rb +11 -0
  100. data/lib/solid/result/version.rb +7 -0
  101. data/lib/solid/result.rb +27 -0
  102. data/lib/solid/success.rb +23 -0
  103. data/lib/solid-result.rb +3 -0
  104. data/sig/solid/failure.rbs +13 -0
  105. data/sig/solid/output.rbs +175 -0
  106. data/sig/solid/result/callable_and_then.rbs +60 -0
  107. data/sig/solid/result/config.rbs +102 -0
  108. data/sig/solid/result/contract.rbs +120 -0
  109. data/sig/solid/result/data.rbs +16 -0
  110. data/sig/solid/result/error.rbs +34 -0
  111. data/sig/solid/result/event_logs.rbs +189 -0
  112. data/sig/solid/result/expectations.rbs +71 -0
  113. data/sig/solid/result/handler.rbs +47 -0
  114. data/sig/solid/result/ignored_types.rbs +9 -0
  115. data/sig/solid/result/mixin.rbs +45 -0
  116. data/sig/solid/result/version.rbs +5 -0
  117. data/sig/solid/result.rbs +85 -0
  118. data/sig/solid/success.rbs +13 -0
  119. metadata +167 -0
@@ -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/solid_result'
9
+
10
+ require 'db/setup'
11
+
12
+ require 'lib/solid/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,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 Solid::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\/solid\/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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid::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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid
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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid
4
+ module Output::CallableAndThen
5
+ class Caller < Result::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
+ Result::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(:memo).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Output)
32
+
33
+ raise Result::Error::UnexpectedOutcome.build(outcome: result, origin: source,
34
+ expected: Output::EXPECTED_OUTCOME)
35
+ end
36
+
37
+ private_class_method :call_proc!, :call_method!
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output
4
+ module Expectations::Mixin
5
+ Factory = Solid::Result::Expectations::Mixin::Factory
6
+
7
+ Methods = Solid::Result::Expectations::Mixin::Methods
8
+
9
+ module Addons
10
+ module Continue
11
+ private def Continue(**value)
12
+ Success(::Solid::Result::IgnoredTypes::CONTINUE, **value)
13
+ end
14
+ end
15
+
16
+ module Given
17
+ private def Given(*values)
18
+ value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
19
+
20
+ Success(::Solid::Result::IgnoredTypes::GIVEN, **value)
21
+ end
22
+ end
23
+
24
+ OPTIONS = { continue: Continue, given: Given }.freeze
25
+
26
+ def self.options(config_flags)
27
+ ::Solid::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output
4
+ class Expectations < Solid::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
+ ::Solid::Output
13
+ end
14
+
15
+ private_class_method :mixin!, :mixin_module, :result_factory_without_expectations
16
+
17
+ def Success(type, **value)
18
+ _ResultAs(Success, type, value)
19
+ end
20
+
21
+ def Failure(type, **value)
22
+ _ResultAs(Failure, type, value)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output::Failure < Solid::Output
4
+ include ::Solid::Failure
5
+
6
+ def and_expose(_type, _keys, **_options)
7
+ self
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output
4
+ module Mixin
5
+ Factory = Solid::Result::Mixin::Factory
6
+
7
+ module Methods
8
+ def Success(type, **value)
9
+ _ResultAs(Success, type, value)
10
+ end
11
+
12
+ def Failure(type, **value)
13
+ _ResultAs(Failure, type, value)
14
+ end
15
+
16
+ private def _ResultAs(kind_class, type, value, terminal: nil)
17
+ kind_class.new(type: type, value: value, source: self, terminal: terminal)
18
+ end
19
+ end
20
+
21
+ module Addons
22
+ module Continue
23
+ def Success(type, **value)
24
+ _ResultAs(Success, type, value, terminal: true)
25
+ end
26
+
27
+ private def Continue(**value)
28
+ _ResultAs(Success, ::Solid::Result::IgnoredTypes::CONTINUE, value)
29
+ end
30
+ end
31
+
32
+ module Given
33
+ private def Given(*values)
34
+ value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
35
+
36
+ _ResultAs(Success, ::Solid::Result::IgnoredTypes::GIVEN, value)
37
+ end
38
+ end
39
+
40
+ OPTIONS = { continue: Continue, given: Given }.freeze
41
+
42
+ def self.options(config_flags)
43
+ ::Solid::Result::Config::Options.addon(map: config_flags, from: OPTIONS)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.mixin_module
49
+ Mixin
50
+ end
51
+
52
+ def self.result_factory
53
+ ::Solid::Output
54
+ end
55
+
56
+ private_class_method :mixin_module, :result_factory
57
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output
4
+ class Error < Solid::Result::Error
5
+ InvalidExposure = ::Class.new(self)
6
+ end
7
+
8
+ class Success < self
9
+ include ::Solid::Success
10
+
11
+ FetchValues = ->(memo_values, keys) do
12
+ fetched_values = memo_values.fetch_values(*keys)
13
+
14
+ keys.zip(fetched_values).to_h
15
+ rescue ::KeyError => e
16
+ message = "#{e.message}. Available to expose: #{memo_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
+ memo_values = memo.merge(value)
29
+
30
+ value_to_expose = FetchValues.call(memo_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,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Output < Solid::Result
4
+ require_relative 'output/failure'
5
+ require_relative 'output/success'
6
+ require_relative 'output/mixin'
7
+ require_relative 'output/expectations'
8
+ require_relative 'output/callable_and_then'
9
+
10
+ EXPECTED_OUTCOME = 'Solid::Output::Success or Solid::Output::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
+ @memo = {}
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
+ memo.merge!(injected_value)
36
+
37
+ super(source, injected_value, _call: _call)
38
+ end
39
+
40
+ def [](key)
41
+ value[key]
42
+ end
43
+
44
+ def dig(...)
45
+ value.dig(...)
46
+ end
47
+
48
+ def fetch(...)
49
+ value.fetch(...)
50
+ end
51
+
52
+ def slice(...)
53
+ value.slice(...)
54
+ end
55
+
56
+ def values_at(...)
57
+ value.values_at(...)
58
+ end
59
+
60
+ def fetch_values(...)
61
+ value.fetch_values(...)
62
+ end
63
+
64
+ protected
65
+
66
+ attr_reader :memo
67
+
68
+ private
69
+
70
+ SourceMethodArity = ->(method) do
71
+ return 0 if method.arity.zero?
72
+
73
+ parameters = method.parameters.map(&:first)
74
+
75
+ return 1 if !parameters.empty? && parameters.all?(/\Akey/)
76
+
77
+ -1
78
+ end
79
+
80
+ def call_and_then_source_method!(method, injected_value)
81
+ memo.merge!(value.merge(injected_value))
82
+
83
+ case SourceMethodArity[method]
84
+ when 0 then source.send(method.name)
85
+ when 1 then source.send(method.name, **memo)
86
+ else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 1)
87
+ end
88
+ end
89
+
90
+ def call_and_then_block!(block)
91
+ memo.merge!(value)
92
+
93
+ block.call(memo)
94
+ end
95
+
96
+ def call_and_then_callable!(source, value:, injected_value:, method_name:)
97
+ memo.merge!(value.merge(injected_value))
98
+
99
+ CallableAndThen::Caller.call(source, value: memo, injected_value: injected_value, method_name: method_name)
100
+ end
101
+
102
+ def ensure_result_object(result, origin:)
103
+ raise_unexpected_outcome_error(result, origin) unless result.is_a?(Solid::Output)
104
+
105
+ return result.tap { _1.memo.merge!(memo) } if result.source.equal?(source)
106
+
107
+ raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
108
+ end
109
+
110
+ def raise_unexpected_outcome_error(result, origin)
111
+ raise Error::UnexpectedOutcome.build(outcome: result, origin: origin, expected: EXPECTED_OUTCOME)
112
+ end
113
+
114
+ private_constant :SourceMethodArity
115
+ end