solid-result 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +98 -0
  3. data/.rubocop_todo.yml +12 -0
  4. data/CHANGELOG.md +600 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +2691 -0
  8. data/Rakefile +28 -0
  9. data/Steepfile +31 -0
  10. data/examples/multiple_listeners/Rakefile +55 -0
  11. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  12. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  13. data/examples/multiple_listeners/app/models/account.rb +11 -0
  14. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  15. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  16. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  17. data/examples/multiple_listeners/app/models/user.rb +15 -0
  18. data/examples/multiple_listeners/config/boot.rb +16 -0
  19. data/examples/multiple_listeners/config/initializers/solid_result.rb +9 -0
  20. data/examples/multiple_listeners/config.rb +27 -0
  21. data/examples/multiple_listeners/db/setup.rb +60 -0
  22. data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
  23. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  24. data/examples/multiple_listeners/lib/solid/result/event_logs_record.rb +27 -0
  25. data/examples/multiple_listeners/lib/solid/result/rollback_on_failure.rb +15 -0
  26. data/examples/service_objects/Rakefile +36 -0
  27. data/examples/service_objects/app/models/account/member.rb +10 -0
  28. data/examples/service_objects/app/models/account.rb +11 -0
  29. data/examples/service_objects/app/models/user/token.rb +7 -0
  30. data/examples/service_objects/app/models/user.rb +15 -0
  31. data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
  32. data/examples/service_objects/app/services/application_service.rb +79 -0
  33. data/examples/service_objects/app/services/user/creation.rb +56 -0
  34. data/examples/service_objects/app/services/user/token/creation.rb +37 -0
  35. data/examples/service_objects/config/boot.rb +17 -0
  36. data/examples/service_objects/config/initializers/solid_result.rb +9 -0
  37. data/examples/service_objects/config.rb +20 -0
  38. data/examples/service_objects/db/setup.rb +49 -0
  39. data/examples/single_listener/Rakefile +92 -0
  40. data/examples/single_listener/app/models/account/member.rb +10 -0
  41. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  42. data/examples/single_listener/app/models/account.rb +11 -0
  43. data/examples/single_listener/app/models/user/creation.rb +67 -0
  44. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  45. data/examples/single_listener/app/models/user/token.rb +7 -0
  46. data/examples/single_listener/app/models/user.rb +15 -0
  47. data/examples/single_listener/config/boot.rb +16 -0
  48. data/examples/single_listener/config/initializers/solid_result.rb +9 -0
  49. data/examples/single_listener/config.rb +23 -0
  50. data/examples/single_listener/db/setup.rb +49 -0
  51. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  52. data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
  53. data/examples/single_listener/lib/solid/result/rollback_on_failure.rb +15 -0
  54. data/lib/solid/failure.rb +23 -0
  55. data/lib/solid/output/callable_and_then.rb +40 -0
  56. data/lib/solid/output/expectations/mixin.rb +31 -0
  57. data/lib/solid/output/expectations.rb +25 -0
  58. data/lib/solid/output/failure.rb +9 -0
  59. data/lib/solid/output/mixin.rb +57 -0
  60. data/lib/solid/output/success.rb +37 -0
  61. data/lib/solid/output.rb +115 -0
  62. data/lib/solid/result/_self.rb +198 -0
  63. data/lib/solid/result/callable_and_then/caller.rb +49 -0
  64. data/lib/solid/result/callable_and_then/config.rb +15 -0
  65. data/lib/solid/result/callable_and_then/error.rb +11 -0
  66. data/lib/solid/result/callable_and_then.rb +9 -0
  67. data/lib/solid/result/config/options.rb +27 -0
  68. data/lib/solid/result/config/switcher.rb +82 -0
  69. data/lib/solid/result/config/switchers/addons.rb +25 -0
  70. data/lib/solid/result/config/switchers/constant_aliases.rb +33 -0
  71. data/lib/solid/result/config/switchers/features.rb +32 -0
  72. data/lib/solid/result/config/switchers/pattern_matching.rb +20 -0
  73. data/lib/solid/result/config.rb +64 -0
  74. data/lib/solid/result/contract/disabled.rb +25 -0
  75. data/lib/solid/result/contract/error.rb +17 -0
  76. data/lib/solid/result/contract/evaluator.rb +45 -0
  77. data/lib/solid/result/contract/for_types.rb +29 -0
  78. data/lib/solid/result/contract/for_types_and_values.rb +46 -0
  79. data/lib/solid/result/contract/interface.rb +21 -0
  80. data/lib/solid/result/contract/type_checker.rb +37 -0
  81. data/lib/solid/result/contract.rb +33 -0
  82. data/lib/solid/result/data.rb +33 -0
  83. data/lib/solid/result/error.rb +59 -0
  84. data/lib/solid/result/event_logs/config.rb +28 -0
  85. data/lib/solid/result/event_logs/listener.rb +51 -0
  86. data/lib/solid/result/event_logs/listeners.rb +87 -0
  87. data/lib/solid/result/event_logs/tracking/disabled.rb +15 -0
  88. data/lib/solid/result/event_logs/tracking/enabled.rb +161 -0
  89. data/lib/solid/result/event_logs/tracking.rb +26 -0
  90. data/lib/solid/result/event_logs/tree.rb +141 -0
  91. data/lib/solid/result/event_logs.rb +27 -0
  92. data/lib/solid/result/expectations/mixin.rb +58 -0
  93. data/lib/solid/result/expectations.rb +75 -0
  94. data/lib/solid/result/failure.rb +11 -0
  95. data/lib/solid/result/handler/allowed_types.rb +45 -0
  96. data/lib/solid/result/handler.rb +57 -0
  97. data/lib/solid/result/ignored_types.rb +14 -0
  98. data/lib/solid/result/mixin.rb +72 -0
  99. data/lib/solid/result/success.rb +11 -0
  100. data/lib/solid/result/version.rb +7 -0
  101. data/lib/solid/result.rb +27 -0
  102. data/lib/solid/success.rb +23 -0
  103. data/lib/solid-result.rb +3 -0
  104. data/sig/solid/failure.rbs +13 -0
  105. data/sig/solid/output.rbs +175 -0
  106. data/sig/solid/result/callable_and_then.rbs +60 -0
  107. data/sig/solid/result/config.rbs +102 -0
  108. data/sig/solid/result/contract.rbs +120 -0
  109. data/sig/solid/result/data.rbs +16 -0
  110. data/sig/solid/result/error.rbs +34 -0
  111. data/sig/solid/result/event_logs.rbs +189 -0
  112. data/sig/solid/result/expectations.rbs +71 -0
  113. data/sig/solid/result/handler.rbs +47 -0
  114. data/sig/solid/result/ignored_types.rbs +9 -0
  115. data/sig/solid/result/mixin.rbs +45 -0
  116. data/sig/solid/result/version.rbs +5 -0
  117. data/sig/solid/result.rbs +85 -0
  118. data/sig/solid/success.rbs +13 -0
  119. metadata +167 -0
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs += %w[lib test]
8
+
9
+ t.test_files = FileList.new('test/**/*_test.rb')
10
+ end
11
+
12
+ Rake::TestTask.new(:test_configuration) do |t|
13
+ t.libs += %w[lib test]
14
+
15
+ t.test_files = FileList.new('test/**/configuration_test.rb')
16
+ end
17
+
18
+ Rake::TestTask.new(:test_event_logs_duration) do |t|
19
+ t.libs += %w[lib test]
20
+
21
+ t.test_files = FileList.new('test/**/duration_test.rb')
22
+ end
23
+
24
+ require 'rubocop/rake_task'
25
+
26
+ RuboCop::RakeTask.new
27
+
28
+ task default: %i[test rubocop]
data/Steepfile ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ D = Steep::Diagnostic
4
+
5
+ target :lib do
6
+ signature 'sig'
7
+
8
+ check 'lib' # Directory name
9
+ # check 'Gemfile' # File name
10
+ # check 'app/models/**/*.rb' # Glob
11
+ # ignore 'lib/templates/*.rb'
12
+
13
+ # library 'singleton' # Standard libraries
14
+ # library 'strong_json' # Gems
15
+
16
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
17
+ # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
18
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
19
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
20
+ configure_code_diagnostics do |hash| # You can setup everything yourself
21
+ hash[D::Ruby::NoMethod] = :information
22
+ end
23
+ end
24
+
25
+ # target :test do
26
+ # signature 'sig', 'sig-private'
27
+ #
28
+ # check 'test'
29
+ #
30
+ # # library 'pathname' # Standard libraries
31
+ # end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_VERSION <= '3.1'
4
+ puts 'This example requires Ruby 3.1 or higher.'
5
+ exit! 1
6
+ end
7
+
8
+ # Usage:
9
+ #
10
+ # rake DISABLE_EVENT_LOGS=t
11
+ # rake DISABLE_LISTENER=t
12
+ #
13
+ # rake HIDE_GIVEN_AND_CONTINUE=t
14
+ #
15
+ # rake BREAK_ACCOUNT_CREATION=t
16
+ # rake BREAK_USER_CREATION=t
17
+ # rake BREAK_USER_TOKEN_CREATION=t
18
+ #
19
+ # rake BREAK_ACCOUNT_CREATION=t HIDE_GIVEN_AND_CONTINUE=t
20
+ task default: %i[solid_result_event_logs]
21
+
22
+ desc 'creates an account and an owner user through Solid::Result'
23
+ task :solid_result_event_logs do
24
+ require_relative 'config'
25
+
26
+ Solid::Result.configuration do |config|
27
+ config.feature.disable!(:event_logs) if ENV['DISABLE_EVENT_LOGS']
28
+
29
+ unless ENV['DISABLE_LISTENER']
30
+ config.event_logs.listener = Solid::Result::EventLogs::Listeners[
31
+ EventLogsListener::Stdout,
32
+ Solid::Result::EventLogsRecord::Listener
33
+ ]
34
+ end
35
+ end
36
+
37
+ result = nil
38
+
39
+ bench = Benchmark.measure do
40
+ result = Account::OwnerCreation.new.call(
41
+ owner: {
42
+ name: "\tJohn Doe \n",
43
+ email: ' JOHN.doe@email.com',
44
+ password: '123123123',
45
+ password_confirmation: '123123123'
46
+ }
47
+ )
48
+ rescue RuntimeBreaker::Interruption => e
49
+ nil
50
+ end
51
+
52
+ puts "\nSolid::Result::EventLogsRecord.count: #{Solid::Result::EventLogsRecord.count}"
53
+
54
+ puts "\nBenchmark: #{bench}"
55
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account::Member < ActiveRecord::Base
4
+ self.table_name = 'account_members'
5
+
6
+ enum role: { owner: 0, admin: 1, contributor: 2 }
7
+
8
+ belongs_to :user, inverse_of: :memberships
9
+ belongs_to :account, inverse_of: :memberships
10
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account
4
+ class OwnerCreation
5
+ include Solid::Output.mixin
6
+ include Solid::Result::RollbackOnFailure
7
+
8
+ def call(**input)
9
+ Solid::Result.event_logs(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 Solid::Output.mixin
6
+ include Solid::Result::RollbackOnFailure
7
+
8
+ def call(**input)
9
+ Solid::Result.event_logs(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 Solid::Output.mixin
6
+
7
+ def call(**input)
8
+ Solid::Result.event_logs(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 'solid-result', path: '../../'
14
+ end
15
+
16
+ require 'active_support/all'
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Solid::Result.config.then do |config|
4
+ config.addon.enable!(:continue)
5
+
6
+ config.pattern_matching.disable!(:nil_as_valid_value_checking)
7
+
8
+ # config.feature.disable!(:expectations) if Rails.env.production?
9
+ end
@@ -0,0 +1,27 @@
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/solid/result/event_logs_record'
14
+ require 'lib/runtime_breaker'
15
+
16
+ module EventLogsListener
17
+ require 'lib/event_logs_listener/stdout'
18
+ end
19
+
20
+ require 'app/models/account'
21
+ require 'app/models/account/member'
22
+ require 'app/models/user'
23
+ require 'app/models/user/token'
24
+
25
+ require 'app/models/account/owner_creation'
26
+ require 'app/models/user/token/creation'
27
+ require 'app/models/user/creation'
@@ -0,0 +1,60 @@
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 :solid_result_event_logs do |t|
14
+ t.string :root_name, null: false, index: true
15
+ t.string :trace_id, index: true
16
+ t.integer :version, null: false
17
+ t.integer :duration, null: false, index: true
18
+ t.json :ids, null: false, default: {}
19
+ t.json :records, null: false, default: []
20
+
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :accounts do |t|
25
+ t.string :uuid, null: false, index: {unique: true}
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :users do |t|
31
+ t.string :uuid, null: false, index: {unique: true}
32
+ t.string :name, null: false
33
+ t.string :email, null: false, index: {unique: true}
34
+ t.string :password_digest, null: false
35
+
36
+ t.timestamps
37
+ end
38
+
39
+ create_table :user_tokens do |t|
40
+ t.belongs_to :user, null: false, foreign_key: true, index: true
41
+ t.string :access_token, null: false
42
+ t.string :refresh_token, null: false
43
+ t.datetime :access_token_expires_at, null: false
44
+ t.datetime :refresh_token_expires_at, null: false
45
+
46
+ t.timestamps
47
+ end
48
+
49
+ create_table :account_members do |t|
50
+ t.integer :role, null: false, default: 0
51
+ t.belongs_to :user, null: false, foreign_key: true, index: true
52
+ t.belongs_to :account, null: false, foreign_key: true, index: true
53
+
54
+ t.timestamps
55
+
56
+ t.index %i[account_id role], unique: true, where: "(role = 0)"
57
+ t.index %i[account_id user_id], unique: true
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventLogsListener::Stdout
4
+ include Solid::Result::EventLogs::Listener
5
+
6
+ def initialize
7
+ @buffer = []
8
+ end
9
+
10
+ def on_start(scope:)
11
+ scope => { id:, name:, desc: }
12
+
13
+ @buffer << [id, "##{id} #{name} - #{desc}".chomp('- ')]
14
+ end
15
+
16
+ def on_record(record:)
17
+ record => { current: { id: }, result: { kind:, type: } }
18
+
19
+ method_name = record.dig(:and_then, :method_name)
20
+
21
+ @buffer << [id, " * #{kind}(#{type}) from method: #{method_name}".chomp('from method: ')]
22
+ end
23
+
24
+ MapNestedMessages = ->(event_logs, buffer, hide_given_and_continue) do
25
+ ids_level_parent = event_logs.dig(:metadata, :ids, :level_parent)
26
+
27
+ messages = buffer.filter_map { |(id, msg)| "#{' ' * ids_level_parent[id].first}#{msg}" if ids_level_parent[id] }
28
+
29
+ messages.reject! { _1.match?(/\(_(given|continue)_\)/) } if hide_given_and_continue
30
+
31
+ messages
32
+ end
33
+
34
+ def on_finish(event_logs:)
35
+ messages = MapNestedMessages[event_logs, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
36
+
37
+ puts messages.join("\n")
38
+ end
39
+
40
+ def before_interruption(exception:, event_logs:)
41
+ messages = MapNestedMessages[event_logs, @buffer, ENV['HIDE_GIVEN_AND_CONTINUE']]
42
+
43
+ puts messages.join("\n")
44
+
45
+ bc = ::ActiveSupport::BacktraceCleaner.new
46
+ bc.add_filter { |line| line.gsub(__dir__.sub('/lib', ''), '').sub(/\A\//, '')}
47
+ bc.add_silencer { |line| /lib\/solid\/result/.match?(line) }
48
+ bc.add_silencer { |line| line.include?(RUBY_VERSION) }
49
+
50
+ dir = "#{FileUtils.pwd[1..]}/"
51
+
52
+ listener_filename = File.basename(__FILE__).chomp('.rb')
53
+
54
+ cb = bc.clean(exception.backtrace)
55
+ cb.each { _1.sub!(dir, '') }
56
+ cb.reject! { _1.match?(/block \(\d levels?\) in|in `block in|internal:kernel|#{listener_filename}/) }
57
+
58
+ puts "\nException:\n #{exception.message} (#{exception.class})\n\nBacktrace:\n #{cb.join("\n ")}"
59
+ end
60
+ 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, "#{env}"
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Result::EventLogsRecord < ActiveRecord::Base
4
+ self.table_name = 'solid_result_event_logs'
5
+
6
+ class Listener
7
+ include ::Solid::Result::EventLogs::Listener
8
+
9
+ def on_finish(event_logs:)
10
+ metadata = event_logs[:metadata]
11
+ root_name = event_logs.dig(:records, 0, :root, :name) || 'Unknown'
12
+
13
+ Solid::Result::EventLogsRecord.create(
14
+ root_name: root_name,
15
+ trace_id: metadata[:trace_id],
16
+ version: event_logs[:version],
17
+ duration: metadata[:duration],
18
+ ids: metadata[:ids],
19
+ records: event_logs[:records]
20
+ )
21
+ rescue ::StandardError => e
22
+ err = "#{e.message} (#{e.class}); Backtrace: #{e.backtrace.join(', ')}"
23
+
24
+ ::Kernel.warn "Error on Solid::Result::EventLogsRecord::Listener#on_finish: #{err}"
25
+ end
26
+ end
27
+ 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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_VERSION <= '3.1'
4
+ puts 'This example requires Ruby 3.1 or higher.'
5
+ exit! 1
6
+ end
7
+
8
+ task default: %i[solid_result_event_logs]
9
+
10
+ task :config do
11
+ require_relative 'config'
12
+ end
13
+
14
+ desc 'creates an account and an owner user through Solid::Result'
15
+ task solid_result_event_logs: %i[config] do
16
+ result1 = Account::OwnerCreation.call(
17
+ owner: {
18
+ name: "\tJohn Doe \n",
19
+ email: ' JOHN.doe@email.com',
20
+ password: '123123123',
21
+ password_confirmation: '123123123'
22
+ }
23
+ )
24
+
25
+ puts result1.inspect
26
+ puts
27
+
28
+ result2 = Account::OwnerCreation.call(
29
+ uuid: "",
30
+ owner: {}
31
+ ).on_failure(:invalid_input) do |output|
32
+ output[:input].errors.full_messages.each do |message|
33
+ puts message
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account::Member < ActiveRecord::Base
4
+ self.table_name = 'account_members'
5
+
6
+ enum role: { owner: 0, admin: 1, contributor: 2 }
7
+
8
+ belongs_to :user, inverse_of: :memberships
9
+ belongs_to :account, inverse_of: :memberships
10
+ 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,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