Giter Club home page Giter Club logo

controller's Introduction

Lotus::Controller

A Rack compatible Controller layer for Lotus.

Status

Gem Version Build Status Coverage Code Climate Dependencies Inline docs

Contact

Rubies

Lotus::Controller supports Ruby (MRI) 2+

Installation

Add this line to your application's Gemfile:

gem 'lotus-controller'

And then execute:

$ bundle

Or install it yourself as:

$ gem install lotus-controller

Usage

Lotus::Controller is a micro library for web frameworks. It works beautifully with Lotus::Router, but it can be employed everywhere. It's designed to be fast and testable.

Actions

The core of this frameworks are the actions. They are the endpoint that responds to incoming HTTP requests.

class Show
  include Lotus::Action

  def call(params)
    @article = Article.find params[:id]
  end
end

The usage of Lotus::Action follows the Lotus philosophy: include a module and implement a minimal interface. In this case, it's only one method: #call(params).

Lotus is designed to not interfere with inheritance. This is important, because you can implement your own initialization strategy.

An action is an object after all, it's important that you have the full control on it. In other words, you have the freedom of instantiate, inject dependencies and test it, both with unit and integration.

In the example below, we're stating that the default repository is Article, but during an unit test we can inject a stubbed version, and invoke #call with the params that we want to simulate. We're avoiding HTTP calls, we're eventually avoiding to hit the database (it depends on the stubbed repository), we're just dealing with message passing. Imagine how fast can be a unit test like this.

class Show
  include Lotus::Action

  def initialize(repository = Article)
    @repository = repository
  end

  def call(params)
    @article = @repository.find params[:id]
  end
end

action = Show.new(MemoryArticleRepository)
action.call({ id: 23 })

Params

The request params are passed as an argument to the #call method. If routed with Lotus::Router, it extracts the relevant bits from the Rack env (eg the requested :id). Otherwise everything it's passed as it is: the full Rack env in production, and the given Hash for unit tests.

With Lotus::Router:

class Show
  include Lotus::Action

  def call(params)
    # ...
    puts params # => { id: 23 } extracted from Rack env
  end
end

Standalone:

class Show
  include Lotus::Action

  def call(params)
    # ...
    puts params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
  end
end

Unit Testing:

class Show
  include Lotus::Action

  def call(params)
    # ...
    puts params # => { id: 23, key: 'value' } passed as it is from testing
  end
end

action   = Show.new
response = action.call({ id: 23, key: 'value' })

Response

The output of #call is a serialized Rack::Response (see #finish):

class Show
  include Lotus::Action

  def call(params)
    # ...
  end
end

action = Show.new
action.call({}) # => [200, {}, [""]]

It has private accessors to explicitly set status, headers and body:

class Show
  include Lotus::Action

  def call(params)
    self.status  = 201
    self.body    = 'Hi!'
    self.headers.merge!({ 'X-Custom' => 'OK' })
  end
end

action = Show.new
action.call({}) # => [201, { "X-Custom" => "OK" }, ["Hi!"]]

Exposures

We know that actions are objects and Lotus::Action respects one of the pillars of OOP: encapsulation. Other frameworks extract instance variables (@ivar) and make them available to the view context. The solution of Lotus::Action is a simple and powerful DSL: expose. It's a thin layer on top of attr_reader. When used, it creates a getter for the given attribute, and adds it to the exposures. Exposures (#exposures) is set of exposed attributes, so that the view context can have the information needed to render a page.

class Show
  include Lotus::Action

  expose :article

  def call(params)
    @article = Article.find params[:id]
  end
end

action = Show.new
action.call({ id: 23 })

assert_equal 23, action.article.id

puts action.exposures # => { article: <Article:0x007f965c1d0318 @id=23> }

Callbacks

It offers powerful, inheritable callbacks chain which is executed before and/or after your #call method invocation:

class Show
  include Lotus::Action

  before :authenticate, :set_article

  def call(params)
  end

  private
  def authenticate
    # ...
  end

  # `params` in the method signature is optional
  def set_article(params)
    @article = Article.find params[:id]
  end
end

Callbacks can also be expressed as anonymous lambdas:

class Show
  include Lotus::Action

  before { ... } # do some authentication stuff
  before {|params| @article = Article.find params[:id] }

  def call(params)
  end
end

Exceptions management

When an exception is raised, it automatically sets the HTTP status to 500:

class Show
  include Lotus::Action

  def call(params)
    raise
  end
end

action = Show.new
action.call({}) # => [500, {}, ["Internal Server Error"]]

You can define how a specific raised exception should be transformed in an HTTP status.

class Show
  include Lotus::Action
  handle_exception RecordNotFound => 404

  def call(params)
    @article = Article.find params[:id]
  end
end

action = Show.new
action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]

Exception policies can be defined globally, before the controllers/actions are loaded.

