simple_command_dispatcher 4.1.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 +36 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +44 -19
- data/README.md +245 -33
- data/lib/simple_command_dispatcher/commands/command_callable.rb +53 -1
- data/lib/simple_command_dispatcher/commands/errors.rb +39 -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 +37 -6
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f29164d7bff98aa5d0b2aed2e12adf6f4408d06bfa789a142b0cf234f656a95a
|
4
|
+
data.tar.gz: ce2ba1f720a44a1a9ced023a7b2769d2a2702d971a6c0a2643aa5926a2585df4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a929a3e9025cfa05bab78e9ec0a33fe15d1b2ca0f1e5696bb3cc9a455cf01214f0b0a6cec737e026bc39ad95d356451fc2e25abaeadd38f72d3817d8431335f
|
7
|
+
data.tar.gz: 7f99ae544a0c2d31abfe8126a43d44766acb3eba9cd14298cc4f84fbe46ff3d5fcfda1a4734c4e2cac47b978bf56c8f6c288fbb031f5d5f61d6f4d3f31e0fad8
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,41 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## Version 4.2.0 - 2025-10-07
|
4
|
+
|
5
|
+
- **New Feature: Configurable Logger with Debug Mode**:
|
6
|
+
|
7
|
+
- Added configurable logger with automatic Rails.logger detection
|
8
|
+
- Introduced debug mode for command execution debugging via `options: { debug: true }`
|
9
|
+
- Logger can be configured via `SimpleCommandDispatcher.configuration.logger`
|
10
|
+
- Added `OptionsService` for managing command execution options
|
11
|
+
- Enhanced `SimpleCommandDispatcher.call` with `options:` parameter to support debug logging
|
12
|
+
- When debug mode is enabled, detailed debug logging shows command execution flow
|
13
|
+
|
14
|
+
- **Test Coverage Improvements**:
|
15
|
+
|
16
|
+
- Achieved 100% test coverage across all modules (243 examples, 0 failures)
|
17
|
+
- Added comprehensive tests for `CommandCallable::Errors` class (15 new tests)
|
18
|
+
- Added comprehensive tests for `CommandCallable::Utils.array_wrap` method (8 new tests)
|
19
|
+
- Added tests for Rails logger auto-detection in configuration
|
20
|
+
- Added tests for debug mode functionality in command execution
|
21
|
+
- Enhanced test coverage for `OptionsService` and logger integration
|
22
|
+
|
23
|
+
- **Documentation Enhancements**:
|
24
|
+
|
25
|
+
- Updated API documentation for `SimpleCommandDispatcher.call` to include `options` parameter
|
26
|
+
- Added comprehensive documentation for `CommandCallable` module with usage examples
|
27
|
+
- Documented all public methods in `Errors` class with examples
|
28
|
+
- Added documentation for `OptionsService` class and debug mode
|
29
|
+
- Enhanced `Configuration` documentation to include logger attribute
|
30
|
+
- Fixed all YARD documentation to accurately reflect current implementation
|
31
|
+
- Added best practice guidance for private `initialize` in CommandCallable commands
|
32
|
+
|
33
|
+
- **Dependency Updates**:
|
34
|
+
|
35
|
+
- Added `irb` and `reline` gems to development dependencies
|
36
|
+
- Addresses Ruby 3.5 deprecation warnings for extracted standard library gems
|
37
|
+
- Rails 8 compatibility confirmed (supports ActiveSupport 8.x)
|
38
|
+
|
3
39
|
## Version 4.1.0 - 2025-07-14
|
4
40
|
|
5
41
|
- **New Feature: CommandCallable Module**:
|
data/Gemfile
CHANGED
@@ -10,7 +10,9 @@ gem 'colorize', '>= 0.8.1', '< 2.0'
|
|
10
10
|
gem 'rake', '>= 13.0', '< 14.0'
|
11
11
|
|
12
12
|
group :development do
|
13
|
+
gem 'irb', '>= 1.0'
|
13
14
|
gem 'pry-byebug', '>= 3.9', '< 4.0'
|
15
|
+
gem 'reline', '>= 0.3'
|
14
16
|
gem 'rubocop', '>= 1.62', '< 2.0'
|
15
17
|
gem 'rubocop-performance', '>= 1.20', '< 2.0'
|
16
18
|
gem 'rubocop-rake', '>= 0.6', '< 1.0'
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
simple_command_dispatcher (4.
|
4
|
+
simple_command_dispatcher (4.2.0)
|
5
5
|
activesupport (>= 7.0.8, < 9.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activesupport (
|
10
|
+
activesupport (8.0.3)
|
11
11
|
base64
|
12
12
|
benchmark (>= 0.3)
|
13
13
|
bigdecimal
|
@@ -19,41 +19,61 @@ GEM
|
|
19
19
|
minitest (>= 5.1)
|
20
20
|
securerandom (>= 0.3)
|
21
21
|
tzinfo (~> 2.0, >= 2.0.5)
|
22
|
+
uri (>= 0.13.1)
|
22
23
|
ast (2.4.3)
|
23
24
|
base64 (0.3.0)
|
24
25
|
benchmark (0.4.1)
|
25
|
-
bigdecimal (3.
|
26
|
+
bigdecimal (3.3.0)
|
26
27
|
byebug (12.0.0)
|
27
28
|
coderay (1.1.3)
|
28
29
|
colorize (1.1.0)
|
29
30
|
concurrent-ruby (1.3.5)
|
30
|
-
connection_pool (2.5.
|
31
|
+
connection_pool (2.5.4)
|
32
|
+
date (3.4.1)
|
31
33
|
diff-lcs (1.6.2)
|
32
34
|
docile (1.4.1)
|
33
35
|
drb (2.2.3)
|
36
|
+
erb (5.0.3)
|
34
37
|
i18n (1.14.7)
|
35
38
|
concurrent-ruby (~> 1.0)
|
36
|
-
|
39
|
+
io-console (0.8.1)
|
40
|
+
irb (1.15.2)
|
41
|
+
pp (>= 0.6.0)
|
42
|
+
rdoc (>= 4.0.0)
|
43
|
+
reline (>= 0.4.2)
|
44
|
+
json (2.15.1)
|
37
45
|
language_server-protocol (3.17.0.5)
|
38
46
|
lint_roller (1.1.0)
|
39
47
|
logger (1.7.0)
|
40
48
|
method_source (1.1.0)
|
41
49
|
minitest (5.25.5)
|
42
50
|
parallel (1.27.0)
|
43
|
-
parser (3.3.
|
51
|
+
parser (3.3.9.0)
|
44
52
|
ast (~> 2.4.1)
|
45
53
|
racc
|
46
|
-
|
54
|
+
pp (0.6.3)
|
55
|
+
prettyprint
|
56
|
+
prettyprint (0.2.0)
|
57
|
+
prism (1.5.1)
|
47
58
|
pry (0.15.2)
|
48
59
|
coderay (~> 1.1)
|
49
60
|
method_source (~> 1.0)
|
50
61
|
pry-byebug (3.11.0)
|
51
62
|
byebug (~> 12.0)
|
52
63
|
pry (>= 0.13, < 0.16)
|
64
|
+
psych (5.2.6)
|
65
|
+
date
|
66
|
+
stringio
|
53
67
|
racc (1.8.1)
|
54
68
|
rainbow (3.1.1)
|
55
69
|
rake (13.3.0)
|
56
|
-
|
70
|
+
rdoc (6.15.0)
|
71
|
+
erb
|
72
|
+
psych (>= 4.0.0)
|
73
|
+
tsort
|
74
|
+
regexp_parser (2.11.3)
|
75
|
+
reline (0.6.2)
|
76
|
+
io-console (~> 0.5)
|
57
77
|
rspec (3.13.1)
|
58
78
|
rspec-core (~> 3.13.0)
|
59
79
|
rspec-expectations (~> 3.13.0)
|
@@ -66,8 +86,8 @@ GEM
|
|
66
86
|
rspec-mocks (3.13.5)
|
67
87
|
diff-lcs (>= 1.2.0, < 2.0)
|
68
88
|
rspec-support (~> 3.13.0)
|
69
|
-
rspec-support (3.13.
|
70
|
-
rubocop (1.
|
89
|
+
rspec-support (3.13.6)
|
90
|
+
rubocop (1.81.1)
|
71
91
|
json (~> 2.3)
|
72
92
|
language_server-protocol (~> 3.17.0.2)
|
73
93
|
lint_roller (~> 1.1.0)
|
@@ -75,20 +95,20 @@ GEM
|
|
75
95
|
parser (>= 3.3.0.2)
|
76
96
|
rainbow (>= 2.2.2, < 4.0)
|
77
97
|
regexp_parser (>= 2.9.3, < 3.0)
|
78
|
-
rubocop-ast (>= 1.
|
98
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
79
99
|
ruby-progressbar (~> 1.7)
|
80
100
|
unicode-display_width (>= 2.4.0, < 4.0)
|
81
|
-
rubocop-ast (1.
|
101
|
+
rubocop-ast (1.47.1)
|
82
102
|
parser (>= 3.3.7.2)
|
83
103
|
prism (~> 1.4)
|
84
|
-
rubocop-performance (1.
|
104
|
+
rubocop-performance (1.26.0)
|
85
105
|
lint_roller (~> 1.1)
|
86
106
|
rubocop (>= 1.75.0, < 2.0)
|
87
|
-
rubocop-ast (>= 1.
|
107
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
88
108
|
rubocop-rake (0.7.1)
|
89
109
|
lint_roller (~> 1.1)
|
90
110
|
rubocop (>= 1.72.1)
|
91
|
-
rubocop-rspec (3.
|
111
|
+
rubocop-rspec (3.7.0)
|
92
112
|
lint_roller (~> 1.1)
|
93
113
|
rubocop (~> 1.72, >= 1.72.1)
|
94
114
|
ruby-progressbar (1.13.0)
|
@@ -97,13 +117,16 @@ GEM
|
|
97
117
|
docile (~> 1.1)
|
98
118
|
simplecov-html (~> 0.11)
|
99
119
|
simplecov_json_formatter (~> 0.1)
|
100
|
-
simplecov-html (0.13.
|
120
|
+
simplecov-html (0.13.2)
|
101
121
|
simplecov_json_formatter (0.1.4)
|
122
|
+
stringio (3.1.7)
|
123
|
+
tsort (0.2.0)
|
102
124
|
tzinfo (2.0.6)
|
103
125
|
concurrent-ruby (~> 1.0)
|
104
|
-
unicode-display_width (3.
|
105
|
-
unicode-emoji (~> 4.
|
106
|
-
unicode-emoji (4.0
|
126
|
+
unicode-display_width (3.2.0)
|
127
|
+
unicode-emoji (~> 4.1)
|
128
|
+
unicode-emoji (4.1.0)
|
129
|
+
uri (1.0.4)
|
107
130
|
|
108
131
|
PLATFORMS
|
109
132
|
arm64-darwin-22
|
@@ -118,8 +141,10 @@ PLATFORMS
|
|
118
141
|
DEPENDENCIES
|
119
142
|
bundler (~> 2.5, >= 2.5.3)
|
120
143
|
colorize (>= 0.8.1, < 2.0)
|
144
|
+
irb (>= 1.0)
|
121
145
|
pry-byebug (>= 3.9, < 4.0)
|
122
146
|
rake (>= 13.0, < 14.0)
|
147
|
+
reline (>= 0.3)
|
123
148
|
rspec (>= 3.10, < 4.0)
|
124
149
|
rubocop (>= 1.62, < 2.0)
|
125
150
|
rubocop-performance (>= 1.20, < 2.0)
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
[](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml)
|
2
|
+
[](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
|
3
|
+
[](https://badge.fury.io/rb/simple_command_dispatcher)
|
4
4
|
[](http://www.rubydoc.info/gems/simple_command_dispatcher/)
|
5
5
|
[](http://www.rubydoc.info/gems/simple_command_dispatcher/)
|
6
6
|
[](https://github.com/gangelo/simple_command_dispatcher/issues)
|
@@ -44,20 +44,96 @@ Or install it yourself as:
|
|
44
44
|
|
45
45
|
- Ruby >= 3.3.0
|
46
46
|
- Rails (optional, but optimized for Rails applications)
|
47
|
+
- Rails 8 compatible (tested with ActiveSupport 8.x)
|
48
|
+
|
49
|
+
## Quick Start
|
50
|
+
|
51
|
+
Here's a complete minimal example showing how to use the gem in a Rails controller:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# 1. Configure the gem (optional - uses Rails.logger by default)
|
55
|
+
# config/initializers/simple_command_dispatcher.rb
|
56
|
+
SimpleCommandDispatcher.configure do |config|
|
57
|
+
config.logger = Rails.logger
|
58
|
+
end
|
59
|
+
|
60
|
+
# 2. Create a command
|
61
|
+
# app/commands/api/v1/authenticate_user.rb
|
62
|
+
module Api
|
63
|
+
module V1
|
64
|
+
class AuthenticateUser
|
65
|
+
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
66
|
+
|
67
|
+
def call
|
68
|
+
user = User.find_by(email: email)
|
69
|
+
return nil unless user&.authenticate(password)
|
70
|
+
|
71
|
+
user
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def initialize(params = {})
|
77
|
+
@email = params[:email]
|
78
|
+
@password = params[:password]
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :email, :password
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# 3. Call the command from your controller
|
87
|
+
# app/controllers/api/v1/sessions_controller.rb
|
88
|
+
class Api::V1::SessionsController < ApplicationController
|
89
|
+
def create
|
90
|
+
command = SimpleCommandDispatcher.call(
|
91
|
+
command: request.path, # "/api/v1/authenticate_user"
|
92
|
+
request_params: params
|
93
|
+
)
|
94
|
+
|
95
|
+
if command.success?
|
96
|
+
render json: { user: command.result }, status: :ok
|
97
|
+
else
|
98
|
+
render json: { errors: command.errors }, status: :unauthorized
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
47
103
|
|
48
104
|
## Basic Usage
|
49
105
|
|
50
106
|
### Simple Command Dispatch
|
51
107
|
|
52
108
|
```ruby
|
53
|
-
# Basic command
|
109
|
+
# Basic command calls - all equivalent
|
110
|
+
|
111
|
+
command = SimpleCommandDispatcher.call(
|
112
|
+
command: '/api/v1/authenticate_user',
|
113
|
+
# No `command_namespace:` param
|
114
|
+
request_params: { email: 'user@example.com', password: 'secret' }
|
115
|
+
)
|
116
|
+
|
117
|
+
command = SimpleCommandDispatcher.call(
|
118
|
+
command: :authenticate_user,
|
119
|
+
command_namespace: '/api/v1',
|
120
|
+
request_params: { email: 'user@example.com', password: 'secret' }
|
121
|
+
)
|
122
|
+
|
54
123
|
command = SimpleCommandDispatcher.call(
|
55
124
|
command: 'AuthenticateUser',
|
56
|
-
command_namespace:
|
125
|
+
command_namespace: %w[api v1],
|
57
126
|
request_params: { email: 'user@example.com', password: 'secret' }
|
58
127
|
)
|
59
128
|
|
60
|
-
#
|
129
|
+
# With debug logging enabled
|
130
|
+
command = SimpleCommandDispatcher.call(
|
131
|
+
command: '/api/v1/authenticate_user',
|
132
|
+
request_params: { email: 'user@example.com', password: 'secret' },
|
133
|
+
options: { debug: true } # Enables detailed debug logging
|
134
|
+
)
|
135
|
+
|
136
|
+
# All the above will execute: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
|
61
137
|
```
|
62
138
|
|
63
139
|
## Command Standardization with CommandCallable
|
@@ -73,7 +149,7 @@ Here's how it works with a real controller example:
|
|
73
149
|
```ruby
|
74
150
|
# app/controllers/api/mechs_controller.rb
|
75
151
|
class Api::MechsController < ApplicationController
|
76
|
-
before_action :route_request, except: [:index]
|
152
|
+
before_action :route_request, except: [:destroy, :index]
|
77
153
|
|
78
154
|
def index
|
79
155
|
render json: { mechs: Mech.all }
|
@@ -88,7 +164,8 @@ class Api::MechsController < ApplicationController
|
|
88
164
|
def route_request
|
89
165
|
command = SimpleCommandDispatcher.call(
|
90
166
|
command: request.path, # "/api/v1/mechs/search"
|
91
|
-
|
167
|
+
# No need to use the `command_namespace` param, since the command namespace
|
168
|
+
# can be gleaned directly from `command: request.path`.
|
92
169
|
request_params: params # Full Rails params hash
|
93
170
|
)
|
94
171
|
|
@@ -101,17 +178,29 @@ class Api::MechsController < ApplicationController
|
|
101
178
|
end
|
102
179
|
```
|
103
180
|
|
104
|
-
**The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search
|
181
|
+
**The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search`.
|
105
182
|
|
106
|
-
**Alternative approach**
|
183
|
+
**Alternative approach** for handling nested resource routes with dynamic actions:
|
107
184
|
|
108
185
|
```ruby
|
109
|
-
#
|
186
|
+
# Handle routes like: /api/v1/mechs/123/variants/456/update
|
187
|
+
# Extract resource action and build namespace from nested resources
|
188
|
+
path_parts = request.path.split("/")
|
189
|
+
action = path_parts.last # "update"
|
190
|
+
resource_path = path_parts[0...-1] # ["/api", "v1", "mechs", "123", "variants", "456"]
|
191
|
+
|
192
|
+
# Build namespace from resource path, filtering out IDs
|
193
|
+
namespace_parts = resource_path.select { |part| !part.match?(/^\d+$/) }
|
194
|
+
|
110
195
|
command = SimpleCommandDispatcher.call(
|
111
|
-
command:
|
112
|
-
command_namespace:
|
113
|
-
request_params: params
|
196
|
+
command: action, # "update"
|
197
|
+
command_namespace: namespace_parts, # ["/api", "v1", "mechs", "variants"]
|
198
|
+
request_params: params.merge(
|
199
|
+
mech_id: path_parts[4], # "123"
|
200
|
+
variant_id: path_parts[6] # "456"
|
201
|
+
)
|
114
202
|
)
|
203
|
+
# Calls: Api::V1::Mechs::Variants::Update.call(mech_id: "123", variant_id: "456", ...)
|
115
204
|
```
|
116
205
|
|
117
206
|
### Versioned Command Examples
|
@@ -121,10 +210,6 @@ command = SimpleCommandDispatcher.call(
|
|
121
210
|
class Api::V1::Mechs::Search
|
122
211
|
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
123
212
|
|
124
|
-
def initialize(params = {})
|
125
|
-
@name = params[:name]
|
126
|
-
end
|
127
|
-
|
128
213
|
def call
|
129
214
|
# V1 search logic - simple name search
|
130
215
|
name.present? ? Mech.where("mech_name ILIKE ?", "%#{name}%") : Mech.none
|
@@ -132,6 +217,10 @@ class Api::V1::Mechs::Search
|
|
132
217
|
|
133
218
|
private
|
134
219
|
|
220
|
+
def initialize(params = {})
|
221
|
+
@name = params[:name]
|
222
|
+
end
|
223
|
+
|
135
224
|
attr_reader :name
|
136
225
|
end
|
137
226
|
|
@@ -139,14 +228,6 @@ end
|
|
139
228
|
class Api::V2::Mechs::Search
|
140
229
|
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
141
230
|
|
142
|
-
def initialize(params = {})
|
143
|
-
@cost = params[:cost]
|
144
|
-
@introduction_year = params[:introduction_year]
|
145
|
-
@mech_name = params[:mech_name]
|
146
|
-
@tonnage = params[:tonnage]
|
147
|
-
@variant = params[:variant]
|
148
|
-
end
|
149
|
-
|
150
231
|
def call
|
151
232
|
# V2 search logic - comprehensive search using scopes
|
152
233
|
Mech.by_cost(cost)
|
@@ -158,6 +239,14 @@ class Api::V2::Mechs::Search
|
|
158
239
|
|
159
240
|
private
|
160
241
|
|
242
|
+
def initialize(params = {})
|
243
|
+
@cost = params[:cost]
|
244
|
+
@introduction_year = params[:introduction_year]
|
245
|
+
@mech_name = params[:mech_name]
|
246
|
+
@tonnage = params[:tonnage]
|
247
|
+
@variant = params[:variant]
|
248
|
+
end
|
249
|
+
|
161
250
|
attr_reader :cost, :introduction_year, :mech_name, :tonnage, :variant
|
162
251
|
end
|
163
252
|
|
@@ -200,6 +289,39 @@ When you prepend `CommandCallable` to your command class, you automatically get:
|
|
200
289
|
4. **Error Handling**: Built-in `errors` object for consistent error management
|
201
290
|
5. **Call Tracking**: Internal tracking to ensure methods work correctly
|
202
291
|
|
292
|
+
**Important:** The `.call` class method returns the command instance itself (not the raw result). Access the actual return value via `.result`:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
command = AuthenticateUser.call(email: 'user@example.com', password: 'secret')
|
296
|
+
command.success? # => true/false
|
297
|
+
command.result # => the actual User object (or whatever your call method returned)
|
298
|
+
command.errors # => errors collection if any
|
299
|
+
```
|
300
|
+
|
301
|
+
**Best Practice:** Make `initialize` private when using `CommandCallable`. This enforces the use of the `.call` class method and ensures proper success/failure tracking. Making `initialize` private prevents direct instantiation that would bypass CommandCallable's functionality:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class YourCommand
|
305
|
+
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
306
|
+
|
307
|
+
def call
|
308
|
+
# Your logic here
|
309
|
+
end
|
310
|
+
|
311
|
+
private # <- initialize should be private
|
312
|
+
|
313
|
+
def initialize(params = {})
|
314
|
+
@params = params
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# This works (correct pattern):
|
319
|
+
YourCommand.call(foo: 'bar')
|
320
|
+
|
321
|
+
# This raises NoMethodError (prevents bypassing CommandCallable):
|
322
|
+
YourCommand.new(foo: 'bar')
|
323
|
+
```
|
324
|
+
|
203
325
|
### Convention Over Configuration: Route-to-Command Mapping
|
204
326
|
|
205
327
|
The gem automatically transforms route paths into Ruby class constants using intelligent camelization, allowing flexible input formats:
|
@@ -274,12 +396,6 @@ request_params: 'single_value'
|
|
274
396
|
class Api::V1::Payments::Process
|
275
397
|
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
276
398
|
|
277
|
-
def initialize(params = {})
|
278
|
-
@amount = params[:amount]
|
279
|
-
@card_token = params[:card_token]
|
280
|
-
@user_id = params[:user_id]
|
281
|
-
end
|
282
|
-
|
283
399
|
def call
|
284
400
|
validate_payment_data
|
285
401
|
return nil if errors.any?
|
@@ -292,6 +408,12 @@ class Api::V1::Payments::Process
|
|
292
408
|
|
293
409
|
private
|
294
410
|
|
411
|
+
def initialize(params = {})
|
412
|
+
@amount = params[:amount]
|
413
|
+
@card_token = params[:card_token]
|
414
|
+
@user_id = params[:user_id]
|
415
|
+
end
|
416
|
+
|
295
417
|
attr_reader :amount, :card_token, :user_id
|
296
418
|
|
297
419
|
def validate_payment_data
|
@@ -345,7 +467,97 @@ The gem can be configured in an initializer:
|
|
345
467
|
```ruby
|
346
468
|
# config/initializers/simple_command_dispatcher.rb
|
347
469
|
SimpleCommandDispatcher.configure do |config|
|
348
|
-
#
|
470
|
+
# Configure the logger (defaults to Rails.logger in Rails apps, or Logger.new($stdout) otherwise)
|
471
|
+
config.logger = Rails.logger
|
472
|
+
|
473
|
+
# Or use a custom logger
|
474
|
+
# config.logger = Logger.new('log/commands.log')
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
478
|
+
### Using Configuration in Commands
|
479
|
+
|
480
|
+
You can access the configured logger within your commands to add custom logging:
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class Api::V1::Payments::Process
|
484
|
+
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
485
|
+
|
486
|
+
def call
|
487
|
+
logger.info("Processing payment for user #{user_id}")
|
488
|
+
|
489
|
+
validate_payment_data
|
490
|
+
return nil if errors.any?
|
491
|
+
|
492
|
+
result = charge_card
|
493
|
+
logger.info("Payment successful: #{result.inspect}")
|
494
|
+
result
|
495
|
+
rescue StandardError => e
|
496
|
+
logger.error("Payment failed: #{e.message}")
|
497
|
+
errors.add(:payment, e.message)
|
498
|
+
nil
|
499
|
+
end
|
500
|
+
|
501
|
+
private
|
502
|
+
|
503
|
+
def initialize(params = {})
|
504
|
+
@amount = params[:amount]
|
505
|
+
@card_token = params[:card_token]
|
506
|
+
@user_id = params[:user_id]
|
507
|
+
end
|
508
|
+
|
509
|
+
attr_reader :amount, :card_token, :user_id
|
510
|
+
|
511
|
+
def logger
|
512
|
+
SimpleCommandDispatcher.configuration.logger
|
513
|
+
end
|
514
|
+
|
515
|
+
def validate_payment_data
|
516
|
+
errors.add(:amount, 'must be positive') if amount.to_i <= 0
|
517
|
+
errors.add(:card_token, 'is required') if card_token.blank?
|
518
|
+
errors.add(:user_id, 'is required') if user_id.blank?
|
519
|
+
end
|
520
|
+
|
521
|
+
def charge_card
|
522
|
+
PaymentProcessor.charge(
|
523
|
+
amount: amount,
|
524
|
+
card_token: card_token,
|
525
|
+
user_id: user_id
|
526
|
+
)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
```
|
530
|
+
|
531
|
+
### Debug Logging
|
532
|
+
|
533
|
+
The gem includes built-in debug logging that can be enabled using the `debug` option. This is useful for debugging command execution flow:
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
# Enable debug logging for a single command
|
537
|
+
command = SimpleCommandDispatcher.call(
|
538
|
+
command: :authenticate_user,
|
539
|
+
command_namespace: '/api/v1',
|
540
|
+
request_params: { email: 'user@example.com', password: 'secret' },
|
541
|
+
options: { debug: true }
|
542
|
+
)
|
543
|
+
|
544
|
+
# Debug logging outputs:
|
545
|
+
# - Begin dispatching command (with command and namespace details)
|
546
|
+
# - Command to execute (the fully qualified class name)
|
547
|
+
# - Constantized command (the actual class constant)
|
548
|
+
# - End dispatching command
|
549
|
+
```
|
550
|
+
|
551
|
+
**Important:** Debug logging mode **does not** skip execution—it still runs your command and returns real results, but with detailed debug output to help you understand what's happening internally.
|
552
|
+
|
553
|
+
**Configure logging level:**
|
554
|
+
|
555
|
+
```ruby
|
556
|
+
# In your Rails initializer or application setup
|
557
|
+
SimpleCommandDispatcher.configure do |config|
|
558
|
+
logger = Logger.new($stdout)
|
559
|
+
logger.level = Logger::DEBUG # Set appropriate level
|
560
|
+
config.logger = logger
|
349
561
|
end
|
350
562
|
```
|
351
563
|
|
@@ -4,11 +4,45 @@ require_relative 'errors'
|
|
4
4
|
|
5
5
|
module SimpleCommandDispatcher
|
6
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
|
7
36
|
module CommandCallable
|
37
|
+
# @return [Object] the return value from the command's call method
|
8
38
|
attr_reader :result
|
9
39
|
|
10
40
|
module ClassMethods
|
11
|
-
#
|
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)
|
12
46
|
def call(...)
|
13
47
|
new(...).call
|
14
48
|
end
|
@@ -18,6 +52,11 @@ module SimpleCommandDispatcher
|
|
18
52
|
base.extend ClassMethods
|
19
53
|
end
|
20
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
|
21
60
|
def call
|
22
61
|
raise NotImplementedError unless defined?(super)
|
23
62
|
|
@@ -27,15 +66,25 @@ module SimpleCommandDispatcher
|
|
27
66
|
self
|
28
67
|
end
|
29
68
|
|
69
|
+
# Returns true if the command was called successfully (no errors).
|
70
|
+
#
|
71
|
+
# @return [Boolean] true if called and no errors present
|
30
72
|
def success?
|
31
73
|
called? && !failure?
|
32
74
|
end
|
33
75
|
alias successful? success?
|
34
76
|
|
77
|
+
# Returns true if the command was called but has errors.
|
78
|
+
#
|
79
|
+
# @return [Boolean] true if called and errors are present
|
35
80
|
def failure?
|
36
81
|
called? && errors.any?
|
37
82
|
end
|
38
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
|
39
88
|
def errors
|
40
89
|
return super if defined?(super)
|
41
90
|
|
@@ -44,6 +93,9 @@ module SimpleCommandDispatcher
|
|
44
93
|
|
45
94
|
private
|
46
95
|
|
96
|
+
# Returns true if the command's call method has been invoked.
|
97
|
+
#
|
98
|
+
# @return [Boolean] true if call has been invoked
|
47
99
|
def called?
|
48
100
|
@called ||= false
|
49
101
|
end
|
@@ -5,27 +5,66 @@ require_relative 'utils'
|
|
5
5
|
module SimpleCommandDispatcher
|
6
6
|
module Commands
|
7
7
|
module CommandCallable
|
8
|
+
# Raised when a command's call method is not implemented
|
8
9
|
class NotImplementedError < ::StandardError; end
|
9
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.
|
10
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']
|
11
26
|
def add(key, value, _opts = {})
|
12
27
|
self[key] ||= []
|
13
28
|
self[key] << value
|
14
29
|
self[key].uniq!
|
15
30
|
end
|
16
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'])
|
17
39
|
def add_multiple_errors(errors_hash)
|
18
40
|
errors_hash.each do |key, values|
|
19
41
|
CommandCallable::Utils.array_wrap(values).each { |value| add key, value }
|
20
42
|
end
|
21
43
|
end
|
22
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}" }
|
23
53
|
def each
|
24
54
|
each_key do |field|
|
25
55
|
self[field].each { |message| yield field, message }
|
26
56
|
end
|
27
57
|
end
|
28
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']
|
29
68
|
def full_messages
|
30
69
|
map { |attribute, message| full_message(attribute, message) }
|
31
70
|
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
|
@@ -6,10 +6,14 @@ require 'core_ext/kernel'
|
|
6
6
|
require 'simple_command_dispatcher/commands/command_callable'
|
7
7
|
require 'simple_command_dispatcher/configuration'
|
8
8
|
require 'simple_command_dispatcher/errors'
|
9
|
+
require 'simple_command_dispatcher/logger'
|
9
10
|
require 'simple_command_dispatcher/services/command_service'
|
11
|
+
require 'simple_command_dispatcher/services/options_service'
|
10
12
|
require 'simple_command_dispatcher/version'
|
11
13
|
|
12
14
|
module SimpleCommandDispatcher
|
15
|
+
extend Logger
|
16
|
+
|
13
17
|
# Provides a way to call your custom commands dynamically.
|
14
18
|
#
|
15
19
|
class << self
|
@@ -18,10 +22,10 @@ module SimpleCommandDispatcher
|
|
18
22
|
#
|
19
23
|
# @param command [Symbol, String] the name of the Command to call.
|
20
24
|
#
|
21
|
-
# @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.
|
22
26
|
# When passing a Hash, the Hash keys serve as documentation only.
|
23
|
-
# For example, ['Api', 'AppName', 'V1'] and { :api :Api, app_name: :AppName, api_version: :V1 }
|
24
|
-
# 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
|
25
29
|
# to call (e.g. 'Api::AppName::V1::MySimpleCommand' = Api::AppName::V1::MySimpleCommand.call(*request_params)).
|
26
30
|
#
|
27
31
|
# @param request_params [Hash, Array, Object] the parameters to pass to the call method of the Command. This
|
@@ -29,6 +33,10 @@ module SimpleCommandDispatcher
|
|
29
33
|
# keyword arguments, Array parameters are passed as positional arguments, and other objects are passed
|
30
34
|
# as a single argument.
|
31
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
|
+
#
|
32
40
|
# @return [Object] the Object returned as a result of calling the Command#call method.
|
33
41
|
#
|
34
42
|
# @example
|
@@ -50,20 +58,43 @@ module SimpleCommandDispatcher
|
|
50
58
|
# command_namespace: ['Api::Auth::JazzMeUp', :V1],
|
51
59
|
# request_params: ['jazz_me@gmail.com', 'JazzM3!']) # => Command result
|
52
60
|
#
|
53
|
-
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
|
+
|
54
72
|
# Create a constantized class from our command and command_namespace...
|
55
|
-
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
|
+
|
56
81
|
validate_command!(constantized_class_object)
|
57
82
|
|
58
83
|
# We know we have a valid command class object if we get here. All we need to do is call the .call
|
59
84
|
# class method, pass the request_params arguments depending on the request_params data type, and
|
60
85
|
# return the results.
|
61
86
|
|
62
|
-
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
|
63
92
|
end
|
64
93
|
|
65
94
|
private
|
66
95
|
|
96
|
+
attr_reader :options
|
97
|
+
|
67
98
|
def call_command(constantized_class_object:, request_params:)
|
68
99
|
if request_params.is_a?(Hash)
|
69
100
|
constantized_class_object.call(**request_params)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_command_dispatcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gene M. Angelo, Jr.
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-10-08 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|
@@ -71,8 +71,10 @@ files:
|
|
71
71
|
- lib/simple_command_dispatcher/errors/required_class_method_missing_error.rb
|
72
72
|
- lib/simple_command_dispatcher/helpers/camelize.rb
|
73
73
|
- lib/simple_command_dispatcher/helpers/trim_all.rb
|
74
|
+
- lib/simple_command_dispatcher/logger.rb
|
74
75
|
- lib/simple_command_dispatcher/services/command_namespace_service.rb
|
75
76
|
- lib/simple_command_dispatcher/services/command_service.rb
|
77
|
+
- lib/simple_command_dispatcher/services/options_service.rb
|
76
78
|
- lib/simple_command_dispatcher/version.rb
|
77
79
|
- simple_command_dispatcher.gemspec
|
78
80
|
homepage: https://github.com/gangelo/simple_command_dispatcher
|
@@ -96,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
98
|
- !ruby/object:Gem::Version
|
97
99
|
version: '0'
|
98
100
|
requirements: []
|
99
|
-
rubygems_version: 3.6.
|
101
|
+
rubygems_version: 3.6.2
|
100
102
|
specification_version: 4
|
101
103
|
summary: Dynamic command execution for Rails applications using convention over configuration
|
102
104
|
- automatically maps request routes to command classes.
|