Plezi's Controllers

In the core of Plezi's framework is a smart Object Oriented Router which acts like a "virtual folder" with RESTful routing and Websocket support.

RESTful routing and Websocket callback support both allow us to use conventionally named methods in our Controller to achive common tasks. Such names methods, as will be explored further on, include the update, create and show RESTful method names, as well as the on_open, on_message(data) and on_close Websocket callbacks.

The first layer of this powerful routing system is the Plezi's Http Router and the core method Plezi.route.

The second layer of this powerful routing system is the Controller class which we are about explore.

What is a Controller Class?

Plezi has the ability to take in any class as a controller class and route HTTP requests to the classes public methods. This powerful routing system has built-in support for RESTful CRUD methods (index, show, new, create, update, delete) and for WebSockets (pre_connect, on_open, on_message(data), on_close, subscribe, and publish).

In effect, Controller classes act as "virtual folders" where methods behave similarly to "files" and where new "files" can be created, edited and deleted by implementing the special (reserved) methods for these actions.

To use a class as a Controller, simply attach it to a route. i.e., type the following in your irb terminal:

require 'plezi'
class UsersController
    def index
        "All Users"
    end
    def show
      "I would love to show you #{params[:id]}... later."
    end
    def foo
      "bar"
    end
end
Plezi.route "/users", UsersController
exit # on `irb` we start Plezi by exiting `irb`

Notice the difference between localhost:3000/users/foo and localhost:3000/users/bar.

Create Read Update Delete - CRUD

Plezi contains special support for CRUD operations (Create, Read, Update, Delete) and RESTful requests through the use of the :id parameter (params['id']) and the following reserverd method names: index, new, create, show, update and delete.

By reviewing the HTTP request method (GET, POST, PATCH, DELETE) the id parameter (absent? new?) and the optional _method parameters, Plezi will route RESTful requests to the correct CRUD method:

When any of the HTTP methods aren't supported by the client (i.e., some browsers don't support the DELETE method), it's possible to use the :_method parameter to emulate any method. i.e. /resource/id?:_method=delete.

A sample CRUD Controller

Here's a sample Controller for CRUD RESTful requests:

require 'plezi'
class CRUDCtrl
  # every request that routes to this controller will create a new instance
  def initialize
  end

  # called when request is GET and params['id'] isn't defined
  def index
    'Listing all objects...'
  end

  # called when request is GET and params['id'] exists
  def show
    "nothing to show for id - #{params['id']} - with parameters: #{params}"
  end

  # called when request is GET and params['id'] == "new" (used for the "create new object" form).
  def new
    'Should we make something new?'
  end

  # called when request is POST or PUT and params['id'] isn't defined or params[:id] == "new"
  def create
    'save called - creating a new object.'
  end

  # called when request is POST or PUT and params['id'] exists and isn't "new"
  def update
    "update called - updating #{params['id']}"
  end

  # called when request is DELETE (or params['_method'] == 'delete') and request.params[:id] exists
  def delete
    "delete called - deleting object #{params['id']}"
  end
end
# a simple RESTful path - all paths are assumed to be RESTful
Plezi.route '/object', CRUDCtrl
# OR, to allow inline _method request parameter, such as POST, PUT, GET or DELETE
Plezi.route '/object/(:id)/(:_method)', CRUDCtrl

For using the Plezi.route paths to add different request parameters, refer to the routes guide.

The Controller as a virtual folder

As already demonstrated by the RESTful API design, the 'id' parameter is used to route the request to a specific CRUD method.

However, the 'id' parameter can also be used to GET, POST or DELETE data using custom methods, so that a Controller can act as a "virtual API folder" or to group together a group of resources.

Here's and example root path that uses method names to publish . i.e.:

require 'plezi'

class RootCtrl
    def index
        "Welcome..."
    end
    def sitemap
        "/ - welcome page\n/sitemap - this page\n/echo - you know"
    end
    def echo
        request.env.to_s
    end
    def sweet_and_special msg = nil
      msg ||= params
        "This idiomatic Plezi method signature answers both HTTP (AJAJ) and Websocket Auto-Dispatch."
    end
    def _not_published
      "methods starting with an underscore aren't exposed"
    end
    def has _arguments
      "methods with required arguments aren't exposed"
    end
    protected
    def secret
      "only un-inherited public methods are exposed."
    end
end
Plezi.route '/', RootCtrl

This is a very powerful feature, especially when writing a backend with a Websocket API and AJAJ (AJAX + JSON) fallback (see the JSON websocket Auto-Dispatch guide).

Run this example route and try visiting:

Websocket Callbacks

Controllers can also be used for Websocket connections, which makes it easier to reuse code when publishing an API that offers both HTTP and Websocket access.

In order to answer Websocket connections, Plezi (and Iodine) define the following reserved callback methods: pre_connect, on_open, on_message(data), on_close and on_shutdown.

To learn more about these callbacks and about Websockets, read the websockets guide.

Helper methods and objects

