simple_command_dispatcher 4.0.0 → 4.2.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 +4 -4
- data/CHANGELOG.md +61 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +45 -20
- data/README.md +408 -262
- data/lib/simple_command_dispatcher/commands/command_callable.rb +104 -0
- data/lib/simple_command_dispatcher/commands/errors.rb +83 -0
- data/lib/simple_command_dispatcher/commands/utils.rb +20 -0
- data/lib/simple_command_dispatcher/configuration.rb +25 -6
- data/lib/simple_command_dispatcher/helpers/camelize.rb +1 -1
- data/lib/simple_command_dispatcher/logger.rb +21 -0
- data/lib/simple_command_dispatcher/services/command_service.rb +31 -31
- data/lib/simple_command_dispatcher/services/options_service.rb +37 -0
- data/lib/simple_command_dispatcher/version.rb +1 -1
- data/lib/simple_command_dispatcher.rb +38 -6
- data/simple_command_dispatcher.gemspec +6 -7
- metadata +19 -14
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'errors'
|
4
|
+
|
5
|
+
module SimpleCommandDispatcher
|
6
|
+
module Commands
|
7
|
+
# CommandCallable provides a standardized interface for command objects with built-in
|
8
|
+
# success/failure tracking and error handling.
|
9
|
+
#
|
10
|
+
# When prepended to a command class, it:
|
11
|
+
# - Adds a class-level `.call` method that instantiates and executes the command
|
12
|
+
# - Tracks command execution with `success?` and `failure?` methods
|
13
|
+
# - Provides error collection via the `errors` object
|
14
|
+
# - Stores the command's return value in `result`
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# class AuthenticateUser
|
18
|
+
# prepend SimpleCommandDispatcher::Commands::CommandCallable
|
19
|
+
#
|
20
|
+
# def initialize(email:, password:)
|
21
|
+
# @email = email
|
22
|
+
# @password = password
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def call
|
26
|
+
# return nil unless user = User.find_by(email: @email)
|
27
|
+
# return nil unless user.authenticate(@password)
|
28
|
+
#
|
29
|
+
# user
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# command = AuthenticateUser.call(email: 'user@example.com', password: 'secret')
|
34
|
+
# command.success? # => true if user found and authenticated
|
35
|
+
# command.result # => User object or nil
|
36
|
+
module CommandCallable
|
37
|
+
# @return [Object] the return value from the command's call method
|
38
|
+
attr_reader :result
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
# Creates a new instance of the command and calls it, passing all arguments through.
|
42
|
+
#
|
43
|
+
# @param args [Array] positional arguments passed to initialize
|
44
|
+
# @param kwargs [Hash] keyword arguments passed to initialize
|
45
|
+
# @return [Object] the command instance (not the result - use .result to get the return value)
|
46
|
+
def call(...)
|
47
|
+
new(...).call
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.prepended(base)
|
52
|
+
base.extend ClassMethods
|
53
|
+
end
|
54
|
+
|
55
|
+
# Executes the command by calling super (your command's implementation).
|
56
|
+
# Tracks execution state and stores the result.
|
57
|
+
#
|
58
|
+
# @return [self] the command instance for method chaining
|
59
|
+
# @raise [NotImplementedError] if the including class doesn't define a call method
|
60
|
+
def call
|
61
|
+
raise NotImplementedError unless defined?(super)
|
62
|
+
|
63
|
+
@called = true
|
64
|
+
@result = super
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if the command was called successfully (no errors).
|
70
|
+
#
|
71
|
+
# @return [Boolean] true if called and no errors present
|
72
|
+
def success?
|
73
|
+
called? && !failure?
|
74
|
+
end
|
75
|
+
alias successful? success?
|
76
|
+
|
77
|
+
# Returns true if the command was called but has errors.
|
78
|
+
#
|
79
|
+
# @return [Boolean] true if called and errors are present
|
80
|
+
def failure?
|
81
|
+
called? && errors.any?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the errors collection for this command.
|
85
|
+
# If the command class defines its own errors method, that will be used instead.
|
86
|
+
#
|
87
|
+
# @return [Errors] the errors collection
|
88
|
+
def errors
|
89
|
+
return super if defined?(super)
|
90
|
+
|
91
|
+
@errors ||= Errors.new
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Returns true if the command's call method has been invoked.
|
97
|
+
#
|
98
|
+
# @return [Boolean] true if call has been invoked
|
99
|
+
def called?
|
100
|
+
@called ||= false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
|
5
|
+
module SimpleCommandDispatcher
|
6
|
+
module Commands
|
7
|
+
module CommandCallable
|
8
|
+
# Raised when a command's call method is not implemented
|
9
|
+
class NotImplementedError < ::StandardError; end
|
10
|
+
|
11
|
+
# Error collection for CommandCallable commands.
|
12
|
+
# Stores validation errors as a hash where keys are field names and values are arrays of error messages.
|
13
|
+
class Errors < Hash
|
14
|
+
# Adds an error message to the specified field.
|
15
|
+
# Automatically prevents duplicate messages for the same field.
|
16
|
+
#
|
17
|
+
# @param key [Symbol, String] the field name
|
18
|
+
# @param value [String] the error message
|
19
|
+
# @param _opts [Hash] reserved for future use
|
20
|
+
# @return [Array] the updated array of error messages for this field
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# errors.add(:email, 'is required')
|
24
|
+
# errors.add(:email, 'is invalid')
|
25
|
+
# errors[:email] # => ['is required', 'is invalid']
|
26
|
+
def add(key, value, _opts = {})
|
27
|
+
self[key] ||= []
|
28
|
+
self[key] << value
|
29
|
+
self[key].uniq!
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds multiple errors from a hash.
|
33
|
+
# Values can be single messages or arrays of messages.
|
34
|
+
#
|
35
|
+
# @param errors_hash [Hash] hash of field names to error message(s)
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# errors.add_multiple_errors(email: 'is required', password: ['is too short', 'is too weak'])
|
39
|
+
def add_multiple_errors(errors_hash)
|
40
|
+
errors_hash.each do |key, values|
|
41
|
+
CommandCallable::Utils.array_wrap(values).each { |value| add key, value }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Iterates over each field and message pair.
|
46
|
+
# If a field has multiple messages, yields once for each message.
|
47
|
+
#
|
48
|
+
# @yieldparam field [Symbol] the field name
|
49
|
+
# @yieldparam message [String] the error message
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# errors.each { |field, message| puts "#{field}: #{message}" }
|
53
|
+
def each
|
54
|
+
each_key do |field|
|
55
|
+
self[field].each { |message| yield field, message }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an array of formatted error messages.
|
60
|
+
# Messages are prefixed with the capitalized field name, except for :base.
|
61
|
+
#
|
62
|
+
# @return [Array<String>] formatted error messages
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# errors.add(:email, 'is required')
|
66
|
+
# errors.add(:base, 'Something went wrong')
|
67
|
+
# errors.full_messages # => ['Email is required', 'Something went wrong']
|
68
|
+
def full_messages
|
69
|
+
map { |attribute, message| full_message(attribute, message) }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def full_message(attribute, message)
|
75
|
+
return message if attribute == :base
|
76
|
+
|
77
|
+
attr_name = attribute.to_s.tr('.', '_').capitalize
|
78
|
+
"#{attr_name} #{message}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCommandDispatcher
|
4
|
+
module Commands
|
5
|
+
module CommandCallable
|
6
|
+
module Utils
|
7
|
+
# Borrowed from active_support/core_ext/array/wrap
|
8
|
+
def self.array_wrap(object)
|
9
|
+
if object.nil?
|
10
|
+
[]
|
11
|
+
elsif object.respond_to?(:to_ary)
|
12
|
+
object.to_ary || [object]
|
13
|
+
else
|
14
|
+
[object]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -3,8 +3,6 @@
|
|
3
3
|
# This is the configuration for SimpleCommandDispatcher.
|
4
4
|
module SimpleCommandDispatcher
|
5
5
|
class << self
|
6
|
-
attr_reader :configuration
|
7
|
-
|
8
6
|
# Configures SimpleCommandDispatcher by yielding the configuration object to the block.
|
9
7
|
#
|
10
8
|
# @yield [Configuration] yields the configuration object to the block
|
@@ -13,7 +11,7 @@ module SimpleCommandDispatcher
|
|
13
11
|
# @example
|
14
12
|
#
|
15
13
|
# SimpleCommandDispatcher.configure do |config|
|
16
|
-
# config.
|
14
|
+
# config.logger = Rails.logger
|
17
15
|
# end
|
18
16
|
def configure
|
19
17
|
self.configuration ||= Configuration.new
|
@@ -23,6 +21,13 @@ module SimpleCommandDispatcher
|
|
23
21
|
configuration
|
24
22
|
end
|
25
23
|
|
24
|
+
# Returns the configuration object, initializing it if necessary
|
25
|
+
#
|
26
|
+
# @return [Configuration] the configuration object
|
27
|
+
def configuration
|
28
|
+
@configuration ||= Configuration.new
|
29
|
+
end
|
30
|
+
|
26
31
|
private
|
27
32
|
|
28
33
|
attr_writer :configuration
|
@@ -31,16 +36,30 @@ module SimpleCommandDispatcher
|
|
31
36
|
# This class encapsulates the configuration properties for this gem and
|
32
37
|
# provides methods and attributes that allow for management of the same.
|
33
38
|
class Configuration
|
34
|
-
#
|
39
|
+
# @return [Logger] the logger instance used for debug output.
|
40
|
+
# Defaults to Rails.logger in Rails applications, or Logger.new($stdout) otherwise.
|
41
|
+
attr_accessor :logger
|
35
42
|
|
36
43
|
# Initializes a new Configuration instance with default values
|
37
44
|
def initialize
|
38
45
|
reset
|
39
46
|
end
|
40
47
|
|
41
|
-
# Resets all configuration attributes to their default values
|
48
|
+
# Resets all configuration attributes to their default values.
|
49
|
+
# Sets logger to Rails.logger if Rails is defined, otherwise creates a new Logger writing to $stdout.
|
42
50
|
def reset
|
43
|
-
|
51
|
+
@logger = default_logger
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def default_logger
|
57
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
58
|
+
Rails.logger
|
59
|
+
else
|
60
|
+
require 'logger'
|
61
|
+
::Logger.new($stdout)
|
62
|
+
end
|
44
63
|
end
|
45
64
|
end
|
46
65
|
end
|
@@ -25,7 +25,7 @@ module SimpleCommandDispatcher
|
|
25
25
|
# For RESTful paths → Ruby constants, use Rails' proven methods
|
26
26
|
# They're fast, reliable, and handle edge cases that matter for constants
|
27
27
|
result = trim_all(token)
|
28
|
-
.gsub(%r{[
|
28
|
+
.gsub(%r{[/\-.\s:]+}, '/') # Normalize separators to /
|
29
29
|
.split('/') # Split into path segments
|
30
30
|
.reject(&:empty?) # Remove empty segments
|
31
31
|
.map { |segment| segment.underscore.camelize } # Rails camelization
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCommandDispatcher
|
4
|
+
# Provides logging functionality for SimpleCommandDispatcher.
|
5
|
+
# Supports configuration to use Rails logger or custom loggers.
|
6
|
+
module Logger
|
7
|
+
private
|
8
|
+
|
9
|
+
def log_debug(string)
|
10
|
+
logger.debug(string) if logger.respond_to?(:debug)
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_error(string)
|
14
|
+
logger.error(string) if logger.respond_to?(:error)
|
15
|
+
end
|
16
|
+
|
17
|
+
def logger
|
18
|
+
SimpleCommandDispatcher.configuration.logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../errors'
|
4
4
|
require_relative '../helpers/camelize'
|
5
|
+
require_relative '../logger'
|
5
6
|
require_relative 'command_namespace_service'
|
6
7
|
|
7
8
|
module SimpleCommandDispatcher
|
@@ -9,8 +10,10 @@ module SimpleCommandDispatcher
|
|
9
10
|
# Handles class and module transformations and instantiation.
|
10
11
|
class CommandService
|
11
12
|
include Helpers::Camelize
|
13
|
+
include Logger
|
12
14
|
|
13
|
-
def initialize(command:, command_namespace: {})
|
15
|
+
def initialize(command:, command_namespace: {}, options: {})
|
16
|
+
@options = options
|
14
17
|
@command = validate_command(command:)
|
15
18
|
@command_namespace = validate_command_namespace(command_namespace:)
|
16
19
|
end
|
@@ -25,15 +28,24 @@ module SimpleCommandDispatcher
|
|
25
28
|
#
|
26
29
|
# @example
|
27
30
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
+
# CommandService.new(command: "Authenticate", command_namespace: "Api").to_class
|
32
|
+
# # => Api::Authenticate
|
33
|
+
# CommandService.new(command: :Authenticate, command_namespace: [:Api, :AppName, :V1]).to_class
|
34
|
+
# # => Api::AppName::V1::Authenticate
|
35
|
+
# CommandService.new(command: :Authenticate,
|
36
|
+
# command_namespace: { api: :Api, app_name: :AppName, api_version: :V2 }).to_class
|
31
37
|
# # => Api::AppName::V2::Authenticate
|
32
|
-
#
|
33
|
-
#
|
38
|
+
# CommandService.new(command: "authenticate", command_namespace: "api::app_name::v1").to_class
|
39
|
+
# # => Api::AppName::V1::Authenticate
|
34
40
|
#
|
35
41
|
def to_class
|
36
|
-
qualified_class_string = to_qualified_class_string
|
42
|
+
qualified_class_string = to_qualified_class_string
|
43
|
+
|
44
|
+
if options.debug?
|
45
|
+
log_debug <<~DEBUG
|
46
|
+
Command to execute: #{qualified_class_string.inspect}
|
47
|
+
DEBUG
|
48
|
+
end
|
37
49
|
|
38
50
|
begin
|
39
51
|
qualified_class_string.constantize
|
@@ -44,24 +56,13 @@ module SimpleCommandDispatcher
|
|
44
56
|
|
45
57
|
private
|
46
58
|
|
47
|
-
|
59
|
+
attr_reader :options, :command, :command_namespace
|
48
60
|
|
49
61
|
# Returns a fully-qualified constantized class (as a string), given the command and command_namespace.
|
50
|
-
# Both parameters are automatically camelized/titleized during processing.
|
51
|
-
#
|
52
|
-
# @param command [Symbol, String] the class name.
|
53
|
-
# @param command_namespace [Hash, Array, String] the modules command belongs to.
|
54
62
|
#
|
55
63
|
# @return [String] the fully qualified class, which includes module(s) and class name.
|
56
64
|
#
|
57
|
-
|
58
|
-
#
|
59
|
-
# to_qualified_class_string("authenticate", "api") # => "Api::Authenticate"
|
60
|
-
# to_qualified_class_string(:Authenticate, [:Api, :AppName, :V1]) # => "Api::AppName::V1::Authenticate"
|
61
|
-
# to_qualified_class_string(:authenticate, { api: :api, app_name: :app_name, api_version: :v1 })
|
62
|
-
# # => "Api::AppName::V1::Authenticate"
|
63
|
-
#
|
64
|
-
def to_qualified_class_string(command, command_namespace)
|
65
|
+
def to_qualified_class_string
|
65
66
|
class_modules_string = CommandNamespaceService.new(command_namespace:).to_class_modules_string
|
66
67
|
class_string = to_class_string(command:)
|
67
68
|
"#{class_modules_string}#{class_string}"
|
@@ -87,19 +88,18 @@ module SimpleCommandDispatcher
|
|
87
88
|
|
88
89
|
# @!visibility public
|
89
90
|
#
|
90
|
-
# Validates command and returns command as a string after
|
91
|
-
# command.gsub(/\s+/, "").
|
91
|
+
# Validates command and returns command as a string after leading and trailing whitespace is stripped.
|
92
92
|
#
|
93
|
-
# @param [Symbol
|
93
|
+
# @param command [Symbol, String] the class name to be validated. command cannot be empty after stripping.
|
94
94
|
#
|
95
|
-
# @return [String] the validated class as a string with
|
95
|
+
# @return [String] the validated class as a string with leading/trailing whitespace removed.
|
96
96
|
#
|
97
97
|
# @raise [ArgumentError] if the command is empty? or not of type String or Symbol.
|
98
98
|
#
|
99
99
|
# @example
|
100
100
|
#
|
101
|
-
# validate_command("
|
102
|
-
# validate_command(:MyClass) # => "MyClass"
|
101
|
+
# validate_command(command: " MyClass ") # => "MyClass"
|
102
|
+
# validate_command(command: :MyClass) # => "MyClass"
|
103
103
|
#
|
104
104
|
def validate_command(command:)
|
105
105
|
unless command.is_a?(Symbol) || command.is_a?(String)
|
@@ -119,17 +119,17 @@ module SimpleCommandDispatcher
|
|
119
119
|
#
|
120
120
|
# Validates and returns command_namespace.
|
121
121
|
#
|
122
|
-
# @param [Hash, Array
|
122
|
+
# @param command_namespace [Hash, Array, String] the module(s) to be validated.
|
123
123
|
#
|
124
|
-
# @return [Hash, Array
|
124
|
+
# @return [Hash, Array, String] the validated module(s), or {} if blank.
|
125
125
|
#
|
126
126
|
# @raise [ArgumentError] if the command_namespace is not of type String, Hash or Array.
|
127
127
|
#
|
128
128
|
# @example
|
129
129
|
#
|
130
|
-
# validate_command_namespace(
|
131
|
-
# validate_command_namespace(:
|
132
|
-
# validate_command_namespace(
|
130
|
+
# validate_command_namespace(command_namespace: "Api::V1") # => "Api::V1"
|
131
|
+
# validate_command_namespace(command_namespace: [:Api, :V1]) # => [:Api, :V1]
|
132
|
+
# validate_command_namespace(command_namespace: { api: :Api, version: :V1 }) # => { api: :Api, version: :V1 }
|
133
133
|
#
|
134
134
|
def validate_command_namespace(command_namespace:)
|
135
135
|
return {} if command_namespace.blank?
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCommandDispatcher
|
4
|
+
module Services
|
5
|
+
# Handles options for command execution and ensures proper initialization.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# options = OptionsService.new(options: { debug: true })
|
9
|
+
# options.debug? # => true
|
10
|
+
class OptionsService
|
11
|
+
# Default options for command execution
|
12
|
+
DEFAULT_OPTIONS = {
|
13
|
+
debug: false
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# Initializes the options service with the provided options merged with defaults.
|
17
|
+
#
|
18
|
+
# @param options [Hash] custom options
|
19
|
+
# @option options [Boolean] :debug (false) enables debug logging when true
|
20
|
+
def initialize(options: {})
|
21
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns true if debug mode is enabled.
|
25
|
+
# When enabled, debug logging will show command execution flow.
|
26
|
+
#
|
27
|
+
# @return [Boolean] true if debug mode is enabled
|
28
|
+
def debug?
|
29
|
+
options[:debug]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -3,12 +3,17 @@
|
|
3
3
|
require 'active_support/core_ext/object/blank'
|
4
4
|
require 'active_support/core_ext/string/inflections'
|
5
5
|
require 'core_ext/kernel'
|
6
|
+
require 'simple_command_dispatcher/commands/command_callable'
|
6
7
|
require 'simple_command_dispatcher/configuration'
|
7
8
|
require 'simple_command_dispatcher/errors'
|
9
|
+
require 'simple_command_dispatcher/logger'
|
8
10
|
require 'simple_command_dispatcher/services/command_service'
|
11
|
+
require 'simple_command_dispatcher/services/options_service'
|
9
12
|
require 'simple_command_dispatcher/version'
|
10
13
|
|
11
14
|
module SimpleCommandDispatcher
|
15
|
+
extend Logger
|
16
|
+
|
12
17
|
# Provides a way to call your custom commands dynamically.
|
13
18
|
#
|
14
19
|
class << self
|
@@ -17,10 +22,10 @@ module SimpleCommandDispatcher
|
|
17
22
|
#
|
18
23
|
# @param command [Symbol, String] the name of the Command to call.
|
19
24
|
#
|
20
|
-
# @param command_namespace [Hash, Array] the ruby modules that qualify the Command to call.
|
25
|
+
# @param command_namespace [Hash, Array, String] the ruby modules that qualify the Command to call.
|
21
26
|
# When passing a Hash, the Hash keys serve as documentation only.
|
22
|
-
# For example, ['Api', 'AppName', 'V1'] and { :api :Api, app_name: :AppName, api_version: :V1 }
|
23
|
-
# will
|
27
|
+
# For example, ['Api', 'AppName', 'V1'], 'Api::AppName::V1', and { :api :Api, app_name: :AppName, api_version: :V1 }
|
28
|
+
# will all produce 'Api::AppName::V1', this string will be prepended to the command to form the Command
|
24
29
|
# to call (e.g. 'Api::AppName::V1::MySimpleCommand' = Api::AppName::V1::MySimpleCommand.call(*request_params)).
|
25
30
|
#
|
26
31
|
# @param request_params [Hash, Array, Object] the parameters to pass to the call method of the Command. This
|
@@ -28,6 +33,10 @@ module SimpleCommandDispatcher
|
|
28
33
|
# keyword arguments, Array parameters are passed as positional arguments, and other objects are passed
|
29
34
|
# as a single argument.
|
30
35
|
#
|
36
|
+
# @param options [Hash] optional configuration for command execution.
|
37
|
+
# Supported options:
|
38
|
+
# - :debug [Boolean] when true, enables debug logging of command execution flow
|
39
|
+
#
|
31
40
|
# @return [Object] the Object returned as a result of calling the Command#call method.
|
32
41
|
#
|
33
42
|
# @example
|
@@ -49,20 +58,43 @@ module SimpleCommandDispatcher
|
|
49
58
|
# command_namespace: ['Api::Auth::JazzMeUp', :V1],
|
50
59
|
# request_params: ['jazz_me@gmail.com', 'JazzM3!']) # => Command result
|
51
60
|
#
|
52
|
-
def call(command:, command_namespace: {}, request_params: nil)
|
61
|
+
def call(command:, command_namespace: {}, request_params: nil, options: {})
|
62
|
+
@options = Services::OptionsService.new(options:)
|
63
|
+
|
64
|
+
if @options.debug?
|
65
|
+
log_debug <<~DEBUG
|
66
|
+
Begin dispatching command
|
67
|
+
command: #{command.inspect}
|
68
|
+
command_namespace: #{command_namespace.inspect}
|
69
|
+
DEBUG
|
70
|
+
end
|
71
|
+
|
53
72
|
# Create a constantized class from our command and command_namespace...
|
54
|
-
constantized_class_object = Services::CommandService.new(command:, command_namespace:).to_class
|
73
|
+
constantized_class_object = Services::CommandService.new(command:, command_namespace:, options: @options).to_class
|
74
|
+
|
75
|
+
if @options.debug?
|
76
|
+
log_debug <<~DEBUG
|
77
|
+
Constantized command: #{constantized_class_object.inspect}
|
78
|
+
DEBUG
|
79
|
+
end
|
80
|
+
|
55
81
|
validate_command!(constantized_class_object)
|
56
82
|
|
57
83
|
# We know we have a valid command class object if we get here. All we need to do is call the .call
|
58
84
|
# class method, pass the request_params arguments depending on the request_params data type, and
|
59
85
|
# return the results.
|
60
86
|
|
61
|
-
call_command(constantized_class_object:, request_params:)
|
87
|
+
command_object = call_command(constantized_class_object:, request_params:)
|
88
|
+
|
89
|
+
log_debug 'End dispatching command' if @options.debug?
|
90
|
+
|
91
|
+
command_object
|
62
92
|
end
|
63
93
|
|
64
94
|
private
|
65
95
|
|
96
|
+
attr_reader :options
|
97
|
+
|
66
98
|
def call_command(constantized_class_object:, request_params:)
|
67
99
|
if request_params.is_a?(Hash)
|
68
100
|
constantized_class_object.call(**request_params)
|
@@ -10,12 +10,11 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ['Gene M. Angelo, Jr.']
|
11
11
|
spec.email = ['public.gma@gmail.com']
|
12
12
|
|
13
|
-
spec.summary = '
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
based on the API version. simple_command_dispatcher allows you to execute either command with one line of code dynamically.'.gsub(/\s+/, ' ')
|
13
|
+
spec.summary = 'Dynamic command execution for Rails applications using convention over configuration - automatically maps request routes to command classes.'
|
14
|
+
spec.description = 'A lightweight Ruby gem that enables Rails applications to dynamically execute command objects using convention over configuration. ' \
|
15
|
+
'Automatically transforms request paths into Ruby class constants, allowing controllers to dispatch commands based on routes and parameters. ' \
|
16
|
+
'Features the optional CommandCallable module for standardized command interfaces with built-in success/failure tracking and error handling. ' \
|
17
|
+
'Perfect for clean, maintainable Rails APIs with RESTful route-to-command mapping. Only depends on ActiveSupport for reliable camelization.'
|
19
18
|
spec.homepage = 'https://github.com/gangelo/simple_command_dispatcher'
|
20
19
|
spec.license = 'MIT'
|
21
20
|
|
@@ -37,5 +36,5 @@ Gem::Specification.new do |spec|
|
|
37
36
|
|
38
37
|
spec.required_ruby_version = Gem::Requirement.new('>= 3.3', '< 4.0')
|
39
38
|
|
40
|
-
spec.add_runtime_dependency 'activesupport', '>= 7.0.8', '<
|
39
|
+
spec.add_runtime_dependency 'activesupport', '>= 7.0.8', '< 9.0'
|
41
40
|
end
|