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,220 @@
1
+ module Toolx::Core::Operation::Flow
2
+ def self.included(base)
3
+ base.send(:extend, ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ # @example
8
+ # executor = MyOperation.flow
9
+ # .bind_with(self)
10
+ # .on(success: :dashboard, fail: :show_error)
11
+ # executor.perform(name: 'Alice', age: 20).success?
12
+ # executor.response # => { user: <User object> }
13
+ # @return [Operation::Flow::Executor] a new instance of Executor for the operation class
14
+ def flow = Executor.new(self)
15
+ end
16
+
17
+ class Executor
18
+ ExecutorError = Class.new(StandardError)
19
+ OperationAlreadyInitialized = Class.new(ExecutorError)
20
+ OperationNotInitialized = Class.new(ExecutorError)
21
+
22
+ def initialize(operation_class)
23
+ @operation_class = operation_class
24
+ self
25
+ end
26
+
27
+ attr_reader :operation_class, :operation, :response, :exception, :fail_errors
28
+
29
+ # Initializes the operation instance with the given params
30
+ # Raises OperationAlreadyInitialized if called twice without reset
31
+ # : (**untyped) -> Executor
32
+ def new(**params)
33
+ raise OperationAlreadyInitialized if operation
34
+
35
+ @operation = operation_class.new(**params)
36
+ self
37
+ end
38
+
39
+ # Forces reinitialization of the operation instance
40
+ # : (**untyped) -> Executor
41
+ def new!(**params)
42
+ @operation = nil
43
+ new(**params)
44
+ end
45
+
46
+ # Runs the operation (initialize if params provided)
47
+ # : (**untyped) -> Executor
48
+ def perform(**params)
49
+ new(**params) if params.present?
50
+ execute
51
+ end
52
+
53
+ # Forces execution (reset and run)
54
+ # : (**untyped) -> Executor
55
+ def perform!(**params)
56
+ clear_operation!.new(**params) if params.present?
57
+ clear!
58
+ execute
59
+ end
60
+
61
+ def fail? = !success?
62
+ def success? = errors.respond_to?(:empty?) ? errors.empty? : !errors
63
+ alias failure? fail?
64
+ alias ok? success?
65
+
66
+ # Returns errors from failure or exception
67
+ # : () -> untyped
68
+ def errors
69
+ @errors ||= fail_errors || exception_error
70
+ end
71
+
72
+ # Clears only the operation instance
73
+ # : () -> Executor
74
+ def clear_operation!
75
+ @operation = nil
76
+ self
77
+ end
78
+
79
+ # Clears execution state (response, error, exception)
80
+ # : () -> Executor
81
+ def clear!
82
+ @response = nil
83
+ @exception = nil
84
+ @fail_errors = nil
85
+ @errors = nil
86
+ self
87
+ end
88
+
89
+ # Clears everything (operation and state)
90
+ # : () -> Executor
91
+ def reset! = clear_operation!.clear!
92
+
93
+ # Binds success/fail handlers with method names
94
+ # : (success: Symbol?, fail: Symbol?) -> Executor
95
+ # Examples:
96
+ # executor.on(success: :dashboard, fail: :show_error)
97
+ # executor.on(success: -> () { render :done })
98
+ # executor.on(success: -> (response) { render :done, locals: { model: response.subject } })
99
+ # executor.on(success: -> (_response, operation) { render :done, locals: { model: operation.response.subject } })
100
+ def on(actions_with_responses = {})
101
+ actions_assign(actions_with_responses, :success, :fail)
102
+ end
103
+
104
+ # Binds success handler with method name or block
105
+ # : (Symbol?) { (Executor) -> void } -> Executor
106
+ # Examples:
107
+ # executor.on_success(:dashboard)
108
+ # executor.on_success { render :done }
109
+ # executor.on_success { |response| render :done, locals: { model: response.subject } }
110
+ # executor.on_success { |_response, operation| render :done, locals: { model: operation.response.subject } }
111
+ def on_success(binded_method = nil, &block)
112
+ actions[:success] = binded_method || block
113
+ self
114
+ end
115
+
116
+ # Binds failure handler with method name or block
117
+ # : (Symbol?) { (Executor) -> void } -> Executor
118
+ # Examples:
119
+ # executor.on_fail(:show_error)
120
+ # executor.on_fail { render :error }
121
+ # executor.on_fail { |response| render :error, locals: { error: response.errors } }
122
+ # executor.on_fail { |_response, operation| render :error, locals: { error: operation.errors } }
123
+ def on_fail(binded_method = nil, &block)
124
+ actions[:fail] = binded_method || block
125
+ self
126
+ end
127
+
128
+ # Assigns object to call symbol handlers on
129
+ # : (Object) -> Executor
130
+ def bind_with(bind_object)
131
+ @bind_object = bind_object
132
+ self
133
+ end
134
+
135
+ # Removes bound handler object
136
+ # : () -> Executor
137
+ def unbind!
138
+ @bind_object = nil
139
+ self
140
+ end
141
+
142
+ # Forces failure with custom error payload
143
+ # : (untyped) -> Executor
144
+ def fail!(fail_obj = true)
145
+ @errors = nil
146
+ @fail_errors = fail_obj
147
+ self
148
+ end
149
+
150
+ # Accessor for action handlers
151
+ # : () -> Hash[Symbol, Proc | Symbol]
152
+ def actions
153
+ @actions ||= {}
154
+ end
155
+
156
+ private
157
+
158
+ def exception_error
159
+ return unless exception
160
+
161
+ {
162
+ exception: {
163
+ class: exception.class.name,
164
+ message: exception.respond_to?(:message) ? exception.message : exception.to_s
165
+ }
166
+ }
167
+ end
168
+
169
+ def execute
170
+ raise OperationNotInitialized unless operation
171
+
172
+ @response = operation.perform
173
+ execute_actions
174
+ self
175
+ rescue => exception
176
+ raise if exception.is_a?(ExecutorError)
177
+ @exception = exception
178
+ execute_actions
179
+ self
180
+ end
181
+
182
+ attr_reader :bind_object
183
+
184
+ def actions_assign(hash, *keys)
185
+ keys.each { |key| actions[key] = hash[key] if hash.key?(key) }
186
+ self
187
+ end
188
+
189
+ def execute_action_kind(kind)
190
+ return unless actions.key?(kind)
191
+
192
+ action = actions[kind]
193
+
194
+ if action.is_a?(Symbol) && bind_object
195
+ arity = bind_object.method(action).arity
196
+ case arity
197
+ when 0 then bind_object.send(action)
198
+ when 1 then bind_object.send(action, response)
199
+ else bind_object.send(action, response, self)
200
+ end
201
+ elsif action.is_a?(Proc)
202
+ case action.arity
203
+ when 0 then action.call
204
+ when 1 then action.call(response)
205
+ else action.call(response, self)
206
+ end
207
+ end
208
+
209
+ # bind_object.send(action, self) if action.is_a?(Symbol) && bind_object
210
+ # action.call(self) if action.is_a?(Proc)
211
+ rescue NameError => e
212
+ raise ExecutorError, "Action '#{action}' not found in #{bind_object.class.name}" if bind_object
213
+ end
214
+
215
+ def execute_actions
216
+ success? ? execute_action_kind(:success) : execute_action_kind(:fail)
217
+ self
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,33 @@
1
+ module Toolx::Core::Operation::ParamsWrapper
2
+ def initialize(*args, **kwargs)
3
+ params = args.first.nil? ? kwargs : args.first
4
+ if self.class.const_defined?(:Params, false) && !params.is_a?(self.class.const_get(:Params, false))
5
+ begin
6
+ params = self.class.const_get(:Params, false).new(params.deep_symbolize_keys)
7
+ rescue Dry::Struct::Error => e
8
+ err = ArgumentError.new <<~MSG
9
+ #{e.message}
10
+ Provided: #{params.inspect}\n
11
+ MSG
12
+ err.set_backtrace(e.backtrace)
13
+ raise err
14
+ end
15
+ end
16
+ @params = params
17
+ if args.present? || kwargs.present?
18
+ super
19
+ else
20
+ super()
21
+ end
22
+ end
23
+
24
+ def self.prepended(base)
25
+ class << base
26
+ prepend ClassMethods
27
+ end
28
+ end
29
+
30
+ module ClassMethods
31
+ def params(&block) = const_set(:Params, Class.new(Dry::Struct, &block))
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_support'
2
+
3
+ module Toolx::Core::Operation::RescueWrapper
4
+ def perform(*args, **kwargs)
5
+ if args.present? || kwargs.present?
6
+ super
7
+ else
8
+ super()
9
+ end
10
+ rescue => exception
11
+ handler = handler_for_rescue(exception)
12
+ return handler.call(exception) if handler
13
+
14
+ raise
15
+ end
16
+
17
+ def self.prepended(base)
18
+ base.include ::ActiveSupport::Rescuable
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module Toolx::Core::Operation::ResponseWrapper
2
+ def perform(*args, **params)
3
+ response = if args.present? || params.present?
4
+ super
5
+ else
6
+ super()
7
+ end
8
+
9
+ if self.class.constants.include?(:Response) && !response.is_a?(self.class.const_get(:Response))
10
+ begin
11
+ response = self.class.const_get(:Response).new(response.to_h.deep_symbolize_keys)
12
+ rescue Dry::Struct::Error => e
13
+ err = ArgumentError.new <<~MSG
14
+ #{e.message}
15
+ Provided: #{response.inspect}\n
16
+ MSG
17
+ err.set_backtrace(e.backtrace)
18
+ raise err
19
+ end
20
+ end
21
+
22
+ response.is_a?(Hash) ? ::Hashie::Mash.new(response) : response
23
+ end
24
+
25
+ def self.prepended(base)
26
+ class << base
27
+ prepend ClassMethods
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def response(&block) = const_set(:Response, Class.new(Dry::Struct, &block))
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ module Toolx::Core::Operation::SimplifiedResult
2
+ def simplified_result(attr_name)
3
+ # This module allows defining a simplified accessor for the result of an Operation.
4
+ # When included and `simplified_result :attribute_name` is called,
5
+ # it adds a class-level `[]` and `.()` method that initializes the operation,
6
+ # performs it, and returns the specified attribute from the result.
7
+ #
8
+ # Example:
9
+ #
10
+ # class Country::Name < Operation
11
+ # simplified_result :value
12
+ # # ...
13
+ # end
14
+ #
15
+ # Country::Name[source: "PL"] # => returns the value attribute from the result
16
+ # or
17
+ # Country::Name.call(source: "PL") # => same as above
18
+ # Country::Name.(source: "PL") # => same as above
19
+ #
20
+ # This is useful for operations that return a single value
21
+ # and should provide a concise and readable API.
22
+ define_singleton_method(:[]) do |args = {}|
23
+ # new(**args).perform.public_send(attr_name)
24
+ begin
25
+ new(**args).perform.public_send(attr_name)
26
+ rescue TypeError, ArgumentError => e
27
+ required = if const_defined?(:Params, false)
28
+ const_get(:Params, false).schema.keys.join(', ')
29
+ else
30
+ 'Operation params are wrongly assigned or not defined'
31
+ end
32
+
33
+ raise ArgumentError, <<~MSG
34
+ Expected #{self.name} keyword args: #{required}
35
+ Got: #{args.inspect}
36
+ #{e.message}
37
+ MSG
38
+ end
39
+ end
40
+
41
+ class << self
42
+ alias_method :call, :[]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module Toolx::Core::Operation::TransactionWrapper
2
+ def perform(*args, **params)
3
+ wrapper = proc do
4
+ if args.present? || params.present?
5
+ super
6
+ else
7
+ super()
8
+ end
9
+ end
10
+
11
+ connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false # rubocop:disable Style/RescueModifier
12
+ if connected && self.class.transactional?
13
+ ActiveRecord::Base.transaction(&wrapper)
14
+ else
15
+ wrapper.call
16
+ end
17
+ end
18
+
19
+ def self.prepended(base)
20
+ class << base
21
+ prepend ClassMethods
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ def transactional? = !@_disable_transactions
27
+
28
+ def transactional=(enabled)
29
+ @_disable_transactions = !enabled
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ class Toolx::Core::Operation < ::Toolx::Core::OperationBase
2
+ require_relative 'operation/callbacks_wrapper'
3
+ require_relative 'operation/transaction_wrapper'
4
+ require_relative 'operation/params_wrapper'
5
+ require_relative 'operation/response_wrapper'
6
+ require_relative 'operation/rescue_wrapper'
7
+ require_relative 'operation/simplified_result'
8
+ require_relative 'operation/flow'
9
+
10
+ extend ::Toolx::Core::Operation::SimplifiedResult
11
+
12
+ class Error < ::Toolx::Core::Errors::NestedStandardError; end
13
+
14
+ attr_reader :params
15
+
16
+ def self.prepend_builtins(subclass)
17
+ subclass.prepend ::Toolx::Core::Operation::CallbacksWrapper
18
+ subclass.prepend ::Toolx::Core::Operation::TransactionWrapper
19
+ subclass.prepend ::Toolx::Core::Operation::ParamsWrapper
20
+ subclass.prepend ::Toolx::Core::Operation::ResponseWrapper
21
+ subclass.prepend ::Toolx::Core::Operation::RescueWrapper
22
+ end
23
+
24
+ def self.inherited(subclass)
25
+ prepend_builtins(subclass)
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ class Toolx::Core::OperationBase
2
+ def initialize(*, **) end
3
+ def perform; end
4
+ end
@@ -0,0 +1,50 @@
1
+ class Toolx::Core::Presenter < SimpleDelegator
2
+ NoPresenterDetected = Class.new(StandardError)
3
+
4
+ def initialize(object, **opts)
5
+ super(object)
6
+ @__opts = opts || {}
7
+ end
8
+
9
+ attr_reader :__opts
10
+ def original_object = __getobj__
11
+
12
+ # Public: Constructs and returns a new presenter instance.
13
+ # Accepts flexible arguments to support subclasses with custom initializers.
14
+ #
15
+ # Example:
16
+ # MyPresenter.present(user, show_deleted: true)
17
+ def self.present(*args, **kwargs, &block) = new(*args, **kwargs, &block)
18
+
19
+ # Public: Returns a Proc that instantiates the presenter.
20
+ # Allows usage like: `Model.all.map(&MyPresenter)`
21
+ def self.to_proc = ->(object) { present(object) }
22
+
23
+ # Public: Returns a Proc that instantiates the presenter and optionally calls a method on it.
24
+ # Useful for transforming collections with presentation logic.
25
+ #
26
+ # Example:
27
+ # Model.all.map(&MyPresenter.as_proc(use: :to_json))
28
+ def self.as_proc(*args, **kwargs, &block)
29
+ use = kwargs.delete(:use)
30
+ ->(object) do
31
+ presenter = present(object, *args, **kwargs, &block)
32
+ use ? presenter.public_send(use) : presenter
33
+ end
34
+ end
35
+
36
+ def self.detect(object)
37
+ object.class::Presenter
38
+ rescue NameError
39
+ raise NoPresenterDetected, "No presenter detected for #{object.class.name}. Please define a Presenter constant in the class."
40
+ end
41
+
42
+ def self.auto_present(object) = detect(object).present(object)
43
+
44
+ def t!(key, opts = {}) = I18n.t!([t_default_key, key].join('.'), **opts.merge(locale: __opts[:locale] || :pl))
45
+ def t(key, opts = {}) = I18n.t([t_default_key, key].join('.'), **opts.merge(locale: __opts[:locale] || :pl))
46
+
47
+ def t_default_key
48
+ @t_default_key ||= self.class.name.sub(/Presenter$/, '').split('::').compact.join('.').underscore
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Toolx::Core::SimpleCrypt
4
+ MissingSecretError = Class.new(StandardError)
5
+ EncryptionError = Class.new(StandardError)
6
+ DecryptionError = Class.new(StandardError)
7
+
8
+ def self.default_secret
9
+ ENV['CRYPT_SECRET'].presence || Rails.application.credentials.secret_key_base.presence || raise(MissingSecretError, 'Encryption secret not configured')
10
+ end
11
+
12
+ def self.crypt(secret = nil)
13
+ key = secret.presence || default_secret
14
+ ActiveSupport::MessageEncryptor.new(key[0..31].bytes.pack('c*'))
15
+ end
16
+
17
+ def self.encrypt(value, secret: nil)
18
+ crypt(secret).encrypt_and_sign(value)
19
+ rescue => e
20
+ raise EncryptionError, "Encryption failed: #{e.message}"
21
+ end
22
+
23
+ def self.decrypt(value, secret: nil)
24
+ crypt(secret).decrypt_and_verify(value)
25
+ rescue => e
26
+ raise DecryptionError, "Decryption failed: #{e.message}"
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toolx
4
+ VERSION = '0.1.0'
5
+ end
data/lib/toolx.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'toolx/version'
4
+
5
+ # Concerns
6
+ Dir[File.join(__dir__, 'toolx/core/concerns/**/*.rb')].sort.each { |f| require f }
7
+
8
+ require_relative 'toolx/core/env'
9
+
10
+ require_relative 'toolx/core/errors/nested_error'
11
+ require_relative 'toolx/core/errors/nested_standard_error'
12
+ require_relative 'toolx/core/errors/app_error'
13
+ require_relative 'toolx/core/errors/api_error'
14
+
15
+ require_relative 'toolx/core/simple_crypt'
16
+ require_relative 'toolx/core/form'
17
+ require_relative 'toolx/core/presenter'
18
+
19
+ require_relative 'toolx/core/operation_base'
20
+ require_relative 'toolx/core/operation'
21
+
22
+ module Toolx
23
+ end
24
+
25
+ if defined?(Rake)
26
+ spec = Gem::Specification.find_by_name 'toolx'
27
+ load "#{spec.gem_dir}/lib/tasks/stateman.rake"
28
+ load "#{spec.gem_dir}/lib/tasks/annotate_rb.rake"
29
+ load "#{spec.gem_dir}/lib/tasks/aliases.rake"
30
+ end
data/sig/toolx.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Toolx
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end