simple_command_dispatcher 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0144dd27d6c3f36427f08f5c44d12960a2a3d3807b08cad71ea1599ea13e8c32
4
- data.tar.gz: 7c8505c9dda676c57695c1860972b5bbe3ad693b14e3c69e0468fda3292b7a62
3
+ metadata.gz: 78aeec50e25d248707c465097b003bf35eb4f6783c7405f68ea08264a1e655a3
4
+ data.tar.gz: 3d1333a439993ac0ad274c87d4244aadada69bcc7ddb5b064f34847b43396e46
5
5
  SHA512:
6
- metadata.gz: afaa936a89c964a9463652cdfc46c50e0c040ce6f57cf2c4b30ffe08f500ebc00ebf7947162c65894301e008776d8c10508353a684e1d6dc060c3dfdc4ef15ed
7
- data.tar.gz: d9497737a1f54a2b43d2dbd54809c8af707da645ba7ef2aace3dc9706a31db051bbe002df484a79525d940c97be3a00bc609f6879434205ef46bf7763b74874c
6
+ metadata.gz: c8c0b2631ad96c502f878df531e41f04a11ef92d11f059303fc4be3ad18afcd7f927c50cc9978f4c63e14f3026e03b7fa78529025a18d99f4bf31e4919152a3f
7
+ data.tar.gz: fe7cb001f4315809acd6699f40e199b864e9e0c88da78ac84ba806f989309367a54e1af71ce7fa490c5cca946a391498bd1ef56e3c2f1a2af0374efa45dbf853
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Version 4.1.0 - 2025-07-14
4
+
5
+ - **New Feature: CommandCallable Module**:
6
+
7
+ - Introduced `SimpleCommandDispatcher::Commands::CommandCallable` module for standardizing command classes
8
+ - Provides automatic `.call` class method generation that instantiates and calls your command
9
+ - Built-in success/failure tracking with `success?` and `failure?` methods based on error state
10
+ - Automatic result tracking - command return values stored in `command.result`
11
+ - Consistent error handling with built-in `errors` object for error collection and management
12
+ - Call tracking to ensure methods work correctly and commands are properly executed
13
+ - Completely optional but recommended for building robust, maintainable commands
14
+
15
+ - **Enhanced Documentation**:
16
+
17
+ - Major README.md overhaul with real-world examples showcasing dynamic command execution
18
+ - Added comprehensive examples demonstrating convention over configuration approach
19
+ - Included versioned API command examples (V1 vs V2) showing practical usage patterns
20
+ - Added controller examples showing how to use `request.path` and `params` for dynamic routing
21
+ - Enhanced payment processing example with proper error handling and rescue patterns
22
+ - Added efficient database query examples using ActiveRecord scopes
23
+ - Improved parameter handling documentation showing kwargs vs single hash approaches
24
+ - Added alternative command splitting approach for more granular control
25
+ - Updated all examples to use `command` variable instead of `result` for clarity
26
+ - Added custom command guidance for users who prefer to roll their own implementations
27
+
3
28
  ## Version 4.0.0 - 2025-07-12
4
29
 
5
30
  - **Documentation Overhaul**:
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_command_dispatcher (4.0.0)
5
- activesupport (>= 7.0.8, < 8.0)
4
+ simple_command_dispatcher (4.1.0)
5
+ activesupport (>= 7.0.8, < 9.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
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=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)
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,7 +42,7 @@ 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)
45
47
 
46
48
  ## Basic Usage
@@ -49,31 +51,171 @@ Or install it yourself as:
49
51
 
