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.
data/README.md CHANGED
@@ -1,308 +1,396 @@
1
- [![Ruby](https://github.com/gangelo/simple_command_dispatcher/actions/workflows/ruby.yml/badge.svg?refresh=2)](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=5)](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
3
- [![Gem Version](https://badge.fury.io/rb/simple_command_dispatcher.svg?refresh=5)](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)
7
7
  [![License](http://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)
8
8
 
9
- # Q. simple_command_dispatcher - what is it?
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
- ## Update as of Version 1.2.1
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
- ## Example
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
- This example assumes the following:
17
+ ## Features
23
18
 
24
- * `application_controller` is a base class, inherited by all other controllers. The `#authenticate_request` method is called for every request in order to make sure the request is authorized (`before_action :authenticate_request`).
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:
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
- ![N|Solid](https://cldup.com/1UeyWzOLic.png)
27
+ ## Installation
29
28
 
30
- Command classes (and the modules they reside under) are named *__according to their file name and respective location within the above folder structure__*; for example, the command class defined in the `/api/my_app1/v1/authenticate_request.rb` file would be defined in this manner:
29
+ Add this line to your application's Gemfile:
31
30
 
32
31
  ```ruby
33
- # /api/my_app1/v1/authenticate_request.rb
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
- Likewise, the command class defined in the `/api/my_app2/v2/update_user.rb` file would be defined in this manner, and so on:
35
+ And then execute:
46
36
 
47
- ```ruby
48
- # /api/my_app2/v2/update_user.rb
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
- The __routes used in this example__, conform to the following format: `"/api/[app_name]/[app_version]/[controller]"` where `[app_name]` = the _application name_,`[app_version]` = the _application version_, and `[controller]` = the _controller_; therefore, running `$ rake routes` for this example would output the following sample route information:
41
+ $ gem install simple_command_dispatcher
61
42
 
43
+ ## Requirements
62
44
 
63
- | Prefix | Verb | URI Pattern | Controller#Action |
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
- ### Request Authentication Code Snippet
50
+ ### Simple Command Dispatch
73
51
 
74
52
  ```ruby
75
- # /config/initializers/simple_command_dispatcher.rb
76
-
77
- # See: http://pothibo.com/2013/07/namespace-stuff-in-your-app-folder/
78
-
79
- =begin
80
- # Uncomment this code if you want to namespace your commands in the following manner, for example:
81
- #
82
- # class Api::MyApp1::V1::AuthenticateRequest; end
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
- Helpers.ensure_namespace("Api::MyApp1::V1")
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
- # Optionally set our configuration setting to allow
138
- # for custom command execution.
139
- SimpleCommand::Dispatcher.configure do |config|
140
- config.allow_custom_commands = true
141
- end
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
- # /app/controllers/application_controller.rb
146
-
147
- require 'simple_command_dispatcher'
148
-
149
- class ApplicationController < ActionController::API
150
- before_action :authenticate_request
151
- attr_reader :current_user
152
-
153
- protected
154
-
155
- def get_command_path
156
- # request.env['PATH_INFO'] could return any number of paths. The important
157
- # thing (in the case of our example), is that we get the portion of the
158
- # path that uniquely identifies the SimpleCommand we need to call; this
159
- # would include the application, the API version and the SimpleCommand
160
- # name itself.
161
- command_path = request.env['PATH_INFO'] # => "/api/[app name]/v1/[action]”
162
- command_path = command_path.split('/').slice(0,4).join('/') # => "/api/[app name]/v1/"
163
- end
164
-
165
- private
166
-
167
- def authenticate_request
168
- # The parameters and options we are passing to the dispatcher, wind up equating
169
- # to the following: Api::MyApp1::V1::AuthenticateRequest.call(request.headers).
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
- ## Custom Commands
104
+ **The Convention:** Request path `/api/v1/mechs/search` automatically maps to command class `Api::V1::Mechs::Search`
188
105
 
189
- As of __Version 1.2.1__ simple_command_dispatcher (SCD) allows you to execute _custom commands_ (i.e. classes that do not prepend the _SimpleCommand_ module) by setting `Configuration#allow_custom_commands = true`.
106
+ **Alternative approach** if you need more control over command name and namespace:
190
107
 
191
- In order to execute _custom commands_, there are three (3) requirements:
192
- 1. Create a _custom command_. Your _custom command_ class must expose a public `::call` class method.
193
- 2. Set the `Configuration#allow_custom_commands` property to `true`.
194
- 3. Execute your _custom command_ by calling the `::call` class method.
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
- ### Custom Command Example
117
+ ### Versioned Command Examples
197
118
 
198
- #### 1. Create a Custom Command
199
119
  ```ruby
200
- # /api/my_app/v1/custom_command.rb
120
+ # app/commands/api/v1/mechs/search.rb
121
+ class Api::V1::Mechs::Search
122
+ prepend SimpleCommandDispatcher::Commands::CommandCallable
201
123
 
202
- module Api
203
- module MyApp
204
- module V1
124
+ def initialize(params = {})
125
+ @name = params[:name]
126
+ end
205
127
 
206
- # This is a custom command that does not prepend SimpleCommand.
207
- class CustomCommand
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
- def self.call(*args)
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
- private
135
+ attr_reader :name
136
+ end
219
137
 
220
- def initialize(params = {})
221
- @param1 = params[:param1]
222
- end
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
- private
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
- attr_accessor :param1
170
+ scope :by_variant, ->(variant) {
171
+ variant.present? ? where("variant ILIKE ?", "%#{variant}%") : none
172
+ }
227
173
 
228
- def execute
229
- if (param1 == :param1)
230
- return true
231
- end
174
+ scope :by_tonnage, ->(tonnage) {
175
+ tonnage.present? ? where(tonnage: tonnage) : none
176
+ }
232
177
 
233
- return false
234
- end
235
- end
178
+ scope :by_cost, ->(cost) {
179
+ cost.present? ? where(cost: cost) : none
180
+ }
236
181
 
237
- end
238
- end
182
+ scope :by_introduction_year, ->(year) {
183
+ year.present? ? where(introduction_year: year) : none
184
+ }
239
185
  end
240
186
  ```
241
- #### 2. Set the `Configuration#allow_custom_commands` property to `true`
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
- # In your rails, rails-api app, etc...
244
- # /config/initializers/simple_command_dispatcher.rb
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
- SimpleCommand::Dispatcher.configure do |config|
247
- config.allow_custom_commands = true
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
- #### 3. Execute your _Custom Command_
252
- Executing your _custom command_ is no different than executing a __SimpleCommand__ command with the exception that you must properly handle the return object that results from calling your _custom command_; being a _custom command_, there is no guarantee that the return object will be the command object as is the case when calling a SimpleCommand command.
270
+ ### Payment Processing Example
271
+
253
272
  ```ruby
254
- # /app/controllers/some_controller.rb
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
- require 'simple_command_dispatcher'
313
+ **Route:** `POST /api/v1/payments/process` automatically calls `Api::V1::Payments::Process.call(params)`
257
314
 
258
- class SomeController < ApplicationController::API
259
- public
315
+ ### Custom Commands
260
316
 
261
- def some_api
262
- success = SimpleCommand::Dispatcher.call(:CustomCommand, get_command_path, { camelize: true}, request.headers)
263
- if success
264
- # Do something...
265
- else
266
- # Do something else...
267
- end
268
- end
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
- ## Installation
341
+ ## Configuration
273
342
 
274
- Add this line to your application's Gemfile:
343
+ The gem can be configured in an initializer:
275
344
 
276
345
  ```ruby
277
- gem 'simple_command_dispatcher'
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
- And then execute:
352
+ ## Migration from v3.x
281
353
 
282
- $ bundle
354
+ If you're upgrading from v3.x, here are the key changes:
283
355
 
284
- Or install it yourself as:
356
+ ### Breaking Changes
285
357
 
286
- $ gem install simple_command_dispatcher
358
+ 1. **Method signature changed to keyword arguments:**
287
359
 
288
- ## Usage
360
+ ```ruby
361
+ # v3.x (old)
362
+ SimpleCommandDispatcher.call(:CreateUser, 'Api::V1', { options }, params)
289
363
 
290
- See the example above.
364
+ # v4.x (new)
365
+ SimpleCommandDispatcher.call(
366
+ command: :CreateUser,
367
+ command_namespace: 'Api::V1',
368
+ request_params: params
369
+ )
370
+ ```
291
371
 
292
- ## Development
372
+ 2. **Removed simple_command dependency:**
293
373
 
294
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- [simple-command]: <https://rubygems.org/gems/simple_command>
308
- [rails-api]: <https://rubygems.org/gems/rails-api>
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