simple_command_dispatcher 3.0.4 → 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 +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 +173 -59
- data/Gemfile +3 -6
- data/Gemfile.lock +77 -70
- data/Jenkinsfile +2 -2
- data/README.md +312 -224
- data/Rakefile +0 -8
- data/lib/core_ext/kernel.rb +22 -0
- data/lib/simple_command_dispatcher/commands/command_callable.rb +52 -0
- data/lib/simple_command_dispatcher/commands/errors.rb +44 -0
- data/lib/simple_command_dispatcher/commands/utils.rb +20 -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 +70 -121
- data/simple_command_dispatcher.gemspec +9 -10
- metadata +26 -42
- 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,396 @@
|
|
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
|
+
📋 **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.
|
21
16
|
|
22
|
-
|
17
|
+
## Features
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
27
26
|
|
28
|
-
|
27
|
+
## Installation
|
29
28
|
|
30
|
-
|
29
|
+
Add this line to your application's Gemfile:
|
31
30
|
|
32
31
|
```ruby
|
33
|
-
|
34
|
-
|
35
|
-
module Api
|
36
|
-
module MyApp1
|
37
|
-
module V1
|
38
|
-
class AuthenticateRequest
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
32
|
+
gem 'simple_command_dispatcher'
|
43
33
|
```
|
44
34
|
|
45
|
-
|
35
|
+
And then execute:
|
46
36
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
module Api
|
51
|
-
module MyApp2
|
52
|
-
module V2
|
53
|
-
class UpdateUser
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
```
|
37
|
+
$ bundle
|
38
|
+
|
39
|
+
Or install it yourself as:
|
59
40
|
|
60
|
-
|
41
|
+
$ gem install simple_command_dispatcher
|
61
42
|
|
43
|
+
## Requirements
|
62
44
|
|
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 |
|
45
|
+
- Ruby >= 3.3.0
|
46
|
+
- Rails (optional, but optimized for Rails applications)
|
70
47
|
|
48
|
+
## Basic Usage
|
71
49
|
|
72
|
-
###
|
50
|
+
### Simple Command Dispatch
|
73
51
|
|
74
52
|
```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
|
53
|
+
# Basic command call
|
54
|
+
command = SimpleCommandDispatcher.call(
|
55
|
+
command: 'AuthenticateUser',
|
56
|
+
command_namespace: 'Api::V1',
|
57
|
+
request_params: { email: 'user@example.com', password: 'secret' }
|
58
|
+
)
|
59
|
+
|
60
|
+
# This executes: Api::V1::AuthenticateUser.call(email: 'user@example.com', password: 'secret')
|
61
|
+
```
|
107
62
|
|
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]
|
122
|
-
|
123
|
-
reloader = ActiveSupport::FileUpdateChecker.new [], path.to_s => [:rb] do
|
124
|
-
ActiveSupport::DescendantsTracker.clear
|
125
|
-
ActiveSupport::Dependencies.clear
|
126
|
-
|
127
|
-
Dir[path + "**/*.rb"].each do |file|
|
128
|
-
ActiveSupport.require_or_load file
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
Rails.application.reloaders << reloader
|
133
|
-
ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
|
134
|
-
reloader.execute
|
135
|
-
end
|
63
|
+
## Command Standardization with CommandCallable
|
136
64
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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:
|
143
72
|
|
144
73
|
```ruby
|
145
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
# Explaination: @param command_modules (e.g. path, "/api/my_app1/v1/"), in concert with @param
|
171
|
-
# options { camelize: true }, is transformed into "Api::MyApp1::V1" and prepended to the
|
172
|
-
# @param command, which becomes "Api::MyApp1::V1::AuthenticateRequest." This string is then
|
173
|
-
# simply constantized; #call is then executed, passing the @param command_parameters
|
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
|
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
|
183
99
|
end
|
100
|
+
end
|
184
101
|
end
|
185
102
|
```
|
186
103
|
|
187
|
-
|
104
|
+
**The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search`
|
188
105
|
|
189
|
-
|
106
|
+
**Alternative approach** if you need more control over command name and namespace:
|
190
107
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
+
```
|
195
116
|
|
196
|
-
###
|
117
|
+
### Versioned Command Examples
|
197
118
|
|
198
|
-
#### 1. Create a Custom Command
|
199
119
|
```ruby
|
200
|
-
# /api/
|
120
|
+
# app/commands/api/v1/mechs/search.rb
|
121
|
+
class Api::V1::Mechs::Search
|
122
|
+
prepend SimpleCommandDispatcher::Commands::CommandCallable
|
201
123
|
|
202
|
-
|
203
|
-
|
204
|
-
|
124
|
+
def initialize(params = {})
|
125
|
+
@name = params[:name]
|
126
|
+
end
|
205
127
|
|
206
|
-
|
207
|
-
|
128
|
+
def call
|
129
|
+
# V1 search logic - simple name search
|
130
|
+
name.present? ? Mech.where("mech_name ILIKE ?", "%#{name}%") : Mech.none
|
131
|
+
end
|
208
132
|
|
209
|
-
|
210
|
-
command = self.new(*args)
|
211
|
-
if command
|
212
|
-
command.send(:execute)
|
213
|
-
else
|
214
|
-
false
|
215
|
-
end
|
216
|
-
end
|
133
|
+
private
|
217
134
|
|
218
|
-
|
135
|
+
attr_reader :name
|
136
|
+
end
|
219
137
|
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
223
163
|
|
224
|
-
|
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
|
+
}
|
225
169
|
|
226
|
-
|
170
|
+
scope :by_variant, ->(variant) {
|
171
|
+
variant.present? ? where("variant ILIKE ?", "%#{variant}%") : none
|
172
|
+
}
|
227
173
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
end
|
174
|
+
scope :by_tonnage, ->(tonnage) {
|
175
|
+
tonnage.present? ? where(tonnage: tonnage) : none
|
176
|
+
}
|
232
177
|
|
233
|
-
|
234
|
-
|
235
|
-
|
178
|
+
scope :by_cost, ->(cost) {
|
179
|
+
cost.present? ? where(cost: cost) : none
|
180
|
+
}
|
236
181
|
|
237
|
-
|
238
|
-
|
182
|
+
scope :by_introduction_year, ->(year) {
|
183
|
+
year.present? ? where(introduction_year: year) : none
|
184
|
+
}
|
239
185
|
end
|
240
186
|
```
|
241
|
-
|
187
|
+
|
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:
|
196
|
+
|
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:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
# All of these are equivalent and call: Api::UserSessions::V1::CreateCommand.call
|
209
|
+
|
210
|
+
# Lowercase strings with various separators
|
211
|
+
SimpleCommandDispatcher.call(
|
212
|
+
command: :create_command,
|
213
|
+
command_namespace: 'api::user_sessions::v1'
|
214
|
+
)
|
215
|
+
|
216
|
+
# Mixed case array
|
217
|
+
SimpleCommandDispatcher.call(
|
218
|
+
command: 'CreateCommand',
|
219
|
+
command_namespace: ['api', 'UserSessions', 'v1']
|
220
|
+
)
|
221
|
+
|
222
|
+
# Route-like strings (optimized for Rails controllers)
|
223
|
+
SimpleCommandDispatcher.call(
|
224
|
+
command: '/create_command',
|
225
|
+
command_namespace: '/api/user_sessions/v1'
|
226
|
+
)
|
227
|
+
|
228
|
+
# Mixed separators (hyphens, dots, spaces)
|
229
|
+
SimpleCommandDispatcher.call(
|
230
|
+
command: 'create-command',
|
231
|
+
command_namespace: 'api.user-sessions/v1'
|
232
|
+
)
|
233
|
+
```
|
234
|
+
|
235
|
+
The transformation handles Unicode characters and removes all whitespace:
|
236
|
+
|
242
237
|
```ruby
|
243
|
-
#
|
244
|
-
|
238
|
+
# Unicode support
|
239
|
+
SimpleCommandDispatcher.call(
|
240
|
+
command: 'café_command',
|
241
|
+
command_namespace: 'api :: café :: v1' # Spaces are removed
|
242
|
+
)
|
243
|
+
# Calls: Api::Café::V1::CaféCommand.call
|
244
|
+
```
|
245
245
|
|
246
|
-
|
247
|
-
|
246
|
+
### Dynamic Parameter Handling
|
247
|
+
|
248
|
+
The dispatcher intelligently handles different parameter types based on how your command initializer is coded:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
# Hash params → keyword arguments
|
252
|
+
def initialize(name:, email:) # kwargs
|
253
|
+
# Called with: YourCommand.call(name: 'John', email: 'john@example.com')
|
248
254
|
end
|
255
|
+
|
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
|
260
|
+
|
261
|
+
# Array params → positional arguments
|
262
|
+
request_params: ['arg1', 'arg2', 'arg3']
|
263
|
+
# Called with: YourCommand.call('arg1', 'arg2', 'arg3')
|
264
|
+
|
265
|
+
# Single param → single argument
|
266
|
+
request_params: 'single_value'
|
267
|
+
# Called with: YourCommand.call('single_value')
|
249
268
|
```
|
250
269
|
|
251
|
-
|
252
|
-
|
270
|
+
### Payment Processing Example
|
271
|
+
|
253
272
|
```ruby
|
254
|
-
#
|
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]
|
281
|
+
end
|
282
|
+
|
283
|
+
def call
|
284
|
+
validate_payment_data
|
285
|
+
return nil if errors.any?
|
286
|
+
|
287
|
+
charge_card
|
288
|
+
rescue StandardError => e
|
289
|
+
errors.add(:payment, e.message)
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
private
|
294
|
+
|
295
|
+
attr_reader :amount, :card_token, :user_id
|
296
|
+
|
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?
|
301
|
+
end
|
302
|
+
|
303
|
+
def charge_card
|
304
|
+
PaymentProcessor.charge(
|
305
|
+
amount: amount,
|
306
|
+
card_token: card_token,
|
307
|
+
user_id: user_id
|
308
|
+
)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
```
|
255
312
|
|
256
|
-
|
313
|
+
**Route:** `POST /api/v1/payments/process` automatically calls `Api::V1::Payments::Process.call(params)`
|
257
314
|
|
258
|
-
|
259
|
-
public
|
315
|
+
### Custom Commands
|
260
316
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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.
|
318
|
+
|
319
|
+
## Error Handling
|
320
|
+
|
321
|
+
The dispatcher provides specific error classes for different failure scenarios:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
begin
|
325
|
+
command = SimpleCommandDispatcher.call(
|
326
|
+
command: 'NonExistentCommand',
|
327
|
+
command_namespace: 'Api::V1'
|
328
|
+
)
|
329
|
+
rescue SimpleCommandDispatcher::Errors::InvalidClassConstantError => e
|
330
|
+
# Command class doesn't exist
|
331
|
+
puts "Command not found: #{e.message}"
|
332
|
+
rescue SimpleCommandDispatcher::Errors::RequiredClassMethodMissingError => e
|
333
|
+
# Command class exists but doesn't have a .call method
|
334
|
+
puts "Invalid command: #{e.message}"
|
335
|
+
rescue ArgumentError => e
|
336
|
+
# Invalid arguments (empty command, wrong parameter types, etc.)
|
337
|
+
puts "Invalid arguments: #{e.message}"
|
269
338
|
end
|
270
339
|
```
|
271
340
|
|
272
|
-
##
|
341
|
+
## Configuration
|
273
342
|
|
274
|
-
|
343
|
+
The gem can be configured in an initializer:
|
275
344
|
|
276
345
|
```ruby
|
277
|
-
|
346
|
+
# config/initializers/simple_command_dispatcher.rb
|
347
|
+
SimpleCommandDispatcher.configure do |config|
|
348
|
+
# Configuration options will be added in future versions
|
349
|
+
end
|
278
350
|
```
|
279
351
|
|
280
|
-
|
352
|
+
## Migration from v3.x
|
281
353
|
|
282
|
-
|
354
|
+
If you're upgrading from v3.x, here are the key changes:
|
283
355
|
|
284
|
-
|
356
|
+
### Breaking Changes
|
285
357
|
|
286
|
-
|
358
|
+
1. **Method signature changed to keyword arguments:**
|
287
359
|
|
288
|
-
|
360
|
+
```ruby
|
361
|
+
# v3.x (old)
|
362
|
+
SimpleCommandDispatcher.call(:CreateUser, 'Api::V1', { options }, params)
|
289
363
|
|
290
|
-
|
364
|
+
# v4.x (new)
|
365
|
+
SimpleCommandDispatcher.call(
|
366
|
+
command: :CreateUser,
|
367
|
+
command_namespace: 'Api::V1',
|
368
|
+
request_params: params
|
369
|
+
)
|
370
|
+
```
|
291
371
|
|
292
|
-
|
372
|
+
2. **Removed simple_command dependency:**
|
293
373
|
|
294
|
-
|
374
|
+
- Commands no longer need to include SimpleCommand
|
375
|
+
- Commands must implement a `.call` class method
|
376
|
+
- Return value is whatever your command returns (no automatic Result object)
|
295
377
|
|
296
|
-
|
378
|
+
3. **Removed configuration options:**
|
379
|
+
|
380
|
+
- `allow_custom_commands` option removed (all commands are "custom" now)
|
381
|
+
- Camelization options removed (always enabled)
|
382
|
+
|
383
|
+
4. **Namespace changes:**
|
384
|
+
- Error classes: `SimpleCommand::Dispatcher::Errors::*` → `SimpleCommandDispatcher::Errors::*`
|
297
385
|
|
298
386
|
## Contributing
|
299
387
|
|
300
388
|
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
389
|
|
302
|
-
|
303
390
|
## License
|
304
391
|
|
305
392
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
306
393
|
|
307
|
-
|
308
|
-
|
394
|
+
## Changelog
|
395
|
+
|
396
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and breaking changes.
|
data/Rakefile
CHANGED
@@ -2,19 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rspec/core/rake_task'
|
5
|
-
require 'yard'
|
6
5
|
|
7
6
|
# Rspec
|
8
7
|
RSpec::Core::RakeTask.new(:spec)
|
9
8
|
#task default: :spec
|
10
9
|
|
11
|
-
# Yard
|
12
|
-
YARD::Rake::YardocTask.new do |t|
|
13
|
-
t.files = ['lib/**/*.rb']
|
14
|
-
t.options = ['--no-cache', '--protected', '--private']
|
15
|
-
t.stats_options = ['--list-undoc']
|
16
|
-
end
|
17
|
-
|
18
10
|
# Load our custom rake tasks.
|
19
11
|
Gem.find_files('tasks/**/*.rake').each { |path| import path }
|
20
12
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
# Returns the eigenclass (singleton class) of the current object.
|
5
|
+
# This allows classes to reference their own class-level methods and variables.
|
6
|
+
#
|
7
|
+
# @return [Class] the eigenclass of the current object
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class MyClass
|
11
|
+
# def self.test
|
12
|
+
# eigenclass
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# MyClass.test # => #<Class:MyClass>
|
16
|
+
#
|
17
|
+
def eigenclass
|
18
|
+
class << self
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|