50
52
  ```ruby
51
53
  # Basic command call
52
- result = SimpleCommandDispatcher.call(
54
+ command = SimpleCommandDispatcher.call(
53
55
  command: 'AuthenticateUser',
54
56
  command_namespace: 'Api::V1',
55
57
  request_params: { email: 'user@example.com', password: 'secret' }
56
58
  )
57
59
 
58
- # This calls: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
60
+ # This executes: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
61
+ ```
62
+
63
+ ## Command Standardization with CommandCallable
64
+
65
+ 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.
66
+
67
+ ### The Real Power: Dynamic Command Execution using convention over configuration
68
+
69
+ 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.
70
+
71
+ Here's how it works with a real controller example:
72
+
73
+ ```ruby
74
+ # app/controllers/api/mechs_controller.rb
75
+ class Api::MechsController < ApplicationController
76
+ before_action :route_request, except: [:index]
77
+
78
+ def index
79
+ render json: { mechs: Mech.all }
80
+ end
81
+
82
+ def search
83
+ # Action intentionally left empty, routing handled by before_action
84
+ end
85
+
86
+ private
87
+
88
+ def route_request
89
+ command = SimpleCommandDispatcher.call(
90
+ command: request.path, # "/api/v1/mechs/search"
91
+ command_namespace: nil, # nil since the command namespace can be gleaned directly from `command: request.path`
92
+ request_params: params # Full Rails params hash
93
+ )
94
+
95
+ if command.success?
96
+ render json: { mechs: command.result }, status: :ok
97
+ else
98
+ render json: { errors: command.errors }, status: :unprocessable_entity
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ **The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search`
105
+
106
+ **Alternative approach** if you need more control over command name and namespace:
107
+
108
+ ```ruby
109
+ # Split the path manually
110
+ 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
114
+ )
115
+ ```
116
+
117
+ ### Versioned Command Examples
118
+
119
+ ```ruby
120
+ # app/commands/api/v1/mechs/search.rb
121
+ class Api::V1::Mechs::Search
122
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
123
+
124
+ def initialize(params = {})
125
+ @name = params[:name]
126
+ end
127
+
128
+ def call
129
+ # V1 search logic - simple name search
130
+ name.present? ? Mech.where("mech_name ILIKE ?", "%#{name}%") : Mech.none
131
+ end
132
+
133
+ private
134
+
135
+ attr_reader :name
136
+ end
137
+
138
+ # app/commands/api/v2/mechs/search.rb
139
+ class Api::V2::Mechs::Search
140
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
141
+
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
+ def call
151
+ # V2 search logic - comprehensive search using scopes
152
+ Mech.by_cost(cost)
153
+ .or(Mech.by_introduction_year(introduction_year))
154
+ .or(Mech.by_mech_name(mech_name))
155
+ .or(Mech.by_tonnage(tonnage))
156
+ .or(Mech.by_variant(variant))
157
+ end
158
+
159
+ private
160
+
161
+ attr_reader :cost, :introduction_year, :mech_name, :tonnage, :variant
162
+ end
163
+
164
+ # app/models/mech.rb (V2 scopes)
165
+ class Mech < ApplicationRecord
166
+ scope :by_mech_name, ->(name) {
167
+ name.present? ? where("mech_name ILIKE ?", "%#{name}%") : none
168
+ }
169
+
170
+ scope :by_variant, ->(variant) {
171
+ variant.present? ? where("variant ILIKE ?", "%#{variant}%") : none
172
+ }
173
+
174
+ scope :by_tonnage, ->(tonnage) {
175
+ tonnage.present? ? where(tonnage: tonnage) : none
176
+ }
177
+
178
+ scope :by_cost, ->(cost) {
179
+ cost.present? ? where(cost: cost) : none
180
+ }
181
+
182
+ scope :by_introduction_year, ->(year) {
183
+ year.present? ? where(introduction_year: year) : none
184
+ }
185
+ end
59
186
  ```
60
187
 
61
- ### Automatic Camelization
188
+ **The Magic:** By convention, routes automatically map to commands:
189
+
190
+ - `/api/v1/mechs/search` → `Api::V1::Mechs::Search`
191
+ - `/api/v2/mechs/search` → `Api::V2::Mechs::Search`
192
+
193
+ ### What CommandCallable Provides
194
+
195
+ When you prepend `CommandCallable` to your command class, you automatically get:
62
196
 
63
- Command names and namespaces are automatically camelized using optimized RESTful route conversion, allowing flexible input formats:
197
+ 1. **Class Method Generation**: Automatic `.call` class method that instantiates and calls your command
198
+ 2. **Result Tracking**: Your command's return value is stored in `command.result`
199
+ 3. **Success/Failure Methods**: `success?` and `failure?` methods based on error state
200
+ 4. **Error Handling**: Built-in `errors` object for consistent error management
201
+ 5. **Call Tracking**: Internal tracking to ensure methods work correctly
202
+
203
+ ### Convention Over Configuration: Route-to-Command Mapping
204
+
205
+ The gem automatically transforms route paths into Ruby class constants using intelligent camelization, allowing flexible input formats:
64
206
 
