An open API service indexing awesome lists of open source software.

https://github.com/gangelo/simple_command_dispatcher

A Ruby Gem that dispatches SimpleCommands (simple_command gem) dynamically, so that API applications do not have to hard-code things like api modules and version numbers.
https://github.com/gangelo/simple_command_dispatcher

api api-rest rails rails-api rails-gem ruby ruby-on-rails

Last synced: 2 months ago
JSON representation

A Ruby Gem that dispatches SimpleCommands (simple_command gem) dynamically, so that API applications do not have to hard-code things like api modules and version numbers.

Awesome Lists containing this project

README

        

[![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)
[![GitHub version](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher.svg?refresh=6)](https://badge.fury.io/gh/gangelo%2Fsimple_command_dispatcher)
[![Gem Version](https://badge.fury.io/rb/simple_command_dispatcher.svg?refresh=6)](https://badge.fury.io/rb/simple_command_dispatcher)
[![](https://ruby-gem-downloads-badge.herokuapp.com/simple_command_dispatcher?type=total)](http://www.rubydoc.info/gems/simple_command_dispatcher/)
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/simple_command_dispatcher/)
[![Report Issues](https://img.shields.io/badge/report-issues-red.svg)](https://github.com/gangelo/simple_command_dispatcher/issues)
[![License](http://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)

# Q. simple_command_dispatcher - what is it?
# A. It's a Ruby gem!!!

## Overview
__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.

## Update as of Version 1.2.1
### Custom 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).

## Example
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:

![N|Solid](https://cldup.com/1UeyWzOLic.png)

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:

```ruby
# /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:

```ruby
# /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 |

### Request Authentication Code Snippet

```ruby
# /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
```

```ruby
# /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
```

## Custom Commands

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:
1. Create a _custom command_. Your _custom command_ class must expose a public `::call` class method.
2. Set the `Configuration#allow_custom_commands` property to `true`.
3. Execute your _custom command_ by calling the `::call` class method.

### Custom Command Example

#### 1. Create a Custom Command
```ruby
# /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
```
#### 2. Set the `Configuration#allow_custom_commands` property to `true`
```ruby
# In your rails, rails-api app, etc...
# /config/initializers/simple_command_dispatcher.rb

SimpleCommand::Dispatcher.configure do |config|
config.allow_custom_commands = true
end
```

#### 3. Execute your _Custom Command_
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.
```ruby
# /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
```

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'simple_command_dispatcher'
```

And then execute:

$ bundle

Or install it yourself as:

$ gem install simple_command_dispatcher

## Usage

See the example above.

## Development

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](https://rubygems.org).

## Contributing

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.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

[simple-command]:
[rails-api]: