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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +58 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/Guardfile +11 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +452 -0
  8. data/Rakefile +12 -0
  9. data/lib/tasks/.keep +0 -0
  10. data/lib/tasks/aliases.rake +50 -0
  11. data/lib/tasks/annotate_rb.rake +23 -0
  12. data/lib/tasks/stateman.rake +50 -0
  13. data/lib/templates/stateman/migration.rb.erb +24 -0
  14. data/lib/templates/stateman/state_machine.rb.erb +37 -0
  15. data/lib/templates/stateman/transition.rb.erb +15 -0
  16. data/lib/toolx/core/concerns/custom_identifier.rb +53 -0
  17. data/lib/toolx/core/concerns/date_time_to_boolean.rb +54 -0
  18. data/lib/toolx/core/concerns/inquirer.rb +89 -0
  19. data/lib/toolx/core/concerns/transformer.rb +41 -0
  20. data/lib/toolx/core/concerns/with_state_machine.rb +49 -0
  21. data/lib/toolx/core/env.rb +45 -0
  22. data/lib/toolx/core/errors/api_error.rb +40 -0
  23. data/lib/toolx/core/errors/app_error.rb +3 -0
  24. data/lib/toolx/core/errors/nested_error.rb +32 -0
  25. data/lib/toolx/core/errors/nested_standard_error.rb +11 -0
  26. data/lib/toolx/core/form.rb +9 -0
  27. data/lib/toolx/core/operation/callbacks_wrapper.rb +27 -0
  28. data/lib/toolx/core/operation/flow.rb +220 -0
  29. data/lib/toolx/core/operation/params_wrapper.rb +33 -0
  30. data/lib/toolx/core/operation/rescue_wrapper.rb +20 -0
  31. data/lib/toolx/core/operation/response_wrapper.rb +34 -0
  32. data/lib/toolx/core/operation/simplified_result.rb +45 -0
  33. data/lib/toolx/core/operation/transaction_wrapper.rb +32 -0
  34. data/lib/toolx/core/operation.rb +27 -0
  35. data/lib/toolx/core/operation_base.rb +4 -0
  36. data/lib/toolx/core/presenter.rb +50 -0
  37. data/lib/toolx/core/simple_crypt.rb +28 -0
  38. data/lib/toolx/version.rb +5 -0
  39. data/lib/toolx.rb +30 -0
  40. data/sig/toolx.rbs +4 -0
  41. 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,3 @@
1
+ class Toolx::Core::Errors::AppError < StandardError
2
+ include ::Toolx::Core::Errors::NestedError
3
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toolx
4
+ module Core
5
+ module Errors
6
+ class NestedStandardError < StandardError
7
+ include ::Toolx::Core::Errors::NestedError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Toolx::Core::Form
4
+ include ActiveModel::Model
5
+ include ActiveModel::Attributes
6
+ include ActiveModel::Validations::Callbacks
7
+
8
+ def as_json(options = nil) = attributes.as_json(options)
9
+ 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