65
207
  ```ruby
66
208
  # All of these are equivalent and call: Api::UserSessions::V1::CreateCommand.call
67
209
 
68
210
  # Lowercase strings with various separators
69
211
  SimpleCommandDispatcher.call(
70
- command: 'create_command',
212
+ command: :create_command,
71
213
  command_namespace: 'api::user_sessions::v1'
72
214
  )
73
215
 
74
216
  # Mixed case array
75
217
  SimpleCommandDispatcher.call(
76
- command: :CreateCommand,
218
+ command: 'CreateCommand',
77
219
  command_namespace: ['api', 'UserSessions', 'v1']
78
220
  )
79
221
 
@@ -90,7 +232,7 @@ SimpleCommandDispatcher.call(
90
232
  )
91
233
  ```
92
234
 
93
- The camelization handles Unicode characters and removes all whitespace (including Unicode whitespace):
235
+ The transformation handles Unicode characters and removes all whitespace:
94
236
 
95
237
  ```ruby
96
238
  # Unicode support
@@ -101,219 +243,78 @@ SimpleCommandDispatcher.call(
101
243
  # Calls: Api::Café::V1::CaféCommand.call
102
244
  ```
103
245
 
104
- ### Parameter Handling
105
-
106
- The dispatcher supports multiple parameter formats:
107
-
108
- ```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')
116
-
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')
124
-
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')
132
-
133
- # No parameters
134
- SimpleCommandDispatcher.call(
135
- command: 'HealthCheck',
136
- command_namespace: 'System'
137
- )
138
- # Calls: System::HealthCheck.call
139
- ```
140
-
141
- ## Rails Integration Example
246
+ ### Dynamic Parameter Handling
142
247
 
143
- Here's a comprehensive example showing how to integrate SCD with a Rails API application:
144
-
145
- ### Application Controller
248
+ The dispatcher intelligently handles different parameter types based on how your command initializer is coded:
146
249
 
147
250
  ```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('/')
161
- end
251
+ # Hash params → keyword arguments
252
+ def initialize(name:, email:) # kwargs
253
+ # Called with: YourCommand.call(name: 'John', email: 'john@example.com')
254
+ end
162
255
 
163
- private
256
+ # Hash params → single hash argument
257
+ def initialize(params = {}) # single hash
258
+ # Called with: YourCommand.call({name: 'John', email: 'john@example.com'})
259
+ end
164
260
 
165
- def authenticate_request
166
- result = SimpleCommandDispatcher.call(
167
- command: 'AuthenticateRequest',
168
- command_namespace: get_command_namespace,
169
- request_params: { headers: request.headers }
170
- )
261
+ # Array params → positional arguments
262
+ request_params: ['arg1', 'arg2', 'arg3']
263
+ # Called with: YourCommand.call('arg1', 'arg2', 'arg3')
171
264
 
172
- if result.success?
173
- @current_user = result.user
174
- else
175
- render json: { error: 'Not Authorized' }, status: 401
176
- end
177
- end
178
- end
265
+ # Single param → single argument
266
+ request_params: 'single_value'
267
+ # Called with: YourCommand.call('single_value')
179
268
  ```
180
269
 
181
- ### Controller Actions
270
+ ### Payment Processing Example
182
271
 
183
272
  ```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
273
+ # app/commands/api/v1/payments/process.rb
274
+ class Api::V1::Payments::Process
275
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
276
+
277
+ def initialize(params = {})
278
+ @amount = params[:amount]
279
+ @card_token = params[:card_token]
280
+ @user_id = params[:user_id]
198
281
  end
199
282
 
200
- def update
201
- result = SimpleCommandDispatcher.call(
202
- command: 'UpdateUser',
203
- command_namespace: get_command_namespace,
204
- request_params: { id: params[:id], **user_params }
205
- )
283
+ def call
284
+ validate_payment_data
285
+ return nil if errors.any?
206
286
 
207
- if result.success?
208
- render json: result.user
209
- else
210
- render json: { errors: result.errors }, status: :unprocessable_entity
211
- end
287
+ charge_card
288
+ rescue StandardError => e
289
+ errors.add(:payment, e.message)
290
+ nil
212
291
  end
213
292
 
214
293
  private
215
294
 
216
- def user_params
217
- params.require(:user).permit(:name, :email, :phone)
218
- end
219
- end
220
- ```
221
-
222
- ### Command Classes
295
+ attr_reader :amount, :card_token, :user_id
223
296
 
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
297
+ def validate_payment_data
298
+ errors.add(:amount, 'must be positive') if amount.to_i <= 0
299
+ errors.add(:card_token, 'is required') if card_token.blank?
300
+ errors.add(:user_id, 'is required') if user_id.blank?
260
301
  end
