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.
- checksums.yaml +7 -0
- data/.rubocop.yml +98 -0
- data/.rubocop_todo.yml +12 -0
- data/CHANGELOG.md +600 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +2691 -0
- data/Rakefile +28 -0
- data/Steepfile +31 -0
- data/examples/multiple_listeners/Rakefile +55 -0
- data/examples/multiple_listeners/app/models/account/member.rb +10 -0
- data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
- data/examples/multiple_listeners/app/models/account.rb +11 -0
- data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
- data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
- data/examples/multiple_listeners/app/models/user/token.rb +7 -0
- data/examples/multiple_listeners/app/models/user.rb +15 -0
- data/examples/multiple_listeners/config/boot.rb +16 -0
- data/examples/multiple_listeners/config/initializers/solid_result.rb +9 -0
- data/examples/multiple_listeners/config.rb +27 -0
- data/examples/multiple_listeners/db/setup.rb +60 -0
- data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
- data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
- data/examples/multiple_listeners/lib/solid/result/event_logs_record.rb +27 -0
- data/examples/multiple_listeners/lib/solid/result/rollback_on_failure.rb +15 -0
- data/examples/service_objects/Rakefile +36 -0
- data/examples/service_objects/app/models/account/member.rb +10 -0
- data/examples/service_objects/app/models/account.rb +11 -0
- data/examples/service_objects/app/models/user/token.rb +7 -0
- data/examples/service_objects/app/models/user.rb +15 -0
- data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
- data/examples/service_objects/app/services/application_service.rb +79 -0
- data/examples/service_objects/app/services/user/creation.rb +56 -0
- data/examples/service_objects/app/services/user/token/creation.rb +37 -0
- data/examples/service_objects/config/boot.rb +17 -0
- data/examples/service_objects/config/initializers/solid_result.rb +9 -0
- data/examples/service_objects/config.rb +20 -0
- data/examples/service_objects/db/setup.rb +49 -0
- data/examples/single_listener/Rakefile +92 -0
- data/examples/single_listener/app/models/account/member.rb +10 -0
- data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
- data/examples/single_listener/app/models/account.rb +11 -0
- data/examples/single_listener/app/models/user/creation.rb +67 -0
- data/examples/single_listener/app/models/user/token/creation.rb +51 -0
- data/examples/single_listener/app/models/user/token.rb +7 -0
- data/examples/single_listener/app/models/user.rb +15 -0
- data/examples/single_listener/config/boot.rb +16 -0
- data/examples/single_listener/config/initializers/solid_result.rb +9 -0
- data/examples/single_listener/config.rb +23 -0
- data/examples/single_listener/db/setup.rb +49 -0
- data/examples/single_listener/lib/runtime_breaker.rb +11 -0
- data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
- data/examples/single_listener/lib/solid/result/rollback_on_failure.rb +15 -0
- data/lib/solid/failure.rb +23 -0
- data/lib/solid/output/callable_and_then.rb +40 -0
- data/lib/solid/output/expectations/mixin.rb +31 -0
- data/lib/solid/output/expectations.rb +25 -0
- data/lib/solid/output/failure.rb +9 -0
- data/lib/solid/output/mixin.rb +57 -0
- data/lib/solid/output/success.rb +37 -0
- data/lib/solid/output.rb +115 -0
- data/lib/solid/result/_self.rb +198 -0
- data/lib/solid/result/callable_and_then/caller.rb +49 -0
- data/lib/solid/result/callable_and_then/config.rb +15 -0
- data/lib/solid/result/callable_and_then/error.rb +11 -0
- data/lib/solid/result/callable_and_then.rb +9 -0
- data/lib/solid/result/config/options.rb +27 -0
- data/lib/solid/result/config/switcher.rb +82 -0
- data/lib/solid/result/config/switchers/addons.rb +25 -0
- data/lib/solid/result/config/switchers/constant_aliases.rb +33 -0
- data/lib/solid/result/config/switchers/features.rb +32 -0
- data/lib/solid/result/config/switchers/pattern_matching.rb +20 -0
- data/lib/solid/result/config.rb +64 -0
- data/lib/solid/result/contract/disabled.rb +25 -0
- data/lib/solid/result/contract/error.rb +17 -0
- data/lib/solid/result/contract/evaluator.rb +45 -0
- data/lib/solid/result/contract/for_types.rb +29 -0
- data/lib/solid/result/contract/for_types_and_values.rb +46 -0
- data/lib/solid/result/contract/interface.rb +21 -0
- data/lib/solid/result/contract/type_checker.rb +37 -0
- data/lib/solid/result/contract.rb +33 -0
- data/lib/solid/result/data.rb +33 -0
- data/lib/solid/result/error.rb +59 -0
- data/lib/solid/result/event_logs/config.rb +28 -0
- data/lib/solid/result/event_logs/listener.rb +51 -0
- data/lib/solid/result/event_logs/listeners.rb +87 -0
- data/lib/solid/result/event_logs/tracking/disabled.rb +15 -0
- data/lib/solid/result/event_logs/tracking/enabled.rb +161 -0
- data/lib/solid/result/event_logs/tracking.rb +26 -0
- data/lib/solid/result/event_logs/tree.rb +141 -0
- data/lib/solid/result/event_logs.rb +27 -0
- data/lib/solid/result/expectations/mixin.rb +58 -0
- data/lib/solid/result/expectations.rb +75 -0
- data/lib/solid/result/failure.rb +11 -0
- data/lib/solid/result/handler/allowed_types.rb +45 -0
- data/lib/solid/result/handler.rb +57 -0
- data/lib/solid/result/ignored_types.rb +14 -0
- data/lib/solid/result/mixin.rb +72 -0
- data/lib/solid/result/success.rb +11 -0
- data/lib/solid/result/version.rb +7 -0
- data/lib/solid/result.rb +27 -0
- data/lib/solid/success.rb +23 -0
- data/lib/solid-result.rb +3 -0
- data/sig/solid/failure.rbs +13 -0
- data/sig/solid/output.rbs +175 -0
- data/sig/solid/result/callable_and_then.rbs +60 -0
- data/sig/solid/result/config.rbs +102 -0
- data/sig/solid/result/contract.rbs +120 -0
- data/sig/solid/result/data.rbs +16 -0
- data/sig/solid/result/error.rbs +34 -0
- data/sig/solid/result/event_logs.rbs +189 -0
- data/sig/solid/result/expectations.rbs +71 -0
- data/sig/solid/result/handler.rbs +47 -0
- data/sig/solid/result/ignored_types.rbs +9 -0
- data/sig/solid/result/mixin.rbs +45 -0
- data/sig/solid/result/version.rbs +5 -0
- data/sig/solid/result.rbs +85 -0
- data/sig/solid/success.rbs +13 -0
- 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,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
|
data/lib/solid/output.rb
ADDED
|
@@ -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
|