bcdd-result 0.12.0 → 0.13.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 +4 -4
- data/.rubocop.yml +16 -1
- data/CHANGELOG.md +70 -16
- data/README.md +293 -83
- data/Steepfile +4 -4
- 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/bcdd.rb +11 -0
- data/examples/multiple_listeners/config.rb +27 -0
- data/examples/multiple_listeners/db/setup.rb +61 -0
- data/examples/multiple_listeners/lib/bcdd/result/rollback_on_failure.rb +15 -0
- data/examples/multiple_listeners/lib/bcdd/result/transitions_record.rb +28 -0
- data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
- data/examples/multiple_listeners/lib/transitions_listener/stdout.rb +54 -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/bcdd.rb +11 -0
- data/examples/single_listener/config.rb +23 -0
- data/examples/single_listener/db/setup.rb +49 -0
- data/examples/single_listener/lib/bcdd/result/rollback_on_failure.rb +15 -0
- data/examples/single_listener/lib/runtime_breaker.rb +11 -0
- data/examples/single_listener/lib/single_transitions_listener.rb +108 -0
- data/lib/bcdd/result/callable_and_then/caller.rb +1 -1
- data/lib/bcdd/result/config.rb +6 -1
- data/lib/bcdd/result/context/expectations/mixin.rb +2 -2
- data/lib/bcdd/result/context/mixin.rb +2 -2
- data/lib/bcdd/result/context/success.rb +20 -2
- data/lib/bcdd/result/contract/for_types.rb +1 -1
- data/lib/bcdd/result/contract/for_types_and_values.rb +2 -0
- data/lib/bcdd/result/expectations/mixin.rb +2 -2
- data/lib/bcdd/result/ignored_types.rb +14 -0
- data/lib/bcdd/result/mixin.rb +2 -2
- data/lib/bcdd/result/transitions/config.rb +26 -0
- data/lib/bcdd/result/transitions/listener.rb +51 -0
- data/lib/bcdd/result/transitions/listeners.rb +87 -0
- data/lib/bcdd/result/transitions/tracking/disabled.rb +1 -13
- data/lib/bcdd/result/transitions/tracking/enabled.rb +76 -17
- data/lib/bcdd/result/transitions/tracking.rb +8 -3
- data/lib/bcdd/result/transitions/tree.rb +26 -0
- data/lib/bcdd/result/transitions.rb +3 -4
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +7 -5
- data/sig/bcdd/result/config.rbs +1 -0
- data/sig/bcdd/result/context.rbs +9 -0
- data/sig/bcdd/result/ignored_types.rbs +9 -0
- data/sig/bcdd/result/transitions.rbs +96 -7
- data/sig/bcdd/result.rbs +2 -2
- metadata +42 -6
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class User
|
4
|
+
class Creation
|
5
|
+
include BCDD::Context.mixin
|
6
|
+
include BCDD::Result::RollbackOnFailure
|
7
|
+
|
8
|
+
def call(**input)
|
9
|
+
BCDD::Result.transitions(name: self.class.name) do
|
10
|
+
Given(input)
|
11
|
+
.and_then(:normalize_input)
|
12
|
+
.and_then(:validate_input)
|
13
|
+
.and_then(:validate_email_uniqueness)
|
14
|
+
.then { |result|
|
15
|
+
rollback_on_failure {
|
16
|
+
result
|
17
|
+
.and_then(:create_user)
|
18
|
+
.and_then(:create_user_token)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
.and_expose(:user_created, %i[user token])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def normalize_input(name:, email:, **options)
|
28
|
+
name = String(name).strip.gsub(/\s+/, ' ')
|
29
|
+
email = String(email).strip.downcase
|
30
|
+
|
31
|
+
uuid = String(options.fetch(:uuid) { ::SecureRandom.uuid }).strip.downcase
|
32
|
+
|
33
|
+
Continue(uuid:, name:, email:)
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_input(uuid:, name:, email:, password:, password_confirmation:)
|
37
|
+
err = ::Hash.new { |hash, key| hash[key] = [] }
|
38
|
+
|
39
|
+
err[:uuid] << 'must be an UUID' unless uuid.match?(/\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/i)
|
40
|
+
err[:name] << 'must be present' if name.blank?
|
41
|
+
err[:email] << 'must be email' unless email.match?(::URI::MailTo::EMAIL_REGEXP)
|
42
|
+
err[:password] << 'must be present' if password.blank?
|
43
|
+
err[:password_confirmation] << 'must be present' if password_confirmation.blank?
|
44
|
+
|
45
|
+
err.empty? ? Continue() : Failure(:invalid_input, **err)
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_email_uniqueness(email:, **)
|
49
|
+
::User.exists?(email:) ? Failure(:email_already_taken) : Continue()
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_user(uuid:, name:, email:, password:, password_confirmation:)
|
53
|
+
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_USER_CREATION')
|
54
|
+
|
55
|
+
user = ::User.create(uuid:, name:, email:, password:, password_confirmation:)
|
56
|
+
|
57
|
+
user.persisted? ? Continue(user:) : Failure(:invalid_record, **user.errors.messages)
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_user_token(user:, **)
|
61
|
+
Token::Creation.new.call(user: user).handle do |on|
|
62
|
+
on.success { |output| Continue(token: output[:token]) }
|
63
|
+
on.failure { raise 'Token creation failed' }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class User::Token
|
4
|
+
class Creation
|
5
|
+
include BCDD::Context.mixin
|
6
|
+
|
7
|
+
def call(**input)
|
8
|
+
BCDD::Result.transitions(name: self.class.name) do
|
9
|
+
Given(input)
|
10
|
+
.and_then(:normalize_input)
|
11
|
+
.and_then(:validate_input)
|
12
|
+
.and_then(:validate_token_existence)
|
13
|
+
.and_then(:create_token)
|
14
|
+
.and_expose(:token_created, %i[token])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def normalize_input(**options)
|
21
|
+
Continue(executed_at: options.fetch(:executed_at) { ::Time.current })
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_input(user:, executed_at:)
|
25
|
+
err = ::Hash.new { |hash, key| hash[key] = [] }
|
26
|
+
|
27
|
+
err[:user] << 'must be a User' unless user.is_a?(::User)
|
28
|
+
err[:user] << 'must be persisted' unless user.try(:persisted?)
|
29
|
+
err[:executed_at] << 'must be a time' unless executed_at.is_a?(::Time)
|
30
|
+
|
31
|
+
err.empty? ? Continue() : Failure(:invalid_user, **err)
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_token_existence(user:, **)
|
35
|
+
user.token.nil? ? Continue() : Failure(:token_already_exists)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_token(user:, executed_at:, **)
|
39
|
+
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_USER_TOKEN_CREATION')
|
40
|
+
|
41
|
+
token = user.create_token(
|
42
|
+
access_token: ::SecureRandom.hex(24),
|
43
|
+
refresh_token: ::SecureRandom.hex(24),
|
44
|
+
access_token_expires_at: executed_at + 15.days,
|
45
|
+
refresh_token_expires_at: executed_at + 30.days
|
46
|
+
)
|
47
|
+
|
48
|
+
token.persisted? ? Continue(token:) : Failure(:token_creation_failed, **token.errors.messages)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class User < ActiveRecord::Base
|
4
|
+
has_secure_password
|
5
|
+
|
6
|
+
has_many :memberships, inverse_of: :user, dependent: :destroy, class_name: '::Account::Member'
|
7
|
+
has_many :accounts, through: :memberships, inverse_of: :users
|
8
|
+
|
9
|
+
where_ownership = -> { where(account_members: { role: :owner }) }
|
10
|
+
|
11
|
+
has_one :ownership, where_ownership, inverse_of: :user, class_name: '::Account::Member'
|
12
|
+
has_one :account, through: :ownership, inverse_of: :owner
|
13
|
+
|
14
|
+
has_one :token, inverse_of: :user, dependent: :destroy, class_name: '::User::Token'
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(__dir__)
|
6
|
+
|
7
|
+
gemfile do
|
8
|
+
source 'https://rubygems.org'
|
9
|
+
|
10
|
+
gem 'sqlite3', '~> 1.7'
|
11
|
+
gem 'bcrypt', '~> 3.1.20'
|
12
|
+
gem 'activerecord', '~> 7.1', '>= 7.1.3', require: 'active_record'
|
13
|
+
gem 'bcdd-result', path: '../../'
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'active_support/all'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
BCDD::Result.config.then do |config|
|
4
|
+
config.addon.enable!(:continue)
|
5
|
+
|
6
|
+
config.constant_alias.enable!('BCDD::Context')
|
7
|
+
|
8
|
+
config.pattern_matching.disable!(:nil_as_valid_value_checking)
|
9
|
+
|
10
|
+
# config.feature.disable!(:expectations) if Rails.env.production?
|
11
|
+
end
|
@@ -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_transitions_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,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SingleTransitionsListener
|
4
|
+
include BCDD::Result::Transitions::Listener
|
5
|
+
|
6
|
+
# A listener will be initialized before the first transition, and it is discarded after the last one.
|
7
|
+
def initialize
|
8
|
+
@buffer = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# This method will be called before each transition block.
|
12
|
+
# The parent transition block will be called first in the case of nested transition 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 transitions in the same block.
|
22
|
+
# It can be used to perform an instrumentation (measure/report) of the transitions.
|
23
|
+
#
|
24
|
+
# @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
|
25
|
+
def around_transitions(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 = ->(transitions, buffer, hide_given_and_continue) do
|
60
|
+
ids_matrix = transitions.dig(:metadata, :ids_matrix)
|
61
|
+
|
62
|
+
messages = buffer.filter_map { |(id, msg)| "#{' ' * ids_matrix[id].last}#{msg}" if ids_matrix[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 transitions tracking.
|
70
|
+
#
|
71
|
+
# @param transitions:
|
72
|
+
# {
|
73
|
+
# :version => 1,
|
74
|
+
# :metadata => {
|
75
|
+
# :duration => 0,
|
76
|
+
# :trace_id => nil,
|
77
|
+
# :ids_tree => [0, [[1, []], [2, []]]],
|
78
|
+
# :ids_matrix => {0 => [0, 0], 1 => [1, 1], 2 => [2, 1]}
|
79
|
+
# },
|
80
|
+
# :records => [
|
81
|
+
# # ...
|
82
|
+
# ]
|
83
|
+
# }
|
84
|
+
def on_finish(transitions:)
|
85
|
+
messages = MapNestedMessages[transitions, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
|
86
|
+
|
87
|
+
puts messages.join("\n")
|
88
|
+
end
|
89
|
+
|
90
|
+
# This method will be called when an exception is raised during the transitions tracking.
|
91
|
+
#
|
92
|
+
# @param exception: Exception
|
93
|
+
# @param transitions: Hash
|
94
|
+
def before_interruption(exception:, transitions:)
|
95
|
+
messages = MapNestedMessages[transitions, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
|
96
|
+
|
97
|
+
puts messages.join("\n")
|
98
|
+
|
99
|
+
bc = ::ActiveSupport::BacktraceCleaner.new
|
100
|
+
bc.add_filter { |line| line.gsub(__dir__.sub('/lib', ''), '').sub(/\A\//, '')}
|
101
|
+
bc.add_silencer { |line| /lib\/bcdd\/result/.match?(line) }
|
102
|
+
bc.add_silencer { |line| line.include?(RUBY_VERSION) }
|
103
|
+
|
104
|
+
backtrace = bc.clean(exception.backtrace)
|
105
|
+
|
106
|
+
puts "\nException: #{exception.message} (#{exception.class}); Backtrace: #{backtrace.join(", ")}"
|
107
|
+
end
|
108
|
+
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
|
8
|
+
Transitions.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)
|
data/lib/bcdd/result/config.rb
CHANGED
@@ -9,7 +9,7 @@ require_relative 'config/switchers/pattern_matching'
|
|
9
9
|
|
10
10
|
class BCDD::Result
|
11
11
|
class Config
|
12
|
-
include Singleton
|
12
|
+
include ::Singleton
|
13
13
|
|
14
14
|
attr_reader :addon, :feature, :constant_alias, :pattern_matching
|
15
15
|
|
@@ -21,6 +21,10 @@ class BCDD::Result
|
|
21
21
|
@and_then_ = CallableAndThen::Config.new
|
22
22
|
end
|
23
23
|
|
24
|
+
def transitions
|
25
|
+
Transitions::Config.instance
|
26
|
+
end
|
27
|
+
|
24
28
|
def and_then!
|
25
29
|
@and_then_
|
26
30
|
end
|
@@ -31,6 +35,7 @@ class BCDD::Result
|
|
31
35
|
constant_alias.freeze
|
32
36
|
pattern_matching.freeze
|
33
37
|
and_then!.freeze
|
38
|
+
transitions.freeze
|
34
39
|
|
35
40
|
super
|
36
41
|
end
|
@@ -9,7 +9,7 @@ class BCDD::Result::Context
|
|
9
9
|
module Addons
|
10
10
|
module Continue
|
11
11
|
private def Continue(**value)
|
12
|
-
Success
|
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
|
20
|
+
Success(::BCDD::Result::IgnoredTypes::GIVEN, **value)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -25,7 +25,7 @@ class BCDD::Result::Context
|
|
25
25
|
end
|
26
26
|
|
27
27
|
private def Continue(**value)
|
28
|
-
_ResultAs(Success,
|
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,
|
36
|
+
_ResultAs(Success, ::BCDD::Result::IgnoredTypes::GIVEN, value)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -1,9 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class BCDD::Result
|
4
|
+
class Context::Error < BCDD::Result::Error
|
5
|
+
InvalidExposure = ::Class.new(self)
|
6
|
+
end
|
7
|
+
|
4
8
|
class Context::Success < Context
|
5
9
|
include ::BCDD::Result::Success::Methods
|
6
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 Context::Error::InvalidExposure, message
|
19
|
+
end
|
20
|
+
|
7
21
|
def and_expose(type, keys, terminal: true)
|
8
22
|
unless keys.is_a?(::Array) && !keys.empty? && keys.all?(::Symbol)
|
9
23
|
raise ::ArgumentError, 'keys must be an Array of Symbols'
|
@@ -11,9 +25,13 @@ class BCDD::Result
|
|
11
25
|
|
12
26
|
Transitions.tracking.reset_and_then!
|
13
27
|
|
14
|
-
|
28
|
+
acc_values = acc.merge(value)
|
29
|
+
|
30
|
+
value_to_expose = FetchValues.call(acc_values, keys)
|
31
|
+
|
32
|
+
expectations = type_checker.expectations
|
15
33
|
|
16
|
-
self.class.new(type: type, value:
|
34
|
+
self.class.new(type: type, value: value_to_expose, source: source, terminal: terminal, expectations: expectations)
|
17
35
|
end
|
18
36
|
end
|
19
37
|
end
|
@@ -38,13 +38,13 @@ class BCDD::Result
|
|
38
38
|
module Addons
|
39
39
|
module Continue
|
40
40
|
private def Continue(value)
|
41
|
-
Success
|
41
|
+
_Result.Success(IgnoredTypes::CONTINUE, value)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
45
|
module Given
|
46
46
|
private def Given(value)
|
47
|
-
Success
|
47
|
+
_Result.Success(IgnoredTypes::GIVEN, value)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
data/lib/bcdd/result/mixin.rb
CHANGED
@@ -32,13 +32,13 @@ class BCDD::Result
|
|
32
32
|
end
|
33
33
|
|
34
34
|
private def Continue(value)
|
35
|
-
_ResultAs(Success,
|
35
|
+
_ResultAs(Success, IgnoredTypes::CONTINUE, value)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
module Given
|
40
40
|
private def Given(value)
|
41
|
-
_ResultAs(Success,
|
41
|
+
_ResultAs(Success, IgnoredTypes::GIVEN, value)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BCDD::Result::Transitions
|
4
|
+
class Config
|
5
|
+
include ::Singleton
|
6
|
+
|
7
|
+
attr_reader :listener, :trace_id
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@trace_id = -> {}
|
11
|
+
@listener = Listener::Null.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def listener=(arg)
|
15
|
+
Listener.kind?(arg) or raise ::ArgumentError, "#{arg.inspect} must be a #{Listener}"
|
16
|
+
|
17
|
+
@listener = arg
|
18
|
+
end
|
19
|
+
|
20
|
+
def trace_id=(arg)
|
21
|
+
raise ::ArgumentError, 'must be a lambda with arity 0' unless arg.is_a?(::Proc) && arg.lambda? && arg.arity.zero?
|
22
|
+
|
23
|
+
@trace_id = arg
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BCDD::Result::Transitions
|
4
|
+
module Listener
|
5
|
+
module ClassMethods
|
6
|
+
def around_transitions?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def around_and_then?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.extend(ClassMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.extended(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.kind?(arg)
|
24
|
+
(arg.is_a?(::Class) && arg < self) || (arg.is_a?(::Module) && arg.is_a?(self)) || arg.is_a?(Listeners::Chain)
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_start(scope:); end
|
28
|
+
|
29
|
+
def around_transitions(scope:)
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
|
33
|
+
def around_and_then(scope:, and_then:)
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_record(record:); end
|
38
|
+
|
39
|
+
def on_finish(transitions:); end
|
40
|
+
|
41
|
+
def before_interruption(exception:, transitions:); end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Listener::Null
|
45
|
+
extend Listener
|
46
|
+
|
47
|
+
def self.new
|
48
|
+
self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|