261
- end
262
- ```
263
302
 
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
303
+ def charge_card
304
+ PaymentProcessor.charge(
305
+ amount: amount,
306
+ card_token: card_token,
307
+ user_id: user_id
308
+ )
295
309
  end
296
310
  end
297
311
  ```
298
312
 
299
- ### Autoloading Commands
313
+ **Route:** `POST /api/v1/payments/process` automatically calls `Api::V1::Payments::Process.call(params)`
300
314
 
301
- To ensure your command classes are properly loaded:
315
+ ### Custom Commands
302
316
 
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
- ```
317
+ 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
318
 
318
319
  ## Error Handling
319
320
 
@@ -321,7 +322,7 @@ The dispatcher provides specific error classes for different failure scenarios:
321
322
 
322
323
  ```ruby
323
324
  begin
324
- result = SimpleCommandDispatcher.call(
325
+ command = SimpleCommandDispatcher.call(
325
326
  command: 'NonExistentCommand',
326
327
  command_namespace: 'Api::V1'
327
328
  )
@@ -337,73 +338,6 @@ rescue ArgumentError => e
337
338
  end
338
339
  ```
339
340
 
340
- ## Advanced Usage
341
-
342
- ### Route-Based Command Dispatch
343
-
344
- For RESTful APIs, you can map routes directly to commands:
345
-
346
- ```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
- )
360
- end
361
- ```
362
-
363
- ### Dynamic API Versioning
364
-
365
- ```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
374
-
375
- # Usage
376
- result = call_versioned_command('authenticate_user', 'v2')
377
- ```
378
-
379
- ### Batch Command Execution
380
-
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
- )
396
- end
397
-
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 }
403
- end
404
- end
405
- ```
406
-
407
341
  ## Configuration
408
342
 
409
343
  The gem can be configured in an initializer:
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module SimpleCommandDispatcher
6
+ module Commands
7
+ module CommandCallable
8
+ attr_reader :result
9
+
10
+ module ClassMethods
11
+ # Accept everything, essentially: `call(*args, **kwargs)``
12
+ def call(...)
13
+ new(...).call
14
+ end
15
+ end
16
+
17
+ def self.prepended(base)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ def call
22
+ raise NotImplementedError unless defined?(super)
23
+
24
+ @called = true
25
+ @result = super
26
+
27
+ self
28
+ end
29
+
30
+ def success?
31
+ called? && !failure?
32
+ end
33
+ alias successful? success?
34
+
35
+ def failure?
36
+ called? && errors.any?
37
+ end
38
+
39
+ def errors
40
+ return super if defined?(super)
41
+
42
+ @errors ||= Errors.new
43
+ end
44
+
45
+ private
46
+
47
+ def called?
48
+ @called ||= false
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ module SimpleCommandDispatcher
6
+ module Commands
7
+ module CommandCallable
8
+ class NotImplementedError < ::StandardError; end
9
+
10
+ class Errors < Hash
11
+ def add(key, value, _opts = {})
12
+ self[key] ||= []
13
+ self[key] << value
14
+ self[key].uniq!
15
+ end
16
+
17
+ def add_multiple_errors(errors_hash)
18
+ errors_hash.each do |key, values|
19
+ CommandCallable::Utils.array_wrap(values).each { |value| add key, value }
20
+ end
21
+ end
22
+
23
+ def each
24
+ each_key do |field|
25
+ self[field].each { |message| yield field, message }
26
+ end
27
+ end
28
+
29
+ def full_messages
30
+ map { |attribute, message| full_message(attribute, message) }
31
+ end
32
+
33
+ private
34
+
35
+ def full_message(attribute, message)
36
+ return message if attribute == :base
37
+
38
+ attr_name = attribute.to_s.tr('.', '_').capitalize
39
+ "#{attr_name} #{message}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCommandDispatcher
4
+ module Commands
5
+ module CommandCallable
6
+ module Utils
7
+ # Borrowed from active_support/core_ext/array/wrap
8
+ def self.array_wrap(object)
9
+ if object.nil?
10
+ []
11
+ elsif object.respond_to?(:to_ary)
12
+ object.to_ary || [object]
13
+ else
14
+ [object]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCommandDispatcher
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_support/core_ext/object/blank'
4
4
  require 'active_support/core_ext/string/inflections'
5
5
  require 'core_ext/kernel'
6
+ require 'simple_command_dispatcher/commands/command_callable'
6
7
  require 'simple_command_dispatcher/configuration'
7
8
  require 'simple_command_dispatcher/errors'
8
9
  require 'simple_command_dispatcher/services/command_service'