Once a controller class had been attached to a route, Plezi will inherit this class and add to it some functionality that is available within the controller's methods.

The following properties and methods are accessible from within your Controller classes.

The request object

The request object is a Rack::Request object, containing the request's information and some helper methods.

Read more at the YARD documentation for the Request object.

The response object

The response object is a Rack::Response object and it more control over the response, such as setting headers, cookies etc'.

Read more at the YARD documentation for the Response object.

The params hash

The params Hash includes all the data from Rack's request.params as well as any parameters provided within the route (more on that in the routes guide).

Param keys are always strings (never symbols). Values are always Strings (except for '_method' which is a symbol).

Some param names are reserved for specific features, as they follow common practice. Reserved params names include:

Read about routes to discover how to use the request path to set parameters.

It's possible to use @params = Plezi.rubyfy params to rehash all the parameters so that all the data is html-sanitized (not SQL sanitized), the keys use Symbols instead of Strings and some strings are converted to their Ruby object couterpart (i.e. Fixnums, true, false and nil - empty strings are replaced with nil). However, this could break binary file uploads, so it isn't performed by default.

The cookies cookie jar is a Hash that both reads and writes cookies. It's a bit of syntetic sugar over Rack's response.set_cookie, response.delete_cookie and Rack's request.cookies hash access.

To read a cookie name, simply:

cookies['name']

To set a cookie's value, simply:

cookies['name'] = "value"

Since cookies are set using Rack's response.set_cookie options, it's possible to set extra cookie properties when setting the value. i.e.:

cookies[:name] = {value: "value", secure: true, httponly: true}

Remember, cookies are sent to the browser using HTTP headers - it is not possible to set cookies after the headers were sent. Make sure to set all cookies before aresponse is set - this is especially important to remember when implementing Websockets, remember to use the pre_connect callback for cookie setting.

The render method

Renders a template file (i.e. .slim/.erb/.md) to a String and attempts to set the response's 'content-type' header (if it's still empty).

For example, to render the file users/index.html.erb with the layout layout.html.slim:

render("layout") { render("users/index") }

Rr, for example, to render the same content in JSON format, using the templates users/index.json.erb with the layout layout.json.erb

params['format'] = 'json'
render("layout") { render("users/index") }

Read more at the YARD documentation for this method.

Rendering can be extended to include more render engines using the Plezi::Renderer.register method.

The requested_method method

This method review's the request and returns the name of the controller method that was invoked.

The url_for URL builder

The controller will attempt to guess the URL used to reach any path within it's parameters, setting query parameters if the parameters are not part of the route's fixed path parameters. i.e.:

MyController.url_for :index # the root of the controller
MyController.url_for id: 1, \_method: :delete # the DELETE method can be emulated for RESTful requests.

Read more at the YARD documentation for this method.

The send_data(data, options = {}) helper

Sends a block of data, setting the file name, mime type and content disposition headers when possible.

This should be a good choice when sending large amounts of data, as the application could leverage future optimizations without the need to update it's code base.

By default, send_data sends the data as an attachment, unless inline: true was set.

If a mime type is provided, it will be used to set the Content-Type header. i.e. mime: "text/plain"

If a file name was provided, Rack will be used to find the correct mime type (unless provided). i.e. filename: "sample.pdf" will set the mime type to application/pdf

Available options: :inline (true / false), :filename, :mime.

The send_file(filename, options = {}) helper

Same as the send_data helper method, but accepts a file name (to be opened and sent) rather then a String.

This method will attempt to leverage Iodines X-Sendfile support when available (when the static file service is active).

The options for this method are the same as the ones used for send_data.

Websocket specific helpers

Some Controller helper methods are only available and relevant after a Websocket connection was established.

Using id to identify a Websocket

A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using #unicast.

Using write to write to a Websocket

After a websocket connection is established, it is possible to use write to write data to the websocket directly.

write writes data to the websocket, framing the data as stated by the Websocket protocol.

data should be a String object.

If the String is Binary encoded, the data will be sent as binary data (according to the Websockets protocol), otherwise the data will be sent as a UTF8 string data. It's possible to use Plezi's Plezi.try_utf8! which will set a String's encoding to UTF-8 only when it's a valid encoding for that String.

Using write before a websocket connection was established will invoke undefined behavior and will probably leading nuclear missiles being directed at your home town.

Read more at the YARD documentation for this method.

The close method (Websockets)

close closes the websocket connection after all the data was sent and sending the Websocket's appropriate goodbye frame.

Read more at the YARD documentation for this method.

Pub/Sub with subscribe and publish

Plezi leverages Iodine's native Pub/Sub. To learn more about Pub/Sub you can peak at the websocket examples. I hope to write a tutorial soon.

The basic use is very simple:

subscription = subscribe :stream
# or
subscription = subscribe(:stream) {|stream, message| write "#{stream}: #{message}" }
publish :stream, "a message"
subscription.close

Subscriptions opened using a connection's method will be automatically destroyed when the connection closes.

Global (non-connection related) subscriptions can be opened as well using the Iodine::PubSub methods.