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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78aeec50e25d248707c465097b003bf35eb4f6783c7405f68ea08264a1e655a3
4
- data.tar.gz: 3d1333a439993ac0ad274c87d4244aadada69bcc7ddb5b064f34847b43396e46
3
+ metadata.gz: f29164d7bff98aa5d0b2aed2e12adf6f4408d06bfa789a142b0cf234f656a95a
4
+ data.tar.gz: ce2ba1f720a44a1a9ced023a7b2769d2a2702d971a6c0a2643aa5926a2585df4
5
5
  SHA512:
6
- metadata.gz: c8c0b2631ad96c502f878df531e41f04a11ef92d11f059303fc4be3ad18afcd7f927c50cc9978f4c63e14f3026e03b7fa78529025a18d99f4bf31e4919152a3f
7
- data.tar.gz: fe7cb001f4315809acd6699f40e199b864e9e0c88da78ac84ba806f989309367a54e1af71ce7fa490c5cca946a391498bd1ef56e3c2f1a2af0374efa45dbf853
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.1.0)
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 (7.2.2.1)
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.2.2)
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.3)
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
- json (2.12.2)
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.8.0)
51
+ parser (3.3.9.0)
44
52
  ast (~> 2.4.1)
45
53
  racc
46
- prism (1.4.0)
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
- regexp_parser (2.10.0)
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.4)
70
- rubocop (1.78.0)
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.45.1, < 2.0)
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.45.1)
101
+ rubocop-ast (1.47.1)
82
102
  parser (>= 3.3.7.2)
83
103
  prism (~> 1.4)
84
- rubocop-performance (1.25.0)
104
+ rubocop-performance (1.26.0)
85
105
  lint_roller (~> 1.1)
86
106
  rubocop (>= 1.75.0, < 2.0)
87
- rubocop-ast (>= 1.38.0, < 2.0)
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.6.0)
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.1)
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.1.4)
105
- unicode-emoji (~> 4.0, >= 4.0.4)
106
- unicode-emoji (4.0.4)
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
- [![Ruby](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml/badge.svg?refresh=7)](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml)
2
- [![GitHub version](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher.svg?refresh=7)](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
3
- [![Gem Version](https://badge.fury.io/rb/simple_command_dispatcher.svg?refresh=7)](https://badge.fury.io/rb/simple_command_dispatcher)
1
+ [![Ruby](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml/badge.svg?refresh=8)](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml)
2
+ [![GitHub version](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher.svg?refresh=8)](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
3
+ [![Gem Version](https://badge.fury.io/rb/simple_command_dispatcher.svg?refresh=8)](https://badge.fury.io/rb/simple_command_dispatcher)
4
4
  [![](https://ruby-gem-downloads-badge.herokuapp.com/simple_command_dispatcher?type=total)](http://www.rubydoc.info/gems/simple_command_dispatcher/)
5
5
  [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/simple_command_dispatcher/)
6
6
  [![Report Issues](https://img.shields.io/badge/report-issues-red.svg)](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 call
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: 'Api::V1',
125
+ command_namespace: %w[api v1],
57
126
  request_params: { email: 'user@example.com', password: 'secret' }
58
127
  )
59
128
 
60
- # This executes: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
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
- command_namespace: nil, # nil since the command namespace can be gleaned directly from `command: request.path`
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** if you need more control over command name and namespace:
183
+ **Alternative approach** for handling nested resource routes with dynamic actions:
107
184
 
108
185
  ```ruby
109
- # Split the path manually
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: request.path.split("/").last, # "search"
112
- command_namespace: request.path.split("/")[0..2], # "/api/v1/mechs"
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
- # Configuration options will be added in future versions
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
- # Accept everything, essentially: `call(*args, **kwargs)``
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.some_option = 'some value'
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
- # TODO: Add attr_xxx here
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
- # TODO: Reset our attributes here e.g. @attr = nil
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{[/\-\.\s:]+}, '/') # Normalize separators to /
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
- # to_class("Authenticate", "Api") # => Api::Authenticate
29
- # to_class(:Authenticate, [:Api, :AppName, :V1]) # => Api::AppName::V1::Authenticate
30
- # to_class(:Authenticate, { :api :Api, app_name: :AppName, api_version: :V2 })
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
- # to_class("authenticate", { :api :api, app_name: :app_name, api_version: :v1 },
33
- # { titleize_class: true, titleize_module: true }) # => Api::AppName::V1::Authenticate
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(command, command_namespace)
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
- attr_accessor :command, :command_namespace
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
- # @example
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 all blanks have been removed using
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 or String] command the class name to be validated. command cannot be empty?
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 blanks removed.
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(" My Class ") # => "MyClass"
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 or String] command_namespace the module(s) to be validated.
122
+ # @param command_namespace [Hash, Array, String] the module(s) to be validated.
123
123
  #
124
- # @return [Hash, Array or String] the validated module(s).
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(" Module ") # => " Module "
131
- # validate_command_namespace(:Module) # => :Module
132
- # validate_command_namespace("ModuleA::ModuleB") # => "ModuleA::ModuleB"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCommandDispatcher
4
- VERSION = '4.1.0'
4
+ VERSION = '4.2.0'
5
5
  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 both produce 'Api::AppName::V1', this string will be prepended to the command to form the Command
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.1.0
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: 1980-01-02 00:00:00.000000000 Z
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.8
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.