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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -1
  3. data/CHANGELOG.md +70 -16
  4. data/README.md +293 -83
  5. data/Steepfile +4 -4
  6. data/examples/multiple_listeners/Rakefile +55 -0
  7. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  8. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  9. data/examples/multiple_listeners/app/models/account.rb +11 -0
  10. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  11. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  12. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  13. data/examples/multiple_listeners/app/models/user.rb +15 -0
  14. data/examples/multiple_listeners/config/boot.rb +16 -0
  15. data/examples/multiple_listeners/config/initializers/bcdd.rb +11 -0
  16. data/examples/multiple_listeners/config.rb +27 -0
  17. data/examples/multiple_listeners/db/setup.rb +61 -0
  18. data/examples/multiple_listeners/lib/bcdd/result/rollback_on_failure.rb +15 -0
  19. data/examples/multiple_listeners/lib/bcdd/result/transitions_record.rb +28 -0
  20. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  21. data/examples/multiple_listeners/lib/transitions_listener/stdout.rb +54 -0
  22. data/examples/single_listener/Rakefile +92 -0
  23. data/examples/single_listener/app/models/account/member.rb +10 -0
  24. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  25. data/examples/single_listener/app/models/account.rb +11 -0
  26. data/examples/single_listener/app/models/user/creation.rb +67 -0
  27. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  28. data/examples/single_listener/app/models/user/token.rb +7 -0
  29. data/examples/single_listener/app/models/user.rb +15 -0
  30. data/examples/single_listener/config/boot.rb +16 -0
  31. data/examples/single_listener/config/initializers/bcdd.rb +11 -0
  32. data/examples/single_listener/config.rb +23 -0
  33. data/examples/single_listener/db/setup.rb +49 -0
  34. data/examples/single_listener/lib/bcdd/result/rollback_on_failure.rb +15 -0
  35. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  36. data/examples/single_listener/lib/single_transitions_listener.rb +108 -0
  37. data/lib/bcdd/result/callable_and_then/caller.rb +1 -1
  38. data/lib/bcdd/result/config.rb +6 -1
  39. data/lib/bcdd/result/context/expectations/mixin.rb +2 -2
  40. data/lib/bcdd/result/context/mixin.rb +2 -2
  41. data/lib/bcdd/result/context/success.rb +20 -2
  42. data/lib/bcdd/result/contract/for_types.rb +1 -1
  43. data/lib/bcdd/result/contract/for_types_and_values.rb +2 -0
  44. data/lib/bcdd/result/expectations/mixin.rb +2 -2
  45. data/lib/bcdd/result/ignored_types.rb +14 -0
  46. data/lib/bcdd/result/mixin.rb +2 -2
  47. data/lib/bcdd/result/transitions/config.rb +26 -0
  48. data/lib/bcdd/result/transitions/listener.rb +51 -0
  49. data/lib/bcdd/result/transitions/listeners.rb +87 -0
  50. data/lib/bcdd/result/transitions/tracking/disabled.rb +1 -13
  51. data/lib/bcdd/result/transitions/tracking/enabled.rb +76 -17
  52. data/lib/bcdd/result/transitions/tracking.rb +8 -3
  53. data/lib/bcdd/result/transitions/tree.rb +26 -0
  54. data/lib/bcdd/result/transitions.rb +3 -4
  55. data/lib/bcdd/result/version.rb +1 -1
  56. data/lib/bcdd/result.rb +7 -5
  57. data/sig/bcdd/result/config.rbs +1 -0
  58. data/sig/bcdd/result/context.rbs +9 -0
  59. data/sig/bcdd/result/ignored_types.rbs +9 -0
  60. data/sig/bcdd/result/transitions.rbs +96 -7
  61. data/sig/bcdd/result.rbs +2 -2
  62. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User::Token < ActiveRecord::Base
4
+ self.table_name = 'user_tokens'
5
+
6
+ belongs_to :user, inverse_of: :token
7
+ 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, source) do
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)
@@ -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.new(type: :continued, value: value, source: self)
12
+ Success(::BCDD::Result::IgnoredTypes::CONTINUE, **value)
13
13
  end
14
14
  end
15
15
 
@@ -17,7 +17,7 @@ class BCDD::Result::Context
17
17
  private def Given(*values)
18
18
  value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
19
19
 
20
- Success.new(type: :given, value: value, source: self)
20
+ Success(::BCDD::Result::IgnoredTypes::GIVEN, **value)
21
21
  end
22
22
  end
23
23
 
@@ -25,7 +25,7 @@ class BCDD::Result::Context
25
25
  end
26
26
 
27
27
  private def Continue(**value)
28
- _ResultAs(Success, :continued, value)
28
+ _ResultAs(Success, ::BCDD::Result::IgnoredTypes::CONTINUE, value)
29
29
  end
30
30
  end
31
31
 
@@ -33,7 +33,7 @@ class BCDD::Result::Context
33
33
  private def Given(*values)
34
34
  value = values.map(&:to_h).reduce({}) { |acc, val| acc.merge(val) }
35
35
 
36
- _ResultAs(Success, :given, value)
36
+ _ResultAs(Success, ::BCDD::Result::IgnoredTypes::GIVEN, value)
37
37
  end
38
38
  end
39
39
 
@@ -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
- exposed_value = acc.merge(value).slice(*keys)
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: exposed_value, source: source, terminal: terminal)
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
@@ -11,7 +11,7 @@ class BCDD::Result
11
11
  end
12
12
 
13
13
  def type?(type)
14
- allowed_types.member?(type)
14
+ IgnoredTypes.include?(type) || allowed_types.member?(type)
15
15
  end
16
16
 
17
17
  def type!(type)
@@ -30,6 +30,8 @@ class BCDD::Result
30
30
  def type_and_value!(data)
31
31
  type, value = data.type, data.value
32
32
 
33
+ return value if IgnoredTypes.include?(type)
34
+
33
35
  value_checking = @types_and_values[type!(type)]
34
36
 
35
37
  checking_result = value_checking === value
@@ -38,13 +38,13 @@ class BCDD::Result
38
38
  module Addons
39
39
  module Continue
40
40
  private def Continue(value)
41
- Success.new(type: :continued, value: value, source: self)
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.new(type: :given, value: value, source: self)
47
+ _Result.Success(IgnoredTypes::GIVEN, value)
48
48
  end
49
49
  end
50
50
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BCDD::Result
4
+ module IgnoredTypes
5
+ LIST = ::Set[
6
+ GIVEN = :_given_,
7
+ CONTINUE = :_continue_
8
+ ].freeze
9
+
10
+ def self.include?(type)
11
+ LIST.member?(type)
12
+ end
13
+ end
14
+ end
@@ -32,13 +32,13 @@ class BCDD::Result
32
32
  end
33
33
 
34
34
  private def Continue(value)
35
- _ResultAs(Success, :continued, value)
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, :given, value)
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