Lotus::Controller.configure do
  handle_exception RecordNotFound => 404
end

class Show
  include Lotus::Action

  def call(params)
    @article = Article.find params[:id]
  end
end

action = Show.new
action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]

This feature can be turned off globally, in a controller or in a single action.

Lotus::Controller.configure do
  handle_exceptions false
end

# or

class ArticlesController
  include Lotus::Controller

  configure do
    handle_exceptions false
  end

  action 'Show' do
    def call(params)
      @article = Article.find params[:id]
    end
  end
end

action = ArticlesController::Show.new
action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]

Throwable HTTP statuses

When #halt is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:

class Show
  include Lotus::Action

  before :authenticate!

  def call(params)
    # ...
  end

  private
  def authenticate!
    halt 401 unless authenticated?
  end
end

action = Show.new
action.call({}) # => [401, {}, ["Unauthorized"]]

Cookies

It offers convenient access to cookies.

They are read as an Hash from Rack env:

require 'lotus/controller'
require 'lotus/action/cookies'

class ReadCookiesFromRackEnv
  include Lotus::Action
  include Lotus::Action::Cookies

  def call(params)
    # ...
    cookies[:foo] # => 'bar'
  end
end

action = ReadCookiesFromRackEnv.new
action.call({'HTTP_COOKIE' => 'foo=bar'})

They are set like an Hash:

require 'lotus/controller'
require 'lotus/action/cookies'

class SetCookies
  include Lotus::Action
  include Lotus::Action::Cookies

  def call(params)
    # ...
    cookies[:foo] = 'bar'
  end
end

action = SetCookies.new
action.call({}) # => [200, {'Set-Cookie' => 'foo=bar'}, '...']

They are removed by setting their value to nil:

require 'lotus/controller'
require 'lotus/action/cookies'

class RemoveCookies
  include Lotus::Action
  include Lotus::Action::Cookies

  def call(params)
    # ...
    cookies[:foo] = nil
  end
end

action = SetCookies.new
action.call({}) # => [200, {'Set-Cookie' => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"}, '...']

Sessions

It has builtin support for Rack sessions:

require 'lotus/controller'
require 'lotus/action/session'

class ReadSessionFromRackEnv
  include Lotus::Action
  include Lotus::Action::Session

  def call(params)
    # ...
    session[:age] # => '31'
  end
end

action = ReadSessionFromRackEnv.new
action.call({ 'rack.session' => { 'age' => '31' }})

Values can be set like an Hash:

require 'lotus/controller'
require 'lotus/action/session'

class SetSession
  include Lotus::Action
  include Lotus::Action::Session

  def call(params)
    # ...
    session[:age] = 31
  end
end

action = SetSession.new
action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."]

Values can be removed like an Hash:

require 'lotus/controller'
require 'lotus/action/session'

class RemoveSession
  include Lotus::Action
  include Lotus::Action::Session

  def call(params)
    # ...
    session[:age] = nil
  end
end

action = RemoveSession.new
action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."] it removes that value from the session

While Lotus::Controller supports sessions natively, it's session store agnostic. You have to specify the session store in your Rack middleware configuration (eg config.ru).

use Rack::Session::Cookie, secret: SecureRandom.hex(64)
run Show.new

Redirect

If you need to redirect the client to another resource, use #redirect_to:

class Create
  include Lotus::Action

  def call(params)
    # ...
    redirect_to 'http://example.com/articles/23'
  end
end

action = Create.new
action.call({ article: { title: 'Hello' }}) # => [302, {'Location' => '/articles/23'}, '']

Mime types

Lotus::Action automatically sets the mime type, according to the request headers. However, you can override this value:

class Show
  include Lotus::Action

  def call(params)
    # ...
    self.content_type = 'application/json'
  end
end

action = Show.new
action.call({ id: 23 }) # => [200, {'Content-Type' => 'application/json'}, '...']

You can restrict the accepted mime types:

class Show
  include Lotus::Action
  accept :html, :json

  def call(params)
    # ...
  end
end

# When called with "\*/\*"            => 200
# When called with "text/html"        => 200
# When called with "application/json" => 200
# When called with "application/xml"  => 406

You can check if the requested mime type is accepted by the client.

class Show
  include Lotus::Action

  def call(params)
    # ...
    # @_env['HTTP_ACCEPT'] # => 'text/html,application/xhtml+xml,application/xml;q=0.9'

    accept?('text/html')        # => true
    accept?('application/xml')  # => true
    accept?('application/json') # => false



    # @_env['HTTP_ACCEPT'] # => '*/*'

    accept?('text/html')        # => true
    accept?('application/xml')  # => true
    accept?('application/json') # => true
  end
end

No rendering, please

Lotus::Controller is designed to be a pure HTTP endpoint, rendering belongs to other layers of MVC. You can set the body directly (see response), or use Lotus::View.

