bcdd-result 0.11.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 +97 -15
- data/README.md +508 -95
- 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 +49 -0
- data/lib/bcdd/result/callable_and_then/config.rb +15 -0
- data/lib/bcdd/result/callable_and_then/error.rb +11 -0
- data/lib/bcdd/result/callable_and_then.rb +9 -0
- data/lib/bcdd/result/config/switchers/features.rb +5 -1
- data/lib/bcdd/result/config.rb +15 -4
- data/lib/bcdd/result/context/callable_and_then.rb +39 -0
- data/lib/bcdd/result/context/expectations/mixin.rb +2 -2
- data/lib/bcdd/result/context/mixin.rb +3 -3
- data/lib/bcdd/result/context/success.rb +29 -7
- data/lib/bcdd/result/context.rb +34 -16
- 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/error.rb +20 -11
- data/lib/bcdd/result/expectations/mixin.rb +3 -3
- data/lib/bcdd/result/expectations.rb +6 -6
- data/lib/bcdd/result/ignored_types.rb +14 -0
- data/lib/bcdd/result/mixin.rb +3 -3
- 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 +4 -6
- data/lib/bcdd/result/transitions/tracking/enabled.rb +103 -24
- data/lib/bcdd/result/transitions/tracking.rb +8 -3
- data/lib/bcdd/result/transitions/tree.rb +36 -6
- data/lib/bcdd/result/transitions.rb +11 -14
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +39 -22
- data/sig/bcdd/result/callable_and_then.rbs +60 -0
- data/sig/bcdd/result/config.rbs +3 -0
- data/sig/bcdd/result/context.rbs +65 -4
- data/sig/bcdd/result/error.rbs +9 -6
- data/sig/bcdd/result/expectations.rbs +4 -4
- data/sig/bcdd/result/ignored_types.rbs +9 -0
- data/sig/bcdd/result/transitions.rbs +107 -7
- data/sig/bcdd/result.rbs +10 -6
- metadata +48 -6
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Account
|
4
|
+
class OwnerCreation
|
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
|
+
.then { |result|
|
14
|
+
rollback_on_failure {
|
15
|
+
result
|
16
|
+
.and_then(:create_owner)
|
17
|
+
.and_then(:create_account)
|
18
|
+
.and_then(:link_owner_to_account)
|
19
|
+
}
|
20
|
+
}.and_expose(:account_owner_created, %i[user account])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def normalize_input(**options)
|
27
|
+
uuid = String(options.fetch(:uuid) { ::SecureRandom.uuid }).strip.downcase
|
28
|
+
|
29
|
+
Continue(uuid:)
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_input(uuid:, owner:)
|
33
|
+
err = ::Hash.new { |hash, key| hash[key] = [] }
|
34
|
+
|
35
|
+
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)
|
36
|
+
err[:owner] << 'must be a Hash' unless owner.is_a?(::Hash)
|
37
|
+
|
38
|
+
err.empty? ? Continue() : Failure(:invalid_input, **err)
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_owner(owner:, **)
|
42
|
+
::User::Creation.new.call(**owner).handle do |on|
|
43
|
+
on.success { |output| Continue(user: { record: output[:user], token: output[:token] }) }
|
44
|
+
on.failure { |output| Failure(:invalid_owner, **output) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_account(uuid:, **)
|
49
|
+
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_ACCOUNT_CREATION')
|
50
|
+
|
51
|
+
account = ::Account.create(uuid:)
|
52
|
+
|
53
|
+
account.persisted? ? Continue(account:) : Failure(:invalid_record, **account.errors.messages)
|
54
|
+
end
|
55
|
+
|
56
|
+
def link_owner_to_account(account:, user:, **)
|
57
|
+
Member.create!(account:, user: user.fetch(:record), role: :owner)
|
58
|
+
|
59
|
+
Continue()
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Account < ActiveRecord::Base
|
4
|
+
has_many :memberships, inverse_of: :account, dependent: :destroy, class_name: '::Account::Member'
|
5
|
+
has_many :users, through: :memberships, inverse_of: :accounts
|
6
|
+
|
7
|
+
where_ownership = -> { where(account_members: {role: :owner}) }
|
8
|
+
|
9
|
+
has_one :ownership, where_ownership, dependent: nil, inverse_of: :account, class_name: '::Account::Member'
|
10
|
+
has_one :owner, through: :ownership, source: :user
|
11
|
+
end
|
@@ -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
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Caller
|
5
|
+
def self.call(source, value:, injected_value:, method_name:)
|
6
|
+
method = callable_method(source, method_name)
|
7
|
+
|
8
|
+
Transitions.tracking.record_and_then(method, injected_value) do
|
9
|
+
result =
|
10
|
+
if source.is_a?(::Proc)
|
11
|
+
call_proc!(source, value, injected_value)
|
12
|
+
else
|
13
|
+
call_method!(source, method, value, injected_value)
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_result_object(source, value, result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.call_proc!(source, value, injected_value)
|
21
|
+
case source.arity
|
22
|
+
when 1 then source.call(value)
|
23
|
+
when 2 then source.call(value, injected_value)
|
24
|
+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: :call, arity: '1..2')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.call_method!(source, method, value, injected_value)
|
29
|
+
case method.arity
|
30
|
+
when 1 then source.send(method.name, value)
|
31
|
+
when 2 then source.send(method.name, value, injected_value)
|
32
|
+
else raise CallableAndThen::Error::InvalidArity.build(source: source, method: method.name, arity: '1..2')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.callable_method(source, method_name)
|
37
|
+
source.method(method_name || Config.instance.and_then!.default_method_name_to_call)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.ensure_result_object(source, _value, result)
|
41
|
+
return result if result.is_a?(::BCDD::Result)
|
42
|
+
|
43
|
+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source)
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :new, :allocate
|
47
|
+
private_class_method :call_proc!, :call_method!, :callable_method, :ensure_result_object
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Config
|
5
|
+
attr_accessor :default_method_name_to_call
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.default_method_name_to_call = :call
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
{ default_method_name_to_call: default_method_name_to_call }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
class CallableAndThen::Error < Error
|
5
|
+
class InvalidArity < self
|
6
|
+
def self.build(source:, method:, arity:)
|
7
|
+
new("Invalid arity for #{source.class}##{method} method. Expected arity: #{arity}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -10,7 +10,11 @@ class BCDD::Result
|
|
10
10
|
},
|
11
11
|
transitions: {
|
12
12
|
default: true,
|
13
|
-
affects: %w[BCDD::Result BCDD::Result::Context]
|
13
|
+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
14
|
+
},
|
15
|
+
and_then!: {
|
16
|
+
default: false,
|
17
|
+
affects: %w[BCDD::Result BCDD::Result::Context BCDD::Result::Expectations BCDD::Result::Context::Expectations]
|
14
18
|
}
|
15
19
|
}.transform_values!(&:freeze).freeze
|
16
20
|
|
data/lib/bcdd/result/config.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'singleton'
|
4
|
-
|
5
3
|
require_relative 'config/options'
|
6
4
|
require_relative 'config/switcher'
|
7
5
|
require_relative 'config/switchers/addons'
|
@@ -11,7 +9,7 @@ require_relative 'config/switchers/pattern_matching'
|
|
11
9
|
|
12
10
|
class BCDD::Result
|
13
11
|
class Config
|
14
|
-
include Singleton
|
12
|
+
include ::Singleton
|
15
13
|
|
16
14
|
attr_reader :addon, :feature, :constant_alias, :pattern_matching
|
17
15
|
|
@@ -20,6 +18,15 @@ class BCDD::Result
|
|
20
18
|
@feature = Features.switcher
|
21
19
|
@constant_alias = ConstantAliases.switcher
|
22
20
|
@pattern_matching = PatternMatching.switcher
|
21
|
+
@and_then_ = CallableAndThen::Config.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def transitions
|
25
|
+
Transitions::Config.instance
|
26
|
+
end
|
27
|
+
|
28
|
+
def and_then!
|
29
|
+
@and_then_
|
23
30
|
end
|
24
31
|
|
25
32
|
def freeze
|
@@ -27,6 +34,8 @@ class BCDD::Result
|
|
27
34
|
feature.freeze
|
28
35
|
constant_alias.freeze
|
29
36
|
pattern_matching.freeze
|
37
|
+
and_then!.freeze
|
38
|
+
transitions.freeze
|
30
39
|
|
31
40
|
super
|
32
41
|
end
|
@@ -45,7 +54,9 @@ class BCDD::Result
|
|
45
54
|
end
|
46
55
|
|
47
56
|
def inspect
|
48
|
-
"#<#{self.class.name}
|
57
|
+
"#<#{self.class.name} " \
|
58
|
+
"options=#{options.keys.sort.inspect} " \
|
59
|
+
"and_then!=#{and_then!.options.inspect}>"
|
49
60
|
end
|
50
61
|
end
|
51
62
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BCDD::Result
|
4
|
+
module Context::CallableAndThen
|
5
|
+
class Caller < 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
|
+
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(:acc).then { _1.merge!(value.merge(_1)) } } if result.is_a?(Context)
|
32
|
+
|
33
|
+
raise Error::UnexpectedOutcome.build(outcome: result, origin: source, expected: Context::EXPECTED_OUTCOME)
|
34
|
+
end
|
35
|
+
|
36
|
+
private_class_method :call_proc!, :call_method!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
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
|
|