-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
522 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# frozen_string_literal: true | ||
|
||
if RUBY_VERSION <= '3.1' | ||
puts 'This example requires Ruby 3.1 or higher.' | ||
exit! 1 | ||
end | ||
|
||
# Usage: | ||
# | ||
# rake DISABLE_TRANSITIONS=t | ||
# rake DISABLE_LISTENER=t | ||
# | ||
# rake HIDE_GIVEN_AND_CONTINUED=t | ||
# | ||
# rake BREAK_ACCOUNT_CREATION=t | ||
# rake BREAK_USER_CREATION=t | ||
# rake BREAK_USER_TOKEN_CREATION=t | ||
# | ||
# rake BREAK_ACCOUNT_CREATION=t HIDE_GIVEN_AND_CONTINUED=t | ||
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' | ||
} | ||
) | ||
rescue RuntimeBreaker::Interruption => e | ||
nil | ||
end | ||
|
||
puts "\nBenchmark: #{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 "\nBenchmark: #{bench}" | ||
end |
12 changes: 12 additions & 0 deletions
12
examples/transitions_listener/app/models/account/member/record.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_members' | ||
|
||
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 |
62 changes: 62 additions & 0 deletions
62
examples/transitions_listener/app/models/account/owner_creation.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# 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:, **) | ||
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_ACCOUNT_CREATION') | ||
|
||
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
13
examples/transitions_listener/app/models/account/record.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_members: {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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# 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:) | ||
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_USER_CREATION') | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_members: { 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 |
51 changes: 51 additions & 0 deletions
51
examples/transitions_listener/app/models/user/token/creation.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# 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:, **) | ||
::RuntimeBreaker.try_to_interrupt(env: 'BREAK_USER_TOKEN_CREATION') | ||
|
||
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
9
examples/transitions_listener/app/models/user/token/record.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# 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' | ||
require 'lib/runtime_breaker' | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
Oops, something went wrong.