Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add examples/transitions_listener
Browse files Browse the repository at this point in the history
serradura committed Jan 29, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 4958276 commit 138937e
Showing 15 changed files with 461 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -8,6 +8,13 @@ require:
AllCops:
NewCops: enable
TargetRubyVersion: 2.7
Exclude:
- 'examples/**/*'
- 'vendor/**/*'
- 'spec/fixtures/**/*'
- 'tmp/**/*'
- '.git/**/*'
- 'bin/*'

Lint/RescueException:
Exclude:
78 changes: 78 additions & 0 deletions examples/transitions_listener/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

if RUBY_VERSION <= '3.1'
puts 'This example requires Ruby 3.1 or higher.'
exit! 1
end

task default: %i[bcdd_result_transitions]

task :config do
require_relative 'config'
end

desc 'creates an account and an owner user through BCDD::Result'
task bcdd_result_transitions: %i[config] do
BCDD::Result.configuration do |config|
config.feature.disable!(:transitions) if ENV['DISABLE_TRANSITIONS']

config.transitions.listener = MyBCDDResultTransitionsListener unless ENV['DISABLE_LISTENER']
end

result = nil

bench = Benchmark.measure do
result = Account::OwnerCreation.new.call(
owner: {
name: "\tJohn Doe \n",
email: ' [email protected]',
password: '123123123',
password_confirmation: '123123123'
}
)
end

puts "BCDD::Result benchmark: #{bench}"
end

desc 'creates an account and an owner user directly through ActiveRecord'
task raw_active_record: %i[config] do
require_relative 'config'

result = nil

bench = Benchmark.measure do
email = '[email protected]'

ActiveRecord::Base.transaction do
User::Record.exists?(email:) and raise "User with email #{email} already exists"

user = User::Record.create!(
uuid: ::SecureRandom.uuid,
name: 'John Doe',
email:,
password: '123123123',
password_confirmation: '123123123'
)

executed_at = ::Time.current

user.token.nil? or raise "User with email #{email} already has a token"

user.create_token!(
access_token: ::SecureRandom.hex(24),
refresh_token: ::SecureRandom.hex(24),
access_token_expires_at: executed_at + 15.days,
refresh_token_expires_at: executed_at + 30.days
)

account = Account::Record.create!(uuid: ::SecureRandom.uuid)

Account::Member::Record.create!(account: account, user: user, role: :owner)

result = { account: account, user: user }
end
end

puts "ActiveRecord benchmark: #{bench}"
end
12 changes: 12 additions & 0 deletions examples/transitions_listener/app/models/account/member/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class Account::Member
class Record < ActiveRecord::Base
self.table_name = 'account_memberships'

enum role: { owner: 0, admin: 1, contributor: 2 }

belongs_to :user, inverse_of: :memberships, class_name: 'User::Record'
belongs_to :account, inverse_of: :memberships, class_name: 'Account::Record'
end
end
60 changes: 60 additions & 0 deletions examples/transitions_listener/app/models/account/owner_creation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Account
class OwnerCreation
include BCDD::Context.mixin
include BCDD::Result::RollbackOnFailure

def call(**input)
BCDD::Result.transitions(name: self.class.name) do
Given(input)
.and_then(:normalize_input)
.and_then(:validate_input)
.then { |result|
rollback_on_failure {
result
.and_then(:create_owner)
.and_then(:create_account)
.and_then(:link_owner_to_account)
}
}.and_expose(:account_owner_created, %i[user account])
end
end

private

def normalize_input(**options)
uuid = String(options.fetch(:uuid) { ::SecureRandom.uuid }).strip.downcase

Continue(uuid:)
end

def validate_input(uuid:, owner:)
err = ::Hash.new { |hash, key| hash[key] = [] }

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)
err[:owner] << 'must be a Hash' unless owner.is_a?(::Hash)

err.empty? ? Continue() : Failure(:invalid_input, **err)
end

def create_owner(owner:, **)
::User::Creation.new.call(**owner).handle do |on|
on.success { |output| Continue(user: { record: output[:user], token: output[:token] }) }
on.failure { |output| Failure(:invalid_owner, **output) }
end
end

def create_account(uuid:, **)
account = Record.create(uuid:)

account.persisted? ? Continue(account:) : Failure(:invalid_record, **account.errors.messages)
end

def link_owner_to_account(account:, user:, **)
Member::Record.create!(account:, user: user.fetch(:record), role: :owner)

Continue()
end
end
end
13 changes: 13 additions & 0 deletions examples/transitions_listener/app/models/account/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class Account::Record < ActiveRecord::Base
self.table_name = 'accounts'

has_many :memberships, inverse_of: :account, dependent: :destroy, class_name: '::Account::Member::Record'
has_many :users, through: :memberships, inverse_of: :accounts, class_name: '::User::Record'

where_ownership = -> { where(account_memberships: {role: :owner}) }

has_one :ownership, where_ownership, dependent: nil, inverse_of: :account, class_name: '::Account::Member::Record'
has_one :owner, through: :ownership, source: :user, class_name: '::User::Record'
end
65 changes: 65 additions & 0 deletions examples/transitions_listener/app/models/user/creation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module User
class Creation
include BCDD::Context.mixin
include BCDD::Result::RollbackOnFailure

def call(**input)
BCDD::Result.transitions(name: self.class.name) do
Given(input)
.and_then(:normalize_input)
.and_then(:validate_input)
.and_then(:validate_email_uniqueness)
.then { |result|
rollback_on_failure {
result
.and_then(:create_user)
.and_then(:create_user_token)
}
}
.and_expose(:user_created, %i[user token])
end
end

