toolx 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +58 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +452 -0
- data/Rakefile +12 -0
- data/lib/tasks/.keep +0 -0
- data/lib/tasks/aliases.rake +50 -0
- data/lib/tasks/annotate_rb.rake +23 -0
- data/lib/tasks/stateman.rake +50 -0
- data/lib/templates/stateman/migration.rb.erb +24 -0
- data/lib/templates/stateman/state_machine.rb.erb +37 -0
- data/lib/templates/stateman/transition.rb.erb +15 -0
- data/lib/toolx/core/concerns/custom_identifier.rb +53 -0
- data/lib/toolx/core/concerns/date_time_to_boolean.rb +54 -0
- data/lib/toolx/core/concerns/inquirer.rb +89 -0
- data/lib/toolx/core/concerns/transformer.rb +41 -0
- data/lib/toolx/core/concerns/with_state_machine.rb +49 -0
- data/lib/toolx/core/env.rb +45 -0
- data/lib/toolx/core/errors/api_error.rb +40 -0
- data/lib/toolx/core/errors/app_error.rb +3 -0
- data/lib/toolx/core/errors/nested_error.rb +32 -0
- data/lib/toolx/core/errors/nested_standard_error.rb +11 -0
- data/lib/toolx/core/form.rb +9 -0
- data/lib/toolx/core/operation/callbacks_wrapper.rb +27 -0
- data/lib/toolx/core/operation/flow.rb +220 -0
- data/lib/toolx/core/operation/params_wrapper.rb +33 -0
- data/lib/toolx/core/operation/rescue_wrapper.rb +20 -0
- data/lib/toolx/core/operation/response_wrapper.rb +34 -0
- data/lib/toolx/core/operation/simplified_result.rb +45 -0
- data/lib/toolx/core/operation/transaction_wrapper.rb +32 -0
- data/lib/toolx/core/operation.rb +27 -0
- data/lib/toolx/core/operation_base.rb +4 -0
- data/lib/toolx/core/presenter.rb +50 -0
- data/lib/toolx/core/simple_crypt.rb +28 -0
- data/lib/toolx/version.rb +5 -0
- data/lib/toolx.rb +30 -0
- data/sig/toolx.rbs +4 -0
- metadata +337 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
namespace :toolx do
|
2
|
+
namespace :aliases do
|
3
|
+
desc 'Generate toolx_aliases.rb'
|
4
|
+
task :generate do
|
5
|
+
file_name = Rails.root.join('lib', 'toolx_aliases.rb')
|
6
|
+
return if File.exist?(file_name)
|
7
|
+
|
8
|
+
File.open(file_name, 'w') do |file|
|
9
|
+
file.write <<~RUBY
|
10
|
+
require 'toolx'
|
11
|
+
|
12
|
+
# WithStateMachine = Toolx::Core::Concerns::WithStateMachine
|
13
|
+
# Inquirer = Toolx::Core::Concerns::Inquirer
|
14
|
+
# DateTimeToBoolean = Toolx::Core::Concerns::DateTimeToBoolean
|
15
|
+
# CustomIdentifier = Toolx::Core::Concerns::CustomIdentifier
|
16
|
+
# Transformer = Toolx::Core::Concerns::Transformer
|
17
|
+
|
18
|
+
NestedError = Toolx::Core::Errors::NestedError
|
19
|
+
NestedStandardError = Toolx::Core::Errors::NestedStandardError
|
20
|
+
AppError = Toolx::Core::Errors::AppError
|
21
|
+
ApiError = Toolx::Core::Errors::ApiError
|
22
|
+
Form = Toolx::Core::Form
|
23
|
+
Presenter = Toolx::Core::Presenter
|
24
|
+
SimpleCrypt = Toolx::Core::SimpleCrypt
|
25
|
+
Operation = Toolx::Core::Operation
|
26
|
+
Operation::Base = ::Toolx::Core::OperationBase
|
27
|
+
Operation::Flow = Toolx::Core::Operation::Flow
|
28
|
+
Env = Toolx::Core::Env
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
puts "Aliases generated at #{Pathname.new(file_name).relative_path_from(Rails.root)}"
|
32
|
+
puts "\n"
|
33
|
+
puts "You can add all Toolx accessories to the app/models/application_record.rb like below:"
|
34
|
+
puts <<~RUBY
|
35
|
+
class ApplicationRecord < ActiveRecord::Base
|
36
|
+
primary_abstract_class
|
37
|
+
|
38
|
+
include Toolx::Core::Concerns::Inquirer
|
39
|
+
include Toolx::Core::Concerns::Transformer
|
40
|
+
include Toolx::Core::Concerns::CustomIdentifier
|
41
|
+
include Toolx::Core::Concerns::DateTimeToBoolean
|
42
|
+
include Toolx::Core::Concerns::WithStateMachine
|
43
|
+
end
|
44
|
+
RUBY
|
45
|
+
puts "\n"
|
46
|
+
puts "Add line below to config/application.rb"
|
47
|
+
puts "require_relative '../lib/toolx_aliases'"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
namespace :toolx do
|
2
|
+
namespace :annotate do
|
3
|
+
desc 'Generate annotate rake tasks'
|
4
|
+
task :generate do
|
5
|
+
file_name = Rails.root.join('lib', 'tasks', 'annotate_rb.rake')
|
6
|
+
return if File.exist?(file_name)
|
7
|
+
|
8
|
+
File.open(file_name, 'w') do |file|
|
9
|
+
file.write <<~RUBY
|
10
|
+
# This rake task was added by annotate_rb gem.
|
11
|
+
|
12
|
+
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
|
13
|
+
if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil?
|
14
|
+
require "annotate_rb"
|
15
|
+
|
16
|
+
AnnotateRb::Core.load_rake_tasks
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
puts "Annotate rake tasks generated at #{Pathname.new(file_name).relative_path_from(Rails.root)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
namespace :toolx do
|
4
|
+
namespace :stateman do
|
5
|
+
desc 'Generate StateMachine, Transition class, and migration'
|
6
|
+
task :generate, [:model] => :environment do |_, args|
|
7
|
+
require 'erb'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
model = args[:model].to_s.camelize
|
11
|
+
abort 'Model name required' if model.blank?
|
12
|
+
|
13
|
+
class_name = model.camelize
|
14
|
+
file_path = model.underscore
|
15
|
+
path = Rails.root.join('app/models', file_path)
|
16
|
+
FileUtils.mkdir_p(path)
|
17
|
+
|
18
|
+
template_root_path = File.expand_path("../templates/stateman", __dir__)
|
19
|
+
|
20
|
+
templates = {
|
21
|
+
'state_machine.rb.erb' => path.join('state_machine.rb'),
|
22
|
+
'transition.rb.erb' => path.join('transition.rb')
|
23
|
+
}
|
24
|
+
|
25
|
+
templates.each do |template_file, output_path|
|
26
|
+
template_path = File.join(template_root_path, template_file)
|
27
|
+
erb = ERB.new(File.read(template_path), trim_mode: '-')
|
28
|
+
File.write(output_path, erb.result_with_hash(model: model, class_name: class_name))
|
29
|
+
puts "Created #{Pathname.new(output_path).relative_path_from(Rails.root)}"
|
30
|
+
end
|
31
|
+
|
32
|
+
version = ActiveRecord::Migration.current_version.to_s[0..2]
|
33
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
34
|
+
migration_path = Rails.root.join("db/migrate/#{timestamp}_create_#{file_path.gsub('/', '_')}_transitions.rb")
|
35
|
+
migration_template = File.join(template_root_path, 'migration.rb.erb')
|
36
|
+
erb = ERB.new(File.read(migration_template), trim_mode: '-')
|
37
|
+
File.write(migration_path, erb.result_with_hash(model: model, class_name: class_name, version: version))
|
38
|
+
puts "Created #{Pathname.new(migration_path).relative_path_from(Rails.root)}"
|
39
|
+
puts "\n"
|
40
|
+
puts "Do not forget update #{args[:model].to_s.camelize} model #{Pathname.new(path).relative_path_from(Rails.root)} by adding:"
|
41
|
+
puts "\n```\n"
|
42
|
+
puts "include Toolx::Core::Concerns::WithStateMachine"
|
43
|
+
puts "with_state_machine"
|
44
|
+
puts "\n```\n"
|
45
|
+
puts "Do not forget it is works only for ActiveRecord adapter so add to your initializer"
|
46
|
+
puts "echo 'Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }' >> #{Rails.root.join('config/initializers', 'statesman.rb')}"
|
47
|
+
puts "\n\nEnjoy!"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<% underscored_model = model.underscore.tr("/", "_") %>
|
2
|
+
<% demodulized_model = model.demodulize.underscore %>
|
3
|
+
class Create<%= class_name.gsub('::', '') %>Transitions < ActiveRecord::Migration[<%= version %>]
|
4
|
+
def change
|
5
|
+
create_table :<%= underscored_model %>_transitions, id: :string do |t|
|
6
|
+
t.references :<%= demodulized_model %>, null: false, type: :string, foreign_key: <%= underscored_model == demodulized_model ? 'true' : "{ to_table: :#{underscored_model.pluralize} }" %>
|
7
|
+
t.string :to_state, null: false
|
8
|
+
t.json :metadata, default: {}
|
9
|
+
t.boolean :most_recent, default: false
|
10
|
+
t.integer :sort_key, null: false
|
11
|
+
t.timestamps null: false
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index(:<%= underscored_model %>_transitions,
|
15
|
+
%i(<%= demodulized_model %>_id sort_key),
|
16
|
+
unique: true,
|
17
|
+
name: 'index_<%= underscored_model %>_transition_parent_sort')
|
18
|
+
add_index(:<%= underscored_model %>_transitions,
|
19
|
+
%i(<%= demodulized_model %>_id most_recent),
|
20
|
+
unique: true,
|
21
|
+
where: 'most_recent',
|
22
|
+
name: 'index_<%= underscored_model %>_transition_parent_most_recent')
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class <%= class_name %>::StateMachine
|
2
|
+
include Statesman::Machine
|
3
|
+
|
4
|
+
state :pending, initial: true
|
5
|
+
# state :pending, initial: true
|
6
|
+
# state :checking_out
|
7
|
+
# state :purchased
|
8
|
+
# state :shipped
|
9
|
+
# state :cancelled
|
10
|
+
# state :failed
|
11
|
+
# state :refunded
|
12
|
+
#
|
13
|
+
# transition from: :pending, to: [:checking_out, :cancelled]
|
14
|
+
# transition from: :checking_out, to: [:purchased, :cancelled]
|
15
|
+
# transition from: :purchased, to: [:shipped, :failed]
|
16
|
+
# transition from: :shipped, to: :refunded
|
17
|
+
#
|
18
|
+
# after_transition do |model, transition|
|
19
|
+
# model.update!(status: transition.to_state)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# guard_transition(to: :checking_out) do |order|
|
23
|
+
# order.products_in_stock?
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# before_transition(from: :checking_out, to: :cancelled) do |order, transition|
|
27
|
+
# order.reallocate_stock
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# before_transition(to: :purchased) do |order, transition|
|
31
|
+
# PaymentService.new(order).submit
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# after_transition(to: :purchased) do |order, transition|
|
35
|
+
# MailerService.order_confirmation(order).deliver
|
36
|
+
# end
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<% underscored_model = model.underscore.tr("/", "_") %>
|
2
|
+
<% demodulized_model = model.demodulize.underscore %>
|
3
|
+
class <%= class_name %>::Transition < ApplicationRecord
|
4
|
+
# include Statesman::Adapters::ActiveRecordTransition
|
5
|
+
belongs_to :<%= demodulized_model %>, class_name: '<%=class_name %>' # , inverse_of: :<%= underscored_model %>_transitions
|
6
|
+
|
7
|
+
#cid :<%= model.downcase[0..2] %>_tr
|
8
|
+
|
9
|
+
attribute :most_recent, :boolean, default: false
|
10
|
+
attribute :to_state, :string
|
11
|
+
attribute :sort_key, :integer
|
12
|
+
attribute :metadata, :json, default: {}
|
13
|
+
|
14
|
+
validates :to_state, inclusion: { in: <%= class_name %>::StateMachine.states }
|
15
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx
|
4
|
+
module Core
|
5
|
+
module Concerns
|
6
|
+
module CustomIdentifier
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# @example Generate a custom ID for the `id` column
|
11
|
+
# class User < ApplicationRecord
|
12
|
+
# include CustomIdentifier
|
13
|
+
# cid 'usr'
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# user.id # => "usr_Nc82xL0w92..."
|
17
|
+
#
|
18
|
+
# @example Use a related model's ID prefix (e.g., workspace)
|
19
|
+
# class Document < ApplicationRecord
|
20
|
+
# belongs_to :workspace
|
21
|
+
# cid 'doc', related: { workspace: 4 }
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # If workspace_id = "wsp_ABCXYZ...", then document id will be like:
|
25
|
+
# # "doc_ABCXabcd123..."
|
26
|
+
#
|
27
|
+
# @param [String] prefix Required prefix to prepend to the ID
|
28
|
+
# @param [Integer] size Total length of the generated ID (default: 16)
|
29
|
+
# @param [Hash] related A hash of one related model and how many characters to copy from its ID
|
30
|
+
# @param [Symbol] name The attribute to assign the generated ID to (default: :id)
|
31
|
+
def cid(prefix, size: 16, related: {}, name: :id)
|
32
|
+
before_create do
|
33
|
+
identifier = send(name)
|
34
|
+
next unless identifier.nil?
|
35
|
+
|
36
|
+
shared = if related.present?
|
37
|
+
object, shared = related.first
|
38
|
+
ref_key = self.class.reflections[object.to_s].foreign_key
|
39
|
+
read_attribute(ref_key || '')&.split('_')&.second&.first(shared)
|
40
|
+
end || ''
|
41
|
+
loop do
|
42
|
+
unique = SecureRandom.base58(size - shared.length)
|
43
|
+
|
44
|
+
send("#{name}=", "#{prefix}_#{shared}#{unique}")
|
45
|
+
break unless self.class.exists?(identifier)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx
|
4
|
+
module Core
|
5
|
+
module Concerns
|
6
|
+
module DateTimeToBoolean
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# @example
|
11
|
+
# class User < ApplicationRecord
|
12
|
+
# include DateTimeToBoolean
|
13
|
+
# date_time_to_boolean :deleted_at
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# user = User.new
|
17
|
+
# user.deleted!
|
18
|
+
# user.deleted = true # or DateTime, Time, String with datetime value
|
19
|
+
# user.deleted_at # => current time
|
20
|
+
# user.deleted? # => true
|
21
|
+
#
|
22
|
+
# @param [Array<Symbol>] columns One or more datetime columns (e.g. `:confirmed_at`, `:deleted_at`)
|
23
|
+
def date_time_to_boolean(*columns)
|
24
|
+
columns.each do |column|
|
25
|
+
m_name = column.to_s.gsub(/_at$/, '').to_sym
|
26
|
+
define_method("#{m_name}?") do
|
27
|
+
public_send(column).present?
|
28
|
+
end
|
29
|
+
define_method("#{m_name}!") do
|
30
|
+
public_send("#{column}=", Time.current)
|
31
|
+
end
|
32
|
+
define_method("#{m_name}=") do |value|
|
33
|
+
if ActiveModel::Type::Boolean.new.cast(value)
|
34
|
+
time = case value
|
35
|
+
when DateTime then value.to_time
|
36
|
+
when Time then value
|
37
|
+
when String
|
38
|
+
Time.parse(value) rescue Time.current
|
39
|
+
else
|
40
|
+
Time.current
|
41
|
+
end
|
42
|
+
public_send("#{column}=", time)
|
43
|
+
else
|
44
|
+
# if the value is false, set the column to nil
|
45
|
+
public_send("#{column}=", nil)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx
|
4
|
+
module Core
|
5
|
+
module Concerns
|
6
|
+
module Inquirer
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
class SymbolInquiry < SimpleDelegator
|
10
|
+
def initialize(sym)
|
11
|
+
raise ArgumentError, 'Must be a Symbol' unless sym.is_a?(Symbol)
|
12
|
+
sym.is_a?(::Toolx::Core::Concerns::Inquirer::SymbolInquiry) ? super(sym.__getobj__) : super(sym)
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method_name, *args, &block)
|
16
|
+
if method_name.to_s.end_with?('?')
|
17
|
+
__getobj__.to_s == method_name.to_s.delete_suffix('?')
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to_missing?(method_name, include_private = false)
|
24
|
+
method_name.to_s.end_with?('?') || super
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect = __getobj__.inspect
|
28
|
+
def to_s = __getobj__.to_s
|
29
|
+
def to_sym = __getobj__
|
30
|
+
def is_a?(klass) = klass == Symbol || super
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
__getobj__ == other.to_sym
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module NilInquiry
|
38
|
+
def method_missing(method_name, *args, &)
|
39
|
+
return false if method_name.to_s.include?('?')
|
40
|
+
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
def respond_to_missing?(method_name, include_private = false)
|
45
|
+
return true if method_name.to_s.include?('?')
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def nil?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.nil_value
|
56
|
+
@nil_value ||= String.new.extend(NilInquiry)
|
57
|
+
end
|
58
|
+
|
59
|
+
class_methods do
|
60
|
+
# @example
|
61
|
+
# class User < ApplicationRecord
|
62
|
+
# include Inquirer
|
63
|
+
# inquirer :status
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# user = User.new(status: "active")
|
67
|
+
# user.status.active? # => true
|
68
|
+
# user.status.inactive? # => false
|
69
|
+
#
|
70
|
+
# user = User.new(status: nil)
|
71
|
+
# user.status.inactive? # => false (instead of NoMethodError)
|
72
|
+
#
|
73
|
+
# @param [Array<Symbol>] methods List of method names (usually attribute readers) to wrap with inquiry behavior
|
74
|
+
def inquirer(*methods)
|
75
|
+
methods.each do |method_name|
|
76
|
+
define_method :"#{method_name}" do
|
77
|
+
value = super() rescue self[method_name] # avoid super() for dry-struct-generated attributes
|
78
|
+
return ::Toolx::Core::Concerns::Inquirer.nil_value if value.blank?
|
79
|
+
return ::Toolx::Core::Concerns::Inquirer::SymbolInquiry.new(value) if value.is_a?(Symbol)
|
80
|
+
|
81
|
+
value.inquiry
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx::Core::Concerns::Transformer
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Optional use of normalize_attributes gem
|
7
|
+
class_methods do
|
8
|
+
# @example Strip and downcase the email before saving
|
9
|
+
# class User < ApplicationRecord
|
10
|
+
# include Toolx::Core::Concerns::Transformer
|
11
|
+
#
|
12
|
+
# transform :email, :strip, :downcase
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# user.email = " Foo@Example.COM "
|
16
|
+
# user.email # => "foo@example.com"
|
17
|
+
#
|
18
|
+
# @example Convert blank strings to nil
|
19
|
+
# transform :first_name, :strip
|
20
|
+
#
|
21
|
+
# @param [Symbol] method_name the attribute to define a transformed setter for
|
22
|
+
# @param [Array<Symbol>] transformations list of transformation methods (e.g., `:strip`, `:downcase`, `:humanize` etc.)
|
23
|
+
def transform(method_name, *transformations)
|
24
|
+
# rubocop:disable all
|
25
|
+
define_method :"#{method_name}=" do |raw_value|
|
26
|
+
return super raw_value unless raw_value.is_a?(String)
|
27
|
+
|
28
|
+
all = (transformations || [])
|
29
|
+
value = begin
|
30
|
+
# permit = %i[upcase downcase strip humanize]
|
31
|
+
# allowed_transformations = (permit & all)
|
32
|
+
all.inject(raw_value) do |current, transformation|
|
33
|
+
current.public_send(transformation) if transformation.is_a?(Symbol) && current.respond_to?(transformation)
|
34
|
+
# TODO: allow to use method class itself like transformation(current) if transformation.is_a?(Method)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
super value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'statesman'
|
4
|
+
|
5
|
+
module Toolx
|
6
|
+
module Core
|
7
|
+
module Concerns
|
8
|
+
module WithStateMachine
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
def in_state?(*state)
|
13
|
+
state.any? { |s| current_state.to_sym == s.to_sym }
|
14
|
+
end
|
15
|
+
|
16
|
+
def not_in_state?(state)
|
17
|
+
!in_state?(state)
|
18
|
+
end
|
19
|
+
|
20
|
+
delegate :can_transition_to?, :transition_to!, :transition_to, :current_state, to: :state_machine
|
21
|
+
end
|
22
|
+
|
23
|
+
class_methods do
|
24
|
+
# It is adding a state machine to the model. Use rake task `bin/rails state_machine:generate[model]` to generate the necessary files.
|
25
|
+
def with_state_machine
|
26
|
+
model_name = name
|
27
|
+
model_namespace = model_name.deconstantize
|
28
|
+
model_base = model_name.demodulize
|
29
|
+
|
30
|
+
transition_class = [model_namespace.presence, model_base, 'Transition'].compact.join('::')
|
31
|
+
state_machine_class = [model_namespace.presence, model_base, 'StateMachine'].compact.join('::')
|
32
|
+
|
33
|
+
has_many :transitions, autosave: false, class_name: transition_class, dependent: :destroy # inverse_of: :project
|
34
|
+
|
35
|
+
include ::Statesman::Adapters::ActiveRecordQueries[
|
36
|
+
transition_class: transition_class.constantize,
|
37
|
+
initial_state: state_machine_class.constantize.initial_state
|
38
|
+
]
|
39
|
+
|
40
|
+
define_method(:state_machine) do
|
41
|
+
instance_variable_get(:@state_machine) ||
|
42
|
+
instance_variable_set(:@state_machine, state_machine_class.constantize.new(self, transition_class: transition_class.constantize, association_name: :transitions))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx
|
4
|
+
module Core
|
5
|
+
module Env
|
6
|
+
MissingEnv = Class.new(StandardError)
|
7
|
+
|
8
|
+
def self.[](value) = env_variable!(value)
|
9
|
+
def self.all = ENV
|
10
|
+
|
11
|
+
def self.method_missing(method_name, default = nil, &)
|
12
|
+
name = method_name.to_s
|
13
|
+
return boolean(name[0..-2], default, &) if name[-1] == '?'
|
14
|
+
return integer(name[0..-2], default, &) if name[-1] == '!'
|
15
|
+
return inquirer(name[0..-2], default, &) if name[-1] == '_'
|
16
|
+
|
17
|
+
env_variable!(name, default, &)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.respond_to?(*_args) = true
|
21
|
+
def respond_to_missing?(*_args) = true
|
22
|
+
|
23
|
+
def self.boolean(value, default, &)
|
24
|
+
result = env_variable!(value, default, &).to_s.downcase
|
25
|
+
ActiveModel::Type::Boolean.new.cast(result)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.integer(value, default, &) = env_variable!(value, default, &).to_i
|
29
|
+
|
30
|
+
def self.inquirer(value, default, &)
|
31
|
+
result = env_variable!(value, default, &)
|
32
|
+
return ::Toolx::Core::Concerns::Inquirer.nil_value if result.blank?
|
33
|
+
return ::Toolx::Core::Concerns::Inquirer::SymbolInquiry.new(result) if result.is_a?(Symbol)
|
34
|
+
|
35
|
+
result.inquiry
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.env_variable!(value, default = nil, &)
|
39
|
+
default ? ENV.fetch(value.to_s.upcase, default, &) : ENV.fetch(value.to_s.upcase, &)
|
40
|
+
rescue KeyError
|
41
|
+
raise MissingEnv, "Missing: #{value.upcase} env variable"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Toolx
|
2
|
+
module Core
|
3
|
+
module Errors
|
4
|
+
class ApiError < ::Toolx::Core::Errors::AppError
|
5
|
+
attr_reader :details
|
6
|
+
|
7
|
+
def initialize(details = nil)
|
8
|
+
@details = details || {}
|
9
|
+
super(@details[:message] || error_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def message_namespace
|
13
|
+
@message_namespace ||= self.class.to_s.deconstantize.underscore.tr!('/', '.')
|
14
|
+
end
|
15
|
+
|
16
|
+
def status
|
17
|
+
@status ||= details[:status] || error_name.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def error_name
|
23
|
+
@error_name ||= self.class.to_s.demodulize.underscore
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
BadRequest = Class.new(ApiError)
|
28
|
+
Unauthorized = Class.new(ApiError)
|
29
|
+
NotFound = Class.new(ApiError)
|
30
|
+
Conflict = Class.new(ApiError)
|
31
|
+
UnprocessableEntity = Class.new(ApiError)
|
32
|
+
FailedDependency = Class.new(ApiError)
|
33
|
+
Forbidden = Class.new(ApiError)
|
34
|
+
InternalServerError = Class.new(ApiError)
|
35
|
+
BadGateway = Class.new(ApiError)
|
36
|
+
ServiceUnavailable = Class.new(ApiError)
|
37
|
+
GatewayTimeout = Class.new(ApiError)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toolx
|
4
|
+
module Core
|
5
|
+
module Errors
|
6
|
+
module NestedError
|
7
|
+
attr_reader :nested, :raw_backtrace
|
8
|
+
|
9
|
+
def initialize(msg = nil, nested = $ERROR_INFO)
|
10
|
+
super(msg)
|
11
|
+
@nested = nested
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_backtrace(backtrace) # rubocop:disable Naming/AccessorMethodName
|
15
|
+
@raw_backtrace = backtrace
|
16
|
+
if nested
|
17
|
+
backtrace -= nested_raw_backtrace
|
18
|
+
backtrace += ["#{nested.backtrace.first}: #{nested.message} (#{nested.class.name})"]
|
19
|
+
backtrace += nested.backtrace[1..] || []
|
20
|
+
end
|
21
|
+
super(backtrace)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def nested_raw_backtrace
|
27
|
+
nested.respond_to?(:raw_backtrace) ? nested.raw_backtrace : nested.backtrace
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Toolx::Core::Operation::CallbacksWrapper
|
2
|
+
def perform(...)
|
3
|
+
run_callbacks :perform do
|
4
|
+
super
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def before(...)
|
10
|
+
set_callback(:perform, :before, ...)
|
11
|
+
end
|
12
|
+
|
13
|
+
def after(...)
|
14
|
+
set_callback(:perform, :after, ...)
|
15
|
+
end
|
16
|
+
|
17
|
+
def around(...)
|
18
|
+
set_callback(:perform, :around, ...)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.prepended(base)
|
23
|
+
base.include ::ActiveSupport::Callbacks
|
24
|
+
base.extend ClassMethods
|
25
|
+
base.define_callbacks :perform
|
26
|
+
end
|
27
|
+
end
|