SimpleController

Use the Ruby on Rails Controller pattern outside of the Rails request stack.

Installation

Add this line to your application's Gemfile:

gem 'simple_controller'

And then execute:

$ bundle

Or install it yourself as:

$ gem install simple_controller

Usage

class UserController < SimpleController::Base
    before_action do
        @user = User.find(params[:user_id])
    end

    def touch
        @user.touch
        @user
    end
end

UserController.call(:touch, user_id: 1) # => returns User object
UserController.new.call(:touch, user_id: 1) # => same as above

It works like a Rails Controller, but has only has the following features:

  • Callbacks
  • params
  • action_name

Router

An optional router is provided to decouple controller classes from identifiers.

class Router < SimpleController::Router; end

# Router.instance is a singleton for ease of use
Router.instance.draw do
  match "threes/multiply"
  match "threes/dividing" => "threes#divide"

  controller :threes do
    match :add
    match subtracting: "subtract"
  end
  # custom syntax
  controller :threes, actions: %i[power]

  namespace :some_namespace do
    match :magic
  end

  # no other Rails-like syntax is available
end

Router.call("threes/multiply", number: 6) # calls ThreesController.call(:multiply, number: 6)
Router.instance.call("threes/multiply", number: 6) # same as above

To customize the controller class:

Router.instance.parse_controller_path do |controller_path|
  # similar to default implementation
  "#{controller_name}_suffix_controller".classify.constantize
end
Router.call("threes/multiply", number: 6) # calls ThreesSuffixController.call(:multiply, number: 6)

Routers add the following Rails features to the Controllers:

  • params[:action] and params[:controller]
  • controller_path and controller_name

Post-Process

Inspired by rails/sprockets Processors, Routes can have processor suffixes to ensure that controller endpoints are composable. For example, given:

class FoldersController < FileSystemController
  def two_readmes
    dir = create_new_directory
    dir.add_files Router.call('files/readme'), Router.call('files/readme')
    dir.path
  end
end

class FilesController < FileSystemController
  def readme
    write_new_file_and_return_path
  end
end

And the Router is set up, FoldersController#two_readmes generates a directory of FilesController#readmes. Processors add the ability to do these calls:

# calls the `s3_key` processor
Router.call('files/readme.s3_key')
# equivalent to:
FilesController.call(:readme, {}, { processors: [:s3_key] }) 

# calls the `zip` processor then the `s3_key` processor
Router.call('folders/two_readmes.s3_key.zip')
# equivalent to (NOTE the reverse order to the processor suffixes):
FoldersController.call(:two_readmes, {}, { processors: [:zip, :s3_key] })

The processors (zip and s3_key) can be defined and implemented in a common Parent controller, in this case:

class FileSystemController < SimpleController::Base
  # output => "path_to_file_or_directory"
  # processors => some combination of [:zip, :s3_key]
  def post_process(output, processors)
    processors.each do |processor|
      case processor
      when :zip
        output = zip_directory_and_return_path_of_zip(output)
      when :s3_key
        output = upload_file_path_to_amazon_s3_and_return_the_s3_key(output)
      end
    end
    output
  end
end