simple_command_dispatcher 3.0.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +47 -25
- data/.gitignore +3 -0
- data/.rubocop.yml +36 -41
- data/.ruby-version +1 -1
- data/CHANGELOG.md +148 -59
- data/Gemfile +3 -6
- data/Gemfile.lock +76 -69
- data/Jenkinsfile +2 -2
- data/README.md +371 -217
- data/Rakefile +0 -8
- data/lib/core_ext/kernel.rb +22 -0
- data/lib/simple_command_dispatcher/configuration.rb +37 -35
- data/lib/simple_command_dispatcher/errors/invalid_class_constant_error.rb +16 -0
- data/lib/simple_command_dispatcher/errors/required_class_method_missing_error.rb +15 -0
- data/lib/simple_command_dispatcher/errors.rb +4 -0
- data/lib/simple_command_dispatcher/helpers/camelize.rb +46 -0
- data/lib/simple_command_dispatcher/helpers/trim_all.rb +16 -0
- data/lib/simple_command_dispatcher/services/command_namespace_service.rb +60 -0
- data/lib/simple_command_dispatcher/services/command_service.rb +152 -0
- data/lib/simple_command_dispatcher/version.rb +2 -4
- data/lib/simple_command_dispatcher.rb +69 -121
- data/simple_command_dispatcher.gemspec +3 -3
- metadata +12 -31
- data/lib/core_extensions/string.rb +0 -10
- data/lib/simple_command_dispatcher/configure.rb +0 -22
- data/lib/simple_command_dispatcher/klass_transform.rb +0 -251
- data/lib/tasks/simple_command_dispatcher_sandbox.rake +0 -222
data/README.md
CHANGED
@@ -1,308 +1,462 @@
|
|
1
|
-
[](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml)
|
2
|
+
[](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
|
3
|
+
[](https://badge.fury.io/rb/simple_command_dispatcher)
|
4
4
|
[](http://www.rubydoc.info/gems/simple_command_dispatcher/)
|
5
5
|
[](http://www.rubydoc.info/gems/simple_command_dispatcher/)
|
6
6
|
[](https://github.com/gangelo/simple_command_dispatcher/issues)
|
7
7
|
[](#license)
|
8
8
|
|
9
|
-
#
|
10
|
-
# A. It's a Ruby gem!!!
|
9
|
+
# simple_command_dispatcher
|
11
10
|
|
12
11
|
## Overview
|
13
|
-
__simple_command_dispatcher__ (SCD) allows you to execute __simple_command__ commands (and now _custom commands_ as of version 1.2.1) in a more dynamic way. If you are not familiar with the _simple_command_ gem, check it out [here][simple-command]. SCD was written specifically with the [rails-api][rails-api] in mind; however, you can use SDC wherever you would use simple_command commands.
|
14
12
|
|
15
|
-
|
16
|
-
### Custom Commands
|
17
|
-
SCD now allows you to execute _custom commands_ (i.e. classes that do not prepend the _SimpleCommand_ module) by setting `Configuration#allow_custom_commands = true` (see the __Custom Commands__ section below for details).
|
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.
|
18
14
|
|
19
|
-
##
|
20
|
-
The below example is from a `rails-api` API that uses token-based authentication and services two mobile applications, identified as *__my_app1__* and *__my_app2__*, in this example.
|
15
|
+
## Features
|
21
16
|
|
22
|
-
|
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
|
23
24
|
|
24
|
-
|
25
|
-
* `request.headers` will contain the authorization token to authorize all requests (`request.headers["Authorization"]`)
|
26
|
-
* This application uses the following folder structure to manage its _simple_command_ commands:
|
27
|
-
|
28
|
-

|
25
|
+
## Installation
|
29
26
|
|
30
|
-
|
27
|
+
Add this line to your application's Gemfile:
|
31
28
|
|
32
29
|
```ruby
|
33
|
-
|
34
|
-
|
35
|
-
module Api
|
36
|
-
module MyApp1
|
37
|
-
module V1
|
38
|
-
class AuthenticateRequest
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
30
|
+
gem 'simple_command_dispatcher'
|
43
31
|
```
|
44
32
|
|
45
|
-
|
33
|
+
And then execute:
|
46
34
|
|
47
|
-
|
48
|
-
# /api/my_app2/v2/update_user.rb
|
35
|
+
$ bundle
|
49
36
|
|
50
|
-
|
51
|
-
module MyApp2
|
52
|
-
module V2
|
53
|
-
class UpdateUser
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
```
|
37
|
+
Or install it yourself as:
|
59
38
|
|
60
|
-
|
39
|
+
$ gem install simple_command_dispatcher
|
61
40
|
|
41
|
+
## Requirements
|
62
42
|
|
63
|
-
|
64
|
-
|
65
|
-
| api_my_app1_v1_user_authenticate | POST | /api/my_app1/v1/user/authenticate(.:format) | api/my_app1/v1/authentication#create |
|
66
|
-
| api_my_app1_v2_user_authenticate | POST | /api/my_app1/v2/user/authenticate(.:format) | api/my_app1/v2/authentication#create |
|
67
|
-
| api_my_app2_v1_user_authenticate | POST | /api/my_app2/v1/user/authenticate(.:format) | api/my_app2/v1/authentication#create |
|
68
|
-
| api_my_app2_v2_user | PATCH | /api/my_app2/v2/users/:id(.:format) | api/my_app2/v2/users#update |
|
69
|
-
| | PUT | /api/my_app2/v2/users/:id(.:format) | api/my_app2/v2/users#update |
|
43
|
+
- Ruby >= 3.1.0
|
44
|
+
- Rails (optional, but optimized for Rails applications)
|
70
45
|
|
46
|
+
## Basic Usage
|
71
47
|
|
72
|
-
###
|
48
|
+
### Simple Command Dispatch
|
73
49
|
|
74
50
|
```ruby
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
|
84
|
-
# As opposed to this:
|
85
|
-
#
|
86
|
-
# module Api
|
87
|
-
# module MyApp1
|
88
|
-
# module V1
|
89
|
-
# class AuthenticateRequest
|
90
|
-
# end
|
91
|
-
# end
|
92
|
-
# end
|
93
|
-
# end
|
94
|
-
#
|
95
|
-
module Helpers
|
96
|
-
def self.ensure_namespace(namespace, scope = "::")
|
97
|
-
namespace_parts = namespace.split("::")
|
98
|
-
|
99
|
-
namespace_chain = ""
|
100
|
-
|
101
|
-
namespace_parts.each { | part |
|
102
|
-
namespace_chain = (namespace_chain.empty?) ? part : "#{namespace_chain}::#{part}"
|
103
|
-
eval("module #{scope}#{namespace_chain}; end")
|
104
|
-
}
|
105
|
-
end
|
106
|
-
end
|
51
|
+
# Basic command call
|
52
|
+
result = SimpleCommandDispatcher.call(
|
53
|
+
command: 'AuthenticateUser',
|
54
|
+
command_namespace: 'Api::V1',
|
55
|
+
request_params: { email: 'user@example.com', password: 'secret' }
|
56
|
+
)
|
57
|
+
|
58
|
+
# This calls: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
|
59
|
+
```
|
107
60
|
|
108
|
-
|
109
|
-
Helpers.ensure_namespace("Api::MyApp1::V2")
|
110
|
-
Helpers.ensure_namespace("Api::MyApp2::V1")
|
111
|
-
Helpers.ensure_namespace("Api::MyApp2::V2")
|
112
|
-
=end
|
113
|
-
|
114
|
-
# simple_command_dispatcher creates commands dynamically; therefore we need
|
115
|
-
# to make sure the namespaces and command classes are loaded before we construct and
|
116
|
-
# call them. The below code traverses the 'app/api' and all subfolders, and
|
117
|
-
# autoloads them so that we do not get any NameError exceptions due to
|
118
|
-
# uninitialized constants.
|
119
|
-
Rails.application.config.to_prepare do
|
120
|
-
path = Rails.root + "app/api"
|
121
|
-
ActiveSupport::Dependencies.autoload_paths -= [path.to_s]
|
61
|
+
### Automatic Camelization
|
122
62
|
|
123
|
-
|
124
|
-
ActiveSupport::DescendantsTracker.clear
|
125
|
-
ActiveSupport::Dependencies.clear
|
63
|
+
Command names and namespaces are automatically camelized using optimized RESTful route conversion, allowing flexible input formats:
|
126
64
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
65
|
+
```ruby
|
66
|
+
# All of these are equivalent and call: Api::UserSessions::V1::CreateCommand.call
|
67
|
+
|
68
|
+
# Lowercase strings with various separators
|
69
|
+
SimpleCommandDispatcher.call(
|
70
|
+
command: 'create_command',
|
71
|
+
command_namespace: 'api::user_sessions::v1'
|
72
|
+
)
|
73
|
+
|
74
|
+
# Mixed case array
|
75
|
+
SimpleCommandDispatcher.call(
|
76
|
+
command: :CreateCommand,
|
77
|
+
command_namespace: ['api', 'UserSessions', 'v1']
|
78
|
+
)
|
79
|
+
|
80
|
+
# Route-like strings (optimized for Rails controllers)
|
81
|
+
SimpleCommandDispatcher.call(
|
82
|
+
command: '/create_command',
|
83
|
+
command_namespace: '/api/user_sessions/v1'
|
84
|
+
)
|
85
|
+
|
86
|
+
# Mixed separators (hyphens, dots, spaces)
|
87
|
+
SimpleCommandDispatcher.call(
|
88
|
+
command: 'create-command',
|
89
|
+
command_namespace: 'api.user-sessions/v1'
|
90
|
+
)
|
91
|
+
```
|
131
92
|
|
132
|
-
|
133
|
-
ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
|
134
|
-
reloader.execute
|
135
|
-
end
|
93
|
+
The camelization handles Unicode characters and removes all whitespace (including Unicode whitespace):
|
136
94
|
|
137
|
-
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
95
|
+
```ruby
|
96
|
+
# Unicode support
|
97
|
+
SimpleCommandDispatcher.call(
|
98
|
+
command: 'café_command',
|
99
|
+
command_namespace: 'api :: café :: v1' # Spaces are removed
|
100
|
+
)
|
101
|
+
# Calls: Api::Café::V1::CaféCommand.call
|
142
102
|
```
|
143
103
|
|
104
|
+
### Parameter Handling
|
105
|
+
|
106
|
+
The dispatcher supports multiple parameter formats:
|
107
|
+
|
144
108
|
```ruby
|
145
|
-
#
|
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
|
142
|
+
|
143
|
+
Here's a comprehensive example showing how to integrate SCD with a Rails API application:
|
144
|
+
|
145
|
+
### Application Controller
|
146
146
|
|
147
|
+
```ruby
|
148
|
+
# app/controllers/application_controller.rb
|
147
149
|
require 'simple_command_dispatcher'
|
148
150
|
|
149
151
|
class ApplicationController < ActionController::API
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
# (e.g. request.headers, which contains ["Authorization"], out authorization token).
|
175
|
-
# Consequently, the correlation between our routes and command class module structure
|
176
|
-
# was no coincidence.
|
177
|
-
command = SimpleCommand::Dispatcher.call(:AuthenticateRequest, get_command_path, { camelize: true}, request.headers)
|
178
|
-
if command.success?
|
179
|
-
@current_user = command.result
|
180
|
-
else
|
181
|
-
render json: { error: 'Not Authorized' }, status: 401
|
182
|
-
end
|
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
|
162
|
+
|
163
|
+
private
|
164
|
+
|
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
|
183
176
|
end
|
177
|
+
end
|
184
178
|
end
|
185
179
|
```
|
186
180
|
|
187
|
-
|
181
|
+
### Controller Actions
|
182
|
+
|
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
|
198
|
+
end
|
199
|
+
|
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
|
+
)
|
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
|
188
213
|
|
189
|
-
|
214
|
+
private
|
190
215
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
216
|
+
def user_params
|
217
|
+
params.require(:user).permit(:name, :email, :phone)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
195
221
|
|
196
|
-
###
|
222
|
+
### Command Classes
|
197
223
|
|
198
|
-
#### 1. Create a Custom Command
|
199
224
|
```ruby
|
200
|
-
# /api/my_app/v1/
|
201
|
-
|
225
|
+
# app/commands/api/my_app/v1/authenticate_request.rb
|
202
226
|
module Api
|
203
|
-
|
204
|
-
|
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
|
+
```
|
205
263
|
|
206
|
-
|
207
|
-
|
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
|
+
```
|
208
298
|
|
209
|
-
|
210
|
-
command = self.new(*args)
|
211
|
-
if command
|
212
|
-
command.send(:execute)
|
213
|
-
else
|
214
|
-
false
|
215
|
-
end
|
216
|
-
end
|
299
|
+
### Autoloading Commands
|
217
300
|
|
218
|
-
|
301
|
+
To ensure your command classes are properly loaded:
|
219
302
|
|
220
|
-
|
221
|
-
|
222
|
-
end
|
303
|
+
```ruby
|
304
|
+
# config/initializers/simple_command_dispatcher.rb
|
223
305
|
|
224
|
-
|
306
|
+
# Autoload command classes
|
307
|
+
Rails.application.config.to_prepare do
|
308
|
+
commands_path = Rails.root.join('app', 'commands')
|
225
309
|
|
226
|
-
|
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
|
+
```
|
227
317
|
|
228
|
-
|
229
|
-
if (param1 == :param1)
|
230
|
-
return true
|
231
|
-
end
|
318
|
+
## Error Handling
|
232
319
|
|
233
|
-
|
234
|
-
end
|
235
|
-
end
|
320
|
+
The dispatcher provides specific error classes for different failure scenarios:
|
236
321
|
|
237
|
-
|
238
|
-
|
322
|
+
```ruby
|
323
|
+
begin
|
324
|
+
result = SimpleCommandDispatcher.call(
|
325
|
+
command: 'NonExistentCommand',
|
326
|
+
command_namespace: 'Api::V1'
|
327
|
+
)
|
328
|
+
rescue SimpleCommandDispatcher::Errors::InvalidClassConstantError => e
|
329
|
+
# Command class doesn't exist
|
330
|
+
puts "Command not found: #{e.message}"
|
331
|
+
rescue SimpleCommandDispatcher::Errors::RequiredClassMethodMissingError => e
|
332
|
+
# Command class exists but doesn't have a .call method
|
333
|
+
puts "Invalid command: #{e.message}"
|
334
|
+
rescue ArgumentError => e
|
335
|
+
# Invalid arguments (empty command, wrong parameter types, etc.)
|
336
|
+
puts "Invalid arguments: #{e.message}"
|
239
337
|
end
|
240
338
|
```
|
241
|
-
#### 2. Set the `Configuration#allow_custom_commands` property to `true`
|
242
|
-
```ruby
|
243
|
-
# In your rails, rails-api app, etc...
|
244
|
-
# /config/initializers/simple_command_dispatcher.rb
|
245
339
|
|
246
|
-
|
247
|
-
|
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
|
+
)
|
248
360
|
end
|
249
361
|
```
|
250
362
|
|
251
|
-
|
252
|
-
|
363
|
+
### Dynamic API Versioning
|
364
|
+
|
253
365
|
```ruby
|
254
|
-
#
|
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
|
255
374
|
|
256
|
-
|
375
|
+
# Usage
|
376
|
+
result = call_versioned_command('authenticate_user', 'v2')
|
377
|
+
```
|
257
378
|
|
258
|
-
|
259
|
-
public
|
379
|
+
### Batch Command Execution
|
260
380
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
269
404
|
end
|
270
405
|
```
|
271
406
|
|
272
|
-
##
|
407
|
+
## Configuration
|
273
408
|
|
274
|
-
|
409
|
+
The gem can be configured in an initializer:
|
275
410
|
|
276
411
|
```ruby
|
277
|
-
|
412
|
+
# config/initializers/simple_command_dispatcher.rb
|
413
|
+
SimpleCommandDispatcher.configure do |config|
|
414
|
+
# Configuration options will be added in future versions
|
415
|
+
end
|
278
416
|
```
|
279
417
|
|
280
|
-
|
418
|
+
## Migration from v3.x
|
281
419
|
|
282
|
-
|
420
|
+
If you're upgrading from v3.x, here are the key changes:
|
283
421
|
|
284
|
-
|
422
|
+
### Breaking Changes
|
285
423
|
|
286
|
-
|
424
|
+
1. **Method signature changed to keyword arguments:**
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
# v3.x (old)
|
428
|
+
SimpleCommandDispatcher.call(:CreateUser, 'Api::V1', { options }, params)
|
429
|
+
|
430
|
+
# v4.x (new)
|
431
|
+
SimpleCommandDispatcher.call(
|
432
|
+
command: :CreateUser,
|
433
|
+
command_namespace: 'Api::V1',
|
434
|
+
request_params: params
|
435
|
+
)
|
436
|
+
```
|
287
437
|
|
288
|
-
|
438
|
+
2. **Removed simple_command dependency:**
|
289
439
|
|
290
|
-
|
440
|
+
- Commands no longer need to include SimpleCommand
|
441
|
+
- Commands must implement a `.call` class method
|
442
|
+
- Return value is whatever your command returns (no automatic Result object)
|
291
443
|
|
292
|
-
|
444
|
+
3. **Removed configuration options:**
|
293
445
|
|
294
|
-
|
446
|
+
- `allow_custom_commands` option removed (all commands are "custom" now)
|
447
|
+
- Camelization options removed (always enabled)
|
295
448
|
|
296
|
-
|
449
|
+
4. **Namespace changes:**
|
450
|
+
- Error classes: `SimpleCommand::Dispatcher::Errors::*` → `SimpleCommandDispatcher::Errors::*`
|
297
451
|
|
298
452
|
## Contributing
|
299
453
|
|
300
454
|
Bug reports and pull requests are welcome on GitHub at https://github.com/gangelo/simple_command_dispatcher. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
301
455
|
|
302
|
-
|
303
456
|
## License
|
304
457
|
|
305
458
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
306
459
|
|
307
|
-
|
308
|
-
|
460
|
+
## Changelog
|
461
|
+
|
462
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and breaking changes.
|