private

def normalize_input(name:, email:, **options)
name = String(name).strip.gsub(/\s+/, ' ')
email = String(email).strip.downcase

uuid = String(options.fetch(:uuid) { ::SecureRandom.uuid }).strip.downcase

Continue(uuid:, name:, email:)
end

def validate_input(uuid:, name:, email:, password:, password_confirmation:)
err = ::Hash.new { |hash, key| hash[key] = [] }

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)
err[:name] << 'must be present' if name.blank?
err[:email] << 'must be email' unless email.match?(::URI::MailTo::EMAIL_REGEXP)
err[:password] << 'must be present' if password.blank?
err[:password_confirmation] << 'must be present' if password_confirmation.blank?

err.empty? ? Continue() : Failure(:invalid_input, **err)
end

def validate_email_uniqueness(email:, **)
Record.exists?(email:) ? Failure(:email_already_taken) : Continue()
end

def create_user(uuid:, name:, email:, password:, password_confirmation:)
user = Record.create(uuid:, name:, email:, password:, password_confirmation:)

user.persisted? ? Continue(user:) : Failure(:invalid_record, **user.errors.messages)
end

def create_user_token(user:, **)
Token::Creation.new.call(user: user).handle do |on|
on.success { |output| Continue(token: output[:token]) }
on.failure { raise 'Token creation failed' }
end
end
end
end
17 changes: 17 additions & 0 deletions examples/transitions_listener/app/models/user/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class User::Record < ActiveRecord::Base
self.table_name = 'users'

has_secure_password

has_many :memberships, inverse_of: :user, dependent: :destroy, class_name: '::Account::Member::Record'
has_many :accounts, through: :memberships, inverse_of: :users, class_name: '::Account::Record'

where_ownership = -> { where(account_memberships: { role: :owner }) }

has_one :ownership, where_ownership, inverse_of: :user, class_name: '::Account::Member::Record'
has_one :account, through: :ownership, inverse_of: :owner, class_name: '::Account::Record'

has_one :token, inverse_of: :user, dependent: :destroy, class_name: '::User::Token::Record'
end
49 changes: 49 additions & 0 deletions examples/transitions_listener/app/models/user/token/creation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module User::Token
class Creation
include BCDD::Context.mixin

def call(**input)
BCDD::Result.transitions(name: self.class.name) do
Given(input)
.and_then(:normalize_input)
.and_then(:validate_input)
.and_then(:validate_token_existence)
.and_then(:create_token)
.and_expose(:token_created, %i[token])
end
end

private

def normalize_input(**options)
Continue(executed_at: options.fetch(:executed_at) { ::Time.current })
end

def validate_input(user:, executed_at:)
err = ::Hash.new { |hash, key| hash[key] = [] }

err[:user] << 'must be a User::Record' unless user.is_a?(::User::Record)
err[:user] << 'must be persisted' unless user.try(:persisted?)
err[:executed_at] << 'must be a time' unless executed_at.is_a?(::Time)

err.empty? ? Continue() : Failure(:invalid_user, **err)
end

def validate_token_existence(user:, **)
user.token.nil? ? Continue() : Failure(:token_already_exists)
end

def create_token(user:, executed_at:, **)
token = user.create_token(
access_token: ::SecureRandom.hex(24),
refresh_token: ::SecureRandom.hex(24),
access_token_expires_at: executed_at + 15.days,
refresh_token_expires_at: executed_at + 30.days
)

token.persisted? ? Continue(token:) : Failure(:token_creation_failed, **token.errors.messages)
end
end
end
9 changes: 9 additions & 0 deletions examples/transitions_listener/app/models/user/token/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module User::Token
class Record < ActiveRecord::Base
self.table_name = 'user_tokens'

belongs_to :user, class_name: 'User::Record', inverse_of: :token
end
end
27 changes: 27 additions & 0 deletions examples/transitions_listener/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

require 'bundler/inline'

$LOAD_PATH.unshift(__dir__)

require_relative 'config/boot'
require_relative 'config/initializers/bcdd'

require 'db/setup'

require 'lib/bcdd/result/rollback_on_failure'
require 'lib/my_bcdd_result_transitions_listener'

module Account
require 'app/models/account/record'
require 'app/models/account/owner_creation'
require 'app/models/account/member/record'
end

module User
require 'app/models/user/record'
require 'app/models/user/creation'

require 'app/models/user/token/record'
require 'app/models/user/token/creation'
end
16 changes: 16 additions & 0 deletions examples/transitions_listener/config/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require 'bundler/inline'

$LOAD_PATH.unshift(__dir__)

gemfile do
source 'https://rubygems.org'

gem 'sqlite3', '~> 1.7'
gem 'bcrypt', '~> 3.1.20'
gem 'activerecord', '~> 7.1', '>= 7.1.3', require: 'active_record'
gem 'bcdd-result', path: '../../'
end

require 'active_support/all'
11 changes: 11 additions & 0 deletions examples/transitions_listener/config/initializers/bcdd.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

BCDD::Result.config.then do |config|
config.addon.enable!(:continue)

config.constant_alias.enable!('BCDD::Context')

config.pattern_matching.disable!(:nil_as_valid_value_checking)

# config.feature.disable!(:expectations) if Rails.env.production?
end
Loading

0 comments on commit 138937e

Please sign in to comment.