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. SCD was written specifically with the rails-api in mind; however, you can use SDC wherever you would use simple_command commands.
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).
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.
This example assumes the following:
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
).request.headers
will contain the authorization token to authorize all requests (request.headers["Authorization"]
)- This application uses the following folder structure to manage its simple_command commands:
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:
# /api/my_app1/v1/authenticate_request.rb
module Api
module MyApp1
module V1
class AuthenticateRequest
end
end
end
end
Likewise, the command class defined in the /api/my_app2/v2/update_user.rb
file would be defined in this manner, and so on:
# /api/my_app2/v2/update_user.rb
module Api
module MyApp2
module V2
class UpdateUser
end
end
end
end
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:
Prefix | Verb | URI Pattern | Controller#Action |
---|---|---|---|
api_my_app1_v1_user_authenticate | POST | /api/my_app1/v1/user/authenticate(.:format) | api/my_app1/v1/authentication#create |
api_my_app1_v2_user_authenticate | POST | /api/my_app1/v2/user/authenticate(.:format) | api/my_app1/v2/authentication#create |
api_my_app2_v1_user_authenticate | POST | /api/my_app2/v1/user/authenticate(.:format) | api/my_app2/v1/authentication#create |
api_my_app2_v2_user | PATCH | /api/my_app2/v2/users/:id(.:format) | api/my_app2/v2/users#update |
PUT | /api/my_app2/v2/users/:id(.:format) | api/my_app2/v2/users#update |
# /config/initializers/simple_command_dispatcher.rb
# See: http://pothibo.com/2013/07/namespace-stuff-in-your-app-folder/
=begin
# Uncomment this code if you want to namespace your commands in the following manner, for example:
#
# class Api::MyApp1::V1::AuthenticateRequest; end
#
# As opposed to this:
#
# module Api
# module MyApp1
# module V1
# class AuthenticateRequest
# end
# end
# end
# end
#
module Helpers
def self.ensure_namespace(namespace, scope = "::")
namespace_parts = namespace.split("::")
namespace_chain = ""
namespace_parts.each { | part |
namespace_chain = (namespace_chain.empty?) ? part : "#{namespace_chain}::#{part}"
eval("module #{scope}#{namespace_chain}; end")
}
end
end
Helpers.ensure_namespace("Api::MyApp1::V1")
Helpers.ensure_namespace("Api::MyApp1::V2")
Helpers.ensure_namespace("Api::MyApp2::V1")
Helpers.ensure_namespace("Api::MyApp2::V2")
=end
# simple_command_dispatcher creates commands dynamically; therefore we need
# to make sure the namespaces and command classes are loaded before we construct and
# call them. The below code traverses the 'app/api' and all subfolders, and
# autoloads them so that we do not get any NameError exceptions due to
# uninitialized constants.
Rails.application.config.to_prepare do
path = Rails.root + "app/api"
ActiveSupport::Dependencies.autoload_paths -= [path.to_s]
reloader = ActiveSupport::FileUpdateChecker.new [], path.to_s => [:rb] do
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
Dir[path + "**/*.rb"].each do |file|
ActiveSupport.require_or_load file
end
end
Rails.application.reloaders << reloader
ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
reloader.execute
end
# Optionally set our configuration setting to allow
# for custom command execution.
SimpleCommand::Dispatcher.configure do |config|
config.allow_custom_commands = true
end
# /app/controllers/application_controller.rb
require 'simple_command_dispatcher'
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user
protected
def get_command_path
# request.env['PATH_INFO'] could return any number of paths. The important
# thing (in the case of our example), is that we get the portion of the
# path that uniquely identifies the SimpleCommand we need to call; this
# would include the application, the API version and the SimpleCommand
# name itself.
command_path = request.env['PATH_INFO'] # => "/api/[app name]/v1/[action]”
command_path = command_path.split('/').slice(0,4).join('/') # => "/api/[app name]/v1/"
end
private
def authenticate_request
# The parameters and options we are passing to the dispatcher, wind up equating
# to the following: Api::MyApp1::V1::AuthenticateRequest.call(request.headers).
# Explaination: @param command_modules (e.g. path, "/api/my_app1/v1/"), in concert with @param
# options { camelize: true }, is transformed into "Api::MyApp1::V1" and prepended to the
# @param command, which becomes "Api::MyApp1::V1::AuthenticateRequest." This string is then
# simply constantized; #call is then executed, passing the @param command_parameters
# (e.g. request.headers, which contains ["Authorization"], out authorization token).
# Consequently, the correlation between our routes and command class module structure
# was no coincidence.
command = SimpleCommand::Dispatcher.call(:AuthenticateRequest, get_command_path, { camelize: true}, request.headers)
if command.success?
@current_user = command.result
else
render json: { error: 'Not Authorized' }, status: 401
end
end
end
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
.
In order to execute custom commands, there are three (3) requirements:
- Create a custom command. Your custom command class must expose a public
::call
class method. - Set the
Configuration#allow_custom_commands
property totrue
. - Execute your custom command by calling the
::call
class method.
# /api/my_app/v1/custom_command.rb
module Api
module MyApp
module V1
# This is a custom command that does not prepend SimpleCommand.
class CustomCommand
def self.call(*args)
command = self.new(*args)
if command
command.send(:execute)
else
false
end
end
private
def initialize(params = {})
@param1 = params[:param1]
end
private
attr_accessor :param1
def execute
if (param1 == :param1)
return true
end
return false
end
end
end
end
end
# In your rails, rails-api app, etc...
# /config/initializers/simple_command_dispatcher.rb
SimpleCommand::Dispatcher.configure do |config|
config.allow_custom_commands = true
end
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.
# /app/controllers/some_controller.rb
require 'simple_command_dispatcher'
class SomeController < ApplicationController::API
public
def some_api
success = SimpleCommand::Dispatcher.call(:CustomCommand, get_command_path, { camelize: true}, request.headers)
if success
# Do something...
else
# Do something else...
end
end
end
Add this line to your application's Gemfile:
gem 'simple_command_dispatcher'
And then execute:
$ bundle
Or install it yourself as:
$ gem install simple_command_dispatcher
See the example above.
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.
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.
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 code of conduct.
The gem is available as open source under the terms of the MIT License.