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.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- [![Ruby](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml/badge.svg?refresh=6)](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=6)](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
3
- [![Gem Version](https://badge.fury.io/rb/simple_command_dispatcher.svg?refresh=6)](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)
@@ -12,15 +12,17 @@
12
12
 
13
13
  **simple_command_dispatcher** (SCD) allows your Rails or Rails API application to _dynamically_ call backend command services from your Rails controller actions using a flexible, convention-over-configuration approach.
14
14
 
15
+ πŸ“‹ **See it in action:** Check out the [demo application](https://github.com/gangelo/simple_command_dispatcher_demo_app) - a Rails API app with tests that demonstrate how to use the gem and its capabilities.
16
+
15
17
  ## Features
16
18
 
17
- - πŸš€ **Dynamic Command Dispatch**: Call command classes by name with flexible namespacing
18
- - πŸ”„ **Automatic Camelization**: Converts RESTful routes to Ruby constants automatically
19
- - 🌐 **Unicode Support**: Handles Unicode characters and whitespace properly
20
- - 🎯 **Multiple Input Formats**: Accepts strings, arrays, hashes for commands and namespaces
21
- - ⚑ **Performance Optimized**: Uses Rails' proven camelization methods for speed
22
- - πŸ”§ **Flexible Parameters**: Supports Hash, Array, and single object parameters
23
- - πŸ“¦ **No Dependencies**: Removed simple_command dependency for lighter footprint
19
+ - πŸ› οΈ **Convention Over Configuration**: Call commands dynamically from controller actions using action routes and parameters
20
+ - 🎭 **Command Standardization**: Optional `CommandCallable` module for consistent command interfaces with built-in success/failure tracking
21
+ - πŸš€ **Dynamic Route-to-Command Mapping**: Automatically transforms request paths into Ruby class constants
22
+ - πŸ”„ **Intelligent Parameter Handling**: Supports Hash, Array, and single object parameters with automatic detection
23
+ - 🌐 **Flexible Input Formats**: Accepts strings, arrays, symbols with various separators and Unicode support
24
+ - ⚑ **Performance Optimized**: Uses Rails' proven camelization methods for fast route-to-constant conversion
25
+ - πŸ“¦ **Lightweight**: Minimal dependencies - only ActiveSupport for reliable camelization
24
26
 
25
27
  ## Installation
26
28
 
@@ -40,40 +42,302 @@ Or install it yourself as:
40
42
 
41
43
  ## Requirements
42
44
 
43
- - Ruby >= 3.1.0
45
+ - Ruby >= 3.3.0
44
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
+ ```
45
103
 
46
104
  ## Basic Usage
47
105
 
48
106
  ### Simple Command Dispatch
49
107
 
50
108
  ```ruby
51
- # Basic command call
52
- result = SimpleCommandDispatcher.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
+
123
+ command = SimpleCommandDispatcher.call(
53
124
  command: 'AuthenticateUser',
54
- command_namespace: 'Api::V1',
125
+ command_namespace: %w[api v1],
55
126
  request_params: { email: 'user@example.com', password: 'secret' }
56
127
  )
57
128
 
58
- # This calls: 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')
137
+ ```
138
+
139
+ ## Command Standardization with CommandCallable
140
+
141
+ The gem includes a powerful `CommandCallable` module that standardizes your command classes, providing automatic success/failure tracking, error handling, and a consistent interface. This module is completely optional but highly recommended for building robust, maintainable commands.
142
+
143
+ ### The Real Power: Dynamic Command Execution using convention over configuration
144
+
145
+ Where this gem truly shines is its ability to **dynamically execute commands** using a **convention over configuration** approach. Command names and namespacing match controller action routes, making it possible to dynamically execute commands based on controller/action routes and pass arguments dynamically using params.
146
+
147
+ Here's how it works with a real controller example:
148
+
149
+ ```ruby
150
+ # app/controllers/api/mechs_controller.rb
151
+ class Api::MechsController < ApplicationController
152
+ before_action :route_request, except: [:destroy, :index]
153
+
154
+ def index
155
+ render json: { mechs: Mech.all }
156
+ end
157
+
158
+ def search
159
+ # Action intentionally left empty, routing handled by before_action
160
+ end
161
+
162
+ private
163
+
164
+ def route_request
165
+ command = SimpleCommandDispatcher.call(
166
+ command: request.path, # "/api/v1/mechs/search"
167
+ # No need to use the `command_namespace` param, since the command namespace
168
+ # can be gleaned directly from `command: request.path`.
169
+ request_params: params # Full Rails params hash
170
+ )
171
+
172
+ if command.success?
173
+ render json: { mechs: command.result }, status: :ok
174
+ else
175
+ render json: { errors: command.errors }, status: :unprocessable_entity
176
+ end
177
+ end
178
+ end
179
+ ```
180
+
181
+ **The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search`.
182
+
183
+ **Alternative approach** for handling nested resource routes with dynamic actions:
184
+
185
+ ```ruby
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
+
195
+ command = SimpleCommandDispatcher.call(
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
+ )
202
+ )
203
+ # Calls: Api::V1::Mechs::Variants::Update.call(mech_id: "123", variant_id: "456", ...)
204
+ ```
205
+
206
+ ### Versioned Command Examples
207
+
208
+ ```ruby
209
+ # app/commands/api/v1/mechs/search.rb
210
+ class Api::V1::Mechs::Search
211
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
212
+
213
+ def call
214
+ # V1 search logic - simple name search
215
+ name.present? ? Mech.where("mech_name ILIKE ?", "%#{name}%") : Mech.none
216
+ end
217
+
218
+ private
219
+
220
+ def initialize(params = {})
221
+ @name = params[:name]
222
+ end
223
+
224
+ attr_reader :name
225
+ end
226
+
227
+ # app/commands/api/v2/mechs/search.rb
228
+ class Api::V2::Mechs::Search
229
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
230
+
231
+ def call
232
+ # V2 search logic - comprehensive search using scopes
233
+ Mech.by_cost(cost)
234
+ .or(Mech.by_introduction_year(introduction_year))
235
+ .or(Mech.by_mech_name(mech_name))
236
+ .or(Mech.by_tonnage(tonnage))
237
+ .or(Mech.by_variant(variant))
238
+ end
239
+
240
+ private
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
+
250
+ attr_reader :cost, :introduction_year, :mech_name, :tonnage, :variant
251
+ end
252
+
253
+ # app/models/mech.rb (V2 scopes)
254
+ class Mech < ApplicationRecord
255
+ scope :by_mech_name, ->(name) {
256
+ name.present? ? where("mech_name ILIKE ?", "%#{name}%") : none
257
+ }
258
+
259
+ scope :by_variant, ->(variant) {
260
+ variant.present? ? where("variant ILIKE ?", "%#{variant}%") : none
261
+ }
262
+
263
+ scope :by_tonnage, ->(tonnage) {
264
+ tonnage.present? ? where(tonnage: tonnage) : none
265
+ }
266
+
267
+ scope :by_cost, ->(cost) {
268
+ cost.present? ? where(cost: cost) : none
269
+ }
270
+
271
+ scope :by_introduction_year, ->(year) {
272
+ year.present? ? where(introduction_year: year) : none
273
+ }
274
+ end
275
+ ```
276
+
277
+ **The Magic:** By convention, routes automatically map to commands:
278
+
279
+ - `/api/v1/mechs/search` β†’ `Api::V1::Mechs::Search`
280
+ - `/api/v2/mechs/search` β†’ `Api::V2::Mechs::Search`
281
+
282
+ ### What CommandCallable Provides
283
+
284
+ When you prepend `CommandCallable` to your command class, you automatically get:
285
+
286
+ 1. **Class Method Generation**: Automatic `.call` class method that instantiates and calls your command
287
+ 2. **Result Tracking**: Your command's return value is stored in `command.result`
288
+ 3. **Success/Failure Methods**: `success?` and `failure?` methods based on error state
289
+ 4. **Error Handling**: Built-in `errors` object for consistent error management
290
+ 5. **Call Tracking**: Internal tracking to ensure methods work correctly
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')
59
323
  ```
60
324
 
61
- ### Automatic Camelization
325
+ ### Convention Over Configuration: Route-to-Command Mapping
62
326
 
63
- Command names and namespaces are automatically camelized using optimized RESTful route conversion, allowing flexible input formats:
327
+ The gem automatically transforms route paths into Ruby class constants using intelligent camelization, allowing flexible input formats:
64
328
 
65
329
  ```ruby
66
330
  # All of these are equivalent and call: Api::UserSessions::V1::CreateCommand.call
67
331
 
68
332
  # Lowercase strings with various separators
69
333
  SimpleCommandDispatcher.call(
70
- command: 'create_command',
334
+ command: :create_command,
71
335
  command_namespace: 'api::user_sessions::v1'
72
336
  )
73
337
 
74
338
  # Mixed case array
75
339
  SimpleCommandDispatcher.call(
76
- command: :CreateCommand,
340
+ command: 'CreateCommand',
77
341
  command_namespace: ['api', 'UserSessions', 'v1']
78
342
  )
79
343
 
@@ -90,7 +354,7 @@ SimpleCommandDispatcher.call(
90
354
  )
91
355
  ```
92
356
 
93
- The camelization handles Unicode characters and removes all whitespace (including Unicode whitespace):
357
+ The transformation handles Unicode characters and removes all whitespace:
94
358
 
95
359
  ```ruby
96
360
  # Unicode support
@@ -101,219 +365,78 @@ SimpleCommandDispatcher.call(
101
365
  # Calls: Api::CafΓ©::V1::CafΓ©Command.call
102
366
  ```
103
367
 
104
- ### Parameter Handling
368
+ ### Dynamic Parameter Handling
105
369
 
106
- The dispatcher supports multiple parameter formats:
370
+ The dispatcher intelligently handles different parameter types based on how your command initializer is coded:
107
371
 
108
372
  ```ruby
109
- # Hash parameters (passed as keyword arguments)
110
- SimpleCommandDispatcher.call(
111
- command: 'CreateUser',
112
- command_namespace: 'Api::V1',
113
- request_params: { name: 'John', email: 'john@example.com' }
114
- )
115
- # Calls: Api::V1::CreateUser.call(name: 'John', email: 'john@example.com')
373
+ # Hash params β†’ keyword arguments
374
+ def initialize(name:, email:) # kwargs
375
+ # Called with: YourCommand.call(name: 'John', email: 'john@example.com')
376
+ end
116
377
 
117
- # Array parameters (passed as positional arguments)
118
- SimpleCommandDispatcher.call(
119
- command: 'ProcessData',
120
- command_namespace: 'Services',
121
- request_params: ['data1', 'data2', 'data3']
122
- )
123
- # Calls: Services::ProcessData.call('data1', 'data2', 'data3')
378
+ # Hash params β†’ single hash argument
379
+ def initialize(params = {}) # single hash
380
+ # Called with: YourCommand.call({name: 'John', email: 'john@example.com'})
381
+ end
124
382
 
125
- # Single parameter
126
- SimpleCommandDispatcher.call(
127
- command: 'SendEmail',
128
- command_namespace: 'Mailers',
129
- request_params: 'user@example.com'
130
- )
131
- # Calls: Mailers::SendEmail.call('user@example.com')
383
+ # Array params β†’ positional arguments
384
+ request_params: ['arg1', 'arg2', 'arg3']
385
+ # Called with: YourCommand.call('arg1', 'arg2', 'arg3')
132
386
 
133
- # No parameters
134
- SimpleCommandDispatcher.call(
135
- command: 'HealthCheck',
136
- command_namespace: 'System'
137
- )
138
- # Calls: System::HealthCheck.call
387
+ # Single param β†’ single argument
388
+ request_params: 'single_value'
389
+ # Called with: YourCommand.call('single_value')
139
390
  ```
140
391
 
141
- ## Rails Integration Example
142
-
143
- Here's a comprehensive example showing how to integrate SCD with a Rails API application:
144
-
145
- ### Application Controller
392
+ ### Payment Processing Example
146
393
 
147
394
  ```ruby
148
- # app/controllers/application_controller.rb
149
- require 'simple_command_dispatcher'
150
-
151
- class ApplicationController < ActionController::API
152
- before_action :authenticate_request
153
- attr_reader :current_user
154
-
155
- protected
156
-
157
- def get_command_namespace
158
- # Extract namespace from request path: "/api/my_app/v1/users" β†’ "api/my_app/v1"
159
- path_segments = request.path.split('/').reject(&:empty?)
160
- path_segments.take(3).join('/')
395
+ # app/commands/api/v1/payments/process.rb
396
+ class Api::V1::Payments::Process
397
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
398
+
399
+ def call
400
+ validate_payment_data
401
+ return nil if errors.any?
402
+
403
+ charge_card
404
+ rescue StandardError => e
405
+ errors.add(:payment, e.message)
406
+ nil
161
407
  end
162
408
 
163
409
  private
164
410
 
165
- def authenticate_request
166
- result = SimpleCommandDispatcher.call(
167
- command: 'AuthenticateRequest',
168
- command_namespace: get_command_namespace,
169
- request_params: { headers: request.headers }
170
- )
171
-
172
- if result.success?
173
- @current_user = result.user
174
- else
175
- render json: { error: 'Not Authorized' }, status: 401
176
- end
411
+ def initialize(params = {})
412
+ @amount = params[:amount]
413
+ @card_token = params[:card_token]
414
+ @user_id = params[:user_id]
177
415
  end
178
- end
179
- ```
180
416
 
181
- ### Controller Actions
417
+ attr_reader :amount, :card_token, :user_id
182
418
 
183
- ```ruby
184
- # app/controllers/api/my_app/v1/users_controller.rb
185
- class Api::MyApp::V1::UsersController < ApplicationController
186
- def create
187
- result = SimpleCommandDispatcher.call(
188
- command: 'CreateUser',
189
- command_namespace: get_command_namespace,
190
- request_params: user_params
191
- )
192
-
193
- if result.success?
194
- render json: result.user, status: :ok
195
- else
196
- render json: { errors: result.errors }, status: :unprocessable_entity
197
- end
419
+ def validate_payment_data
420
+ errors.add(:amount, 'must be positive') if amount.to_i <= 0
421
+ errors.add(:card_token, 'is required') if card_token.blank?
422
+ errors.add(:user_id, 'is required') if user_id.blank?
198
423
  end
199
424
 
200
- def update
201
- result = SimpleCommandDispatcher.call(
202
- command: 'UpdateUser',
203
- command_namespace: get_command_namespace,
204
- request_params: { id: params[:id], **user_params }
425
+ def charge_card
426
+ PaymentProcessor.charge(
427
+ amount: amount,
428
+ card_token: card_token,
429
+ user_id: user_id
205
430
  )
206
-
207
- if result.success?
208
- render json: result.user
209
- else
210
- render json: { errors: result.errors }, status: :unprocessable_entity
211
- end
212
- end
213
-
214
- private
215
-
216
- def user_params
217
- params.require(:user).permit(:name, :email, :phone)
218
431
  end
219
432
  end
220
433
  ```
221
434
 
222
- ### Command Classes
435
+ **Route:** `POST /api/v1/payments/process` automatically calls `Api::V1::Payments::Process.call(params)`
223
436
 
224
- ```ruby
225
- # app/commands/api/my_app/v1/authenticate_request.rb
226
- module Api
227
- module MyApp
228
- module V1
229
- class AuthenticateRequest
230
- def self.call(headers:)
231
- new(headers: headers).call
232
- end
233
-
234
- def initialize(headers:)
235
- @headers = headers
236
- end
237
-
238
- def call
239
- user = authenticate_with_token
240
- if user
241
- OpenStruct.new(success?: true, user: user)
242
- else
243
- OpenStruct.new(success?: false, errors: ['Invalid token'])
244
- end
245
- end
246
-
247
- private
248
-
249
- attr_reader :headers
250
-
251
- def authenticate_with_token
252
- token = headers['Authorization']&.gsub('Bearer ', '')
253
- return nil unless token
254
-
255
- # Your authentication logic here
256
- User.find_by(auth_token: token)
257
- end
258
- end
259
- end
260
- end
261
- end
262
- ```
437
+ ### Custom Commands
263
438
 
264
- ```ruby
265
- # app/commands/api/my_app/v1/create_user.rb
266
- module Api
267
- module MyApp
268
- module V1
269
- class CreateUser
270
- def self.call(**params)
271
- new(**params).call
272
- end
273
-
274
- def initialize(name:, email:, phone: nil)
275
- @name = name
276
- @email = email
277
- @phone = phone
278
- end
279
-
280
- def call
281
- user = User.new(name: name, email: email, phone: phone)
282
-
283
- if user.save
284
- OpenStruct.new(success?: true, user: user)
285
- else
286
- OpenStruct.new(success?: false, errors: user.errors.full_messages)
287
- end
288
- end
289
-
290
- private
291
-
292
- attr_reader :name, :email, :phone
293
- end
294
- end
295
- end
296
- end
297
- ```
298
-
299
- ### Autoloading Commands
300
-
301
- To ensure your command classes are properly loaded:
302
-
303
- ```ruby
304
- # config/initializers/simple_command_dispatcher.rb
305
-
306
- # Autoload command classes
307
- Rails.application.config.to_prepare do
308
- commands_path = Rails.root.join('app', 'commands')
309
-
310
- if commands_path.exist?
311
- Dir[commands_path.join('**', '*.rb')].each do |file|
312
- require_dependency file
313
- end
314
- end
315
- end
316
- ```
439
+ You can create your own command classes without `CommandCallable`. Just ensure your command responds to the `.call` class method and returns whatever structure you need. The dispatcher will call your command and return the result - your convention, your rules.
317
440
 
318
441
  ## Error Handling
319
442
 
@@ -321,7 +444,7 @@ The dispatcher provides specific error classes for different failure scenarios:
321
444
 
322
445
  ```ruby
323
446
  begin
324
- result = SimpleCommandDispatcher.call(
447
+ command = SimpleCommandDispatcher.call(
325
448
  command: 'NonExistentCommand',
326
449
  command_namespace: 'Api::V1'
327
450
  )
@@ -337,81 +460,104 @@ rescue ArgumentError => e
337
460
  end
338
461
  ```
339
462
 
340
- ## Advanced Usage
341
-
342
- ### Route-Based Command Dispatch
463
+ ## Configuration
343
464
 
344
- For RESTful APIs, you can map routes directly to commands:
465
+ The gem can be configured in an initializer:
345
466
 
346
467
  ```ruby
347
- # Extract command from route
348
- def dispatch_from_route
349
- # Route: "/api/my_app/v1/users/create"
350
- path_segments = request.path.split('/').reject(&:empty?)
351
-
352
- namespace = path_segments.take(3).join('/') # "api/my_app/v1"
353
- command = path_segments.last # "create"
354
-
355
- SimpleCommandDispatcher.call(
356
- command: "#{command}_#{controller_name.singularize}", # "create_user"
357
- command_namespace: namespace,
358
- request_params: request_params
359
- )
468
+ # config/initializers/simple_command_dispatcher.rb
469
+ SimpleCommandDispatcher.configure do |config|
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')
360
475
  end
361
476
  ```
362
477
 
363
- ### Dynamic API Versioning
478
+ ### Using Configuration in Commands
479
+
480
+ You can access the configured logger within your commands to add custom logging:
364
481
 
365
482
  ```ruby
366
- # Handle multiple API versions dynamically
367
- def call_versioned_command(command_name, version = 'v1')
368
- SimpleCommandDispatcher.call(
369
- command: command_name,
370
- command_namespace: ['api', app_name, version],
371
- request_params: request_params
372
- )
373
- end
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
374
500
 
375
- # Usage
376
- result = call_versioned_command('authenticate_user', 'v2')
377
- ```
501
+ private
378
502
 
379
- ### Batch Command Execution
503
+ def initialize(params = {})
504
+ @amount = params[:amount]
505
+ @card_token = params[:card_token]
506
+ @user_id = params[:user_id]
507
+ end
380
508
 
381
- ```ruby
382
- # Execute multiple related commands
383
- def process_user_registration(user_data)
384
- commands = [
385
- { command: 'validate_user', params: user_data },
386
- { command: 'create_user', params: user_data },
387
- { command: 'send_welcome_email', params: { email: user_data[:email] } }
388
- ]
389
-
390
- results = commands.map do |cmd|
391
- SimpleCommandDispatcher.call(
392
- command: cmd[:command],
393
- command_namespace: 'user_registration',
394
- request_params: cmd[:params]
395
- )
509
+ attr_reader :amount, :card_token, :user_id
510
+
511
+ def logger
512
+ SimpleCommandDispatcher.configuration.logger
396
513
  end
397
514
 
398
- # Check if all commands succeeded
399
- if results.all?(&:success?)
400
- { success: true, user: results[1].user }
401
- else
402
- { success: false, errors: results.map(&:errors).flatten.compact }
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
+ )
403
527
  end
404
528
  end
405
529
  ```
406
530
 
407
- ## Configuration
531
+ ### Debug Logging
408
532
 
409
- The gem can be configured in an initializer:
533
+ The gem includes built-in debug logging that can be enabled using the `debug` option. This is useful for debugging command execution flow:
410
534
 
411
535
  ```ruby
412
- # config/initializers/simple_command_dispatcher.rb
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
413
557
  SimpleCommandDispatcher.configure do |config|
414
- # Configuration options will be added in future versions
558
+ logger = Logger.new($stdout)
559
+ logger.level = Logger::DEBUG # Set appropriate level
560
+ config.logger = logger
415
561
  end
416
562
  ```
417
563