Use the Ruby on Rails Controller pattern outside of the Rails request stack.
Add this line to your application's Gemfile:
gem 'simple_controller'
And then execute:
$ bundle
Or install it yourself as:
$ gem install simple_controller
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
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]
andparams[:controller]
controller_path
andcontroller_name
Inspired by rails/sprockets
Processors, Routes can have processor suffixes to ensure that controller endpoints
are composable, but allows a final processing step. 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#readme
s. Processors add the ability to do these calls:
# calls the `s3_key` processor (upload to s3 and return the s3_key)
Router.call('files/readme.s3_key')
# equivalent to:
FilesController.call(:readme, {}, { processors: [:s3_key] })
# calls the `zip` processor then the `s3_key` processor
# in other words, zip the directory then upload to s3 and return the s3_key
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] })
Internally, the #post_process
method takes the output of your action
and does the following:
# pseudo-code for `SimpleController::Base#call`
def call
post_process(__call_the_controller_action__, processors)
end
So the processors (zip
and s3_key
) can be defined and implemented in a common Parent controller #post_process
method, 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