Controllers

A Controller is nothing more than a logical group for actions.

class ArticlesController
  class Index
    include Lotus::Action

    # ...
  end

  class Show
    include Lotus::Action

    # ...
  end
end

Which is a bit verboses. Instead, just do:

class ArticlesController
  include Lotus::Controller

  action 'Index' do
    # ...
  end

  action 'Show' do
    # ...
  end
end

ArticlesController::Index.new.call({})

Lotus::Router integration

While Lotus::Router works great with this framework, Lotus::Controller doesn't depend from it. You, as developer, are free to choose your own routing system.

But, if you use them together, the only constraint is that an action must support arity 0 in its constructor. The following examples are valid constructors:

def initialize
end

def initialize(repository = Article)
end

def initialize(repository: Article)
end

def initialize(options = {})
end

def initialize(*args)
end

Please note that this is subject to change: we're working to remove this constraint.

Lotus::Router supports lazy loading for controllers. While this policy can be a convenient fallback, you should know that it's the slower option. Be sure of loading your controllers before you initialize the router.

Rack integration

Lotus::Controller is compatible with Rack. However, it doesn't mount any middleware. While a Lotus application's architecture is more web oriented, this framework is designed to build pure HTTP endpoints.

Rack middleware

Rack middleware can be configured globally in config.ru, but often they add an unnecessary overhead for all those endpoints who aren't direct users of a certain middleware. Think about a middleware to create sessions, where only SessionsController::Create may be involved and the rest of the application shouldn't pay the performance ticket of calling that middleware.

An action can employ one or more Rack middleware, with .use.

require 'lotus/controller'

class SessionsController
  include Lotus::Controller

  action 'Create' do
    use OmniAuth

    def call(params)
      # ...
    end
  end
end
require 'lotus/controller'

class SessionsController
  include Lotus::Controller

  action 'Create' do
    use XMiddleware.new('x', 123)
    use YMiddleware.new
    use ZMiddleware

    def call(params)
      # ...
    end
  end
end

Configuration

Lotus::Controller can be configured with a DSL that determines its behavior. It supports a few options:

require 'lotus/controller'

Lotus::Controller.configure do
  # Handle exceptions with HTTP statuses (true) or don't catch them (false)
  # Argument: boolean, defaults to true
  #
  handle_exceptions true

  # If the given exception is raised, return that HTTP status
  # It can be used multiple times
  # Argument: hash, empty by default
  #
  handle_exception ArgumentError => 404

  # Configure which module to include when Lotus::Controller.action is used
  # Argument: module, defaults to Lotus::Action
  #
  action_module MyApp::Action # module, defaults to Lotus::Action

  # Configure the modules to be included/extended/prepended by default.
  # Argument: proc, empty by default
  #
  modules do
    include Lotus::Action::Sessions
    prepend MyLibrary::Session::Store
  end
end

All those global configurations can be overwritten at a finer grained level: controllers. Each controller and action has its own copy of the global configuration, so that changes are inherited from the top to the bottom, but not bubbled up in the opposite direction.

require 'lotus/controller'

Lotus::Controller.configure do
  handle_exception ArgumentError => 404
end

class ArticlesController
  include Lotus::Controller

  configure do
    handle_exceptions false
  end

  action 'Create' do
    def call(params)
      raise ArgumentError
    end
  end
end

class UsersController
  include Lotus::Controller

  action 'Create' do
    def call(params)
      raise ArgumentError
    end
  end
end

UsersController::Create.new.call({}) # => HTTP 400

ArticlesController::Create.new.call({})
  # => raises ArgumentError because we set handle_exceptions to false

Reusability

Lotus::Controller can be used as a singleton framework as seen in this README. The application code includes Lotus::Controller or Lotus::Action directly and the configuration is unique per Ruby process.

While this is convenient for tiny applications, it doesn't fit well for more complex scenarios, where we want micro applications to coexist together.

require 'lotus/controller'

module WebApp
  Controller = Lotus::Controller.duplicate
end

module ApiApp
  Controller = Lotus::Controller.duplicate(self) do
    handle_exception ArgumentError => 400
  end
end

The code above defines WebApp::Controller and WebApp::Action, to be used for the WebApp endpoints, while ApiApp::Controller and ApiApp::Action have a different configuration.

Thread safety

An Action is mutable. When used without Lotus::Router, be sure to instantiate an action for each request.

# config.ru
require 'lotus/controller'

class Action
  include Lotus::Action

  def self.call(env)
    new.call(env)
  end

  def call(params)
    self.body = object_id.to_s
  end
end

run Action

Versioning

Lotus::Controller uses Semantic Versioning 2.0.0

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Copyright

Copyright 2014 Luca Guidi โ€“ Released under MIT License

controller's People

Contributors

jodosha avatar rrrene avatar sidonath avatar stefanoverna avatar zlw avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.