@@ -10,12 +10,11 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = ['Gene M. Angelo, Jr.']
11
11
  spec.email = ['public.gma@gmail.com']
12
12
 
13
- spec.summary = 'Provides a way to dispatch simple_command (ruby gem) commands or your own custom commands (service objects) in a more dynamic manner
14
- within your service API. Ideal for rails-api.'
15
- spec.description = 'Within a services API (rails-api for instance), you may have a need to execute different simple_commands or your own custom commands (service objects)
16
- based on one or more factors: multiple application, API version, user type, user credentials, etc. For example,
17
- your service API may need to execute either Api::Auth::V1::AuthenticateCommand.call(...) or Api::Auth::V2::AuthenticateCommand.call(...)
18
- based on the API version. simple_command_dispatcher allows you to execute either command with one line of code dynamically.'.gsub(/\s+/, ' ')
13
+ spec.summary = 'Dynamic command execution for Rails applications using convention over configuration - automatically maps request routes to command classes.'
14
+ spec.description = 'A lightweight Ruby gem that enables Rails applications to dynamically execute command objects using convention over configuration. ' \
15
+ 'Automatically transforms request paths into Ruby class constants, allowing controllers to dispatch commands based on routes and parameters. ' \
16
+ 'Features the optional CommandCallable module for standardized command interfaces with built-in success/failure tracking and error handling. ' \
17
+ 'Perfect for clean, maintainable Rails APIs with RESTful route-to-command mapping. Only depends on ActiveSupport for reliable camelization.'
19
18
  spec.homepage = 'https://github.com/gangelo/simple_command_dispatcher'
20
19
  spec.license = 'MIT'
21
20
 
@@ -37,5 +36,5 @@ Gem::Specification.new do |spec|
37
36
 
38
37
  spec.required_ruby_version = Gem::Requirement.new('>= 3.3', '< 4.0')
39
38
 
40
- spec.add_runtime_dependency 'activesupport', '>= 7.0.8', '< 8.0'
39
+ spec.add_runtime_dependency 'activesupport', '>= 7.0.8', '< 9.0'
41
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_command_dispatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gene M. Angelo, Jr.
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: 7.0.8
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '8.0'
21
+ version: '9.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,13 +28,14 @@ dependencies:
28
28
  version: 7.0.8
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
- version: '8.0'
32
- description: 'Within a services API (rails-api for instance), you may have a need
33
- to execute different simple_commands or your own custom commands (service objects)
34
- based on one or more factors: multiple application, API version, user type, user
35
- credentials, etc. For example, your service API may need to execute either Api::Auth::V1::AuthenticateCommand.call(...)
36
- or Api::Auth::V2::AuthenticateCommand.call(...) based on the API version. simple_command_dispatcher
37
- allows you to execute either command with one line of code dynamically.'
31
+ version: '9.0'
32
+ description: A lightweight Ruby gem that enables Rails applications to dynamically
33
+ execute command objects using convention over configuration. Automatically transforms
34
+ request paths into Ruby class constants, allowing controllers to dispatch commands
35
+ based on routes and parameters. Features the optional CommandCallable module for
36
+ standardized command interfaces with built-in success/failure tracking and error
37
+ handling. Perfect for clean, maintainable Rails APIs with RESTful route-to-command
38
+ mapping. Only depends on ActiveSupport for reliable camelization.
38
39
  email:
39
40
  - public.gma@gmail.com
40
41
  executables: []
@@ -61,6 +62,9 @@ files:
61
62
  - bin/setup
62
63
  - lib/core_ext/kernel.rb
63
64
  - lib/simple_command_dispatcher.rb
65
+ - lib/simple_command_dispatcher/commands/command_callable.rb
66
+ - lib/simple_command_dispatcher/commands/errors.rb
67
+ - lib/simple_command_dispatcher/commands/utils.rb
64
68
  - lib/simple_command_dispatcher/configuration.rb
65
69
  - lib/simple_command_dispatcher/errors.rb
66
70
  - lib/simple_command_dispatcher/errors/invalid_class_constant_error.rb
@@ -94,7 +98,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
98
  requirements: []
95
99
  rubygems_version: 3.6.8
96
100
  specification_version: 4
97
- summary: Provides a way to dispatch simple_command (ruby gem) commands or your own
98
- custom commands (service objects) in a more dynamic manner within your service API.
99
- Ideal for rails-api.
101
+ summary: Dynamic command execution for Rails applications using convention over configuration
102
+ - automatically maps request routes to command classes.
100
103
  test_files: []