Controllers and Routing

Controllers

Requests in Tailor are generally handled by Controllers. Controllers coordinate between the other subsystems in the app, for instance by fetching records from the database and passing them to a template which will use them to populate a web page. Controllers are also responsible for running filters, security checks, data integrity checks, processing forms, and so on.

when a request comes in, the routing system determines what controller and action the request is for, and creates a controller to handle it. A controller is created with a Request, a string with the action name, and a callback that it should call with a Response object. Once it is created, the action is run. Actions are described in more detail below.

Requests and Responses

When a request comes in, Tailor parse the raw data into a Request structure. You should mostly need to work with the requestParameters on the request, which is a dictionary mapping strings to strings from the request data. If you are dealing with a multipart form for file uploads, you can get the files from the request's uploadedFiles attribute. The request will provide more information about the request, such as the path, cookies, origin IP address, HTTP method, and raw data, but most of that should be handled by Tailor itself.

When your controller is ready to respond to the request, it will have to create a Response structure. There are four parts to a request: HTTP code, body, headers, and cookies. There are also some helpers for building the body in pieces: appendString and appendData. You generally shouldn't have to build requests by hand, though. The Controller class has a few helper methods that will build responses for common response types.

Requests and responses both have a cookies attribute, which will give you a CookieJar. This will allow you to read and write cookie values through a subscript. For instance, you can read a request's cookie value for the key "breadcrumb" by calling request.cookies["breadcrumb"]. If you want to set that cookie on a response, you can call response.cookies["breadcrumb"] = "hats/index".If you need to specify additional information when setting a cookie on a response, like the domain or expiry policy, you can use the setCookie method.

One very common use case for cookies is session management, and Tailor provides this automatically. The Session class manages an additional key-value store that will be kept in a single cookie with the key "_session". This information is encrypted with an AES key that stays on your app server, so the client cannot tamper with or even read the session data. The session also has the ability to store flash messages, using the flash and setFlash methods. When you set a flash message on the session data that you send our with a response, that message will be available for the next request, and then automatically cleared. This can be useful for setting one-time messages like confirmations of a successful action, which you don't want to keep showing up on later page loads. The controller will build a session from the request's cookie information when it is initialized, and make it available in the session attribute.

Response Helpers

Building a response from scratch for every action would get tedious, so Tailor provides helper methods for building responses. The simplest of these is generateResponse. This will create a response, set all the cookies from the original request on it, and then give the request to a block that you give. Once your block is done, the session information will be stored on the request, and it will be given to the controller's callback. This means that you just need to set the response information that is unique to your action, like the response code, custom headers, and response body. The method takes care of all the boiler plate.

There are two kinds of responses that Tailor provides more specific helpers for. The first is rendering a template. You can call the responseWith method, giving it a Template instance, and it will render the template into a response. You don't need to build the response or call generateResponse, you just need to provide the template.

The second kind of response helper is for redirecting. There are a few methods for this, supporting different kinds of redirect scenarios. They are all named redirectTo. The first version takes only a path, and is useful for redirecting to hard-coded paths, or paths that are provided by another source. The second version takes a controller name, action, and request parameters. This will build a path and redirect to it. All the arguments will default to matching the current request, so this can be a concise way to redirect to other actions in the same controller, or to the same action but with different request parameters. The third form takes a controller type, action, and request parameters. This does not have any default for the controller or action, and can be more concise when redirecting to other controllers.

There is also a method called render404, which responds with a 404 error. This can be helpful when handling a request with invalid parameters.

Actions

An action is a description of how a controller will handle a request. It contains a name, a body function that handles the request, and a list of filters that run pre-checks for the request. Typically the body and filters will be methods on your controller, but wrapping them in an action allows them to be called dynamically when the request comes in.

Filters provide a way to define code that runs before a controller action. They allow you to write checks that run before several actions, and which can render their own responses and prevent the action from running at all. For instance, many requests in your app may require that a user be signed in before they can access a page. You can define an authenticate method in a high-level controller which checks for this, and then apply it to all the different actions. You could also just call it in each action, and return from the action if the filter returns false, but that can make it too easy to forget to add an authentication check when you add a new action.

Another common use case for filters is checking the correctness of request parameters. Let's say you have a HatsController that will pull up Hats by an id in the request parameters. For the actions that rely on this id, like show, edit, and update, you want to check that the id is valid before it causes a problem. You could add a filter that tries to pull up the hat, and if it cannot find one, redirects to the list of hats with an error message.

Your controllers must declare their available actions in the class-level actions method. Here's what that might look like.

class HatsController: Controller {
  override class func actions() -> Action {
    return [
      Action(name: "index", body: wrap(indexAction)),
      Action(name: "new", body: wrap(newAction)),
      Action(name: "create", body: wrap(createAction)),
      Action(name: "show", body: wrap(showAction), filters: [wrap(checkHat)]),
      Action(name: "edit", body: wrap(editAction), filters: [wrap(checkHat)]),
      Action(name: "update", body: wrap(updateAction), filters: [wrap(checkHat)])
    ]
  }

  /*
    The rest of the methods are provided in the example further down in
    this guide.
    */
}

This syntax takes advantage of the fact that instance methods can be accessed at the class level to get a curried version. For instance, if indexAction is an instance method on HatsController, then HatsController.indexAction returns a block that takes in a HatsController and returns another block, which is an implementation of running indexAction on that controller. The wrap method does some typecasting to allow these curried functions to operate on plain Controllers, which is necessary for the Action class to use it.

Because we want to support this concise syntax, the types for action bodies and filters are a little odd. You shouldn't have to set action bodies and filters to literal values that often, though. You should generally use instance methods for your actions, and use the syntax above.

Authentication

Tailor provides support for authenticating users with an email address and password out of the box. This authentication is built around the User model. This model is backed by a a table called users, which must have id, email_address, and encrypted_password fields. You can extend this model and this table with whatever other custom fields and behavior you want for your app's user modeling.

You can create a new user like you would any other record, but there is also a simple initializer that takes an email address and an unencrypted password, and it will encrypt the password with Bcrypt and set it on the record. The rest of the authentication helpers assume the passwords are encrypted with Bcrypt. The hasPassword method determines whether a user has the password provided, and the authenticate class method looks for a user with a combination of email address and password.

Tailor also provides helpers in the controller for using this authentication system. The signIn method signs a user in, by putting their user ID in the session. It also has a variant that takes an email address and password and tries to sign the user in, returning a boolean value indicating whether the credentials were successful. There is also a currentUser field on controllers, which tries to fetch a user based on the user ID in the session. Finally, there is a signOut method, which remove the user information from the session.

Example Controller

/**
  This controller provides a page for managing the hats in the inventory.

  This assumes that we also have the templates that are defined in the
  template guides, but you can ignore the details of the template rendering
  for now.
  */
class HatsController : Controller {
  override class func actions() -> Action {
    return [
      Action(name: "index", body: wrap(indexAction)),
      Action(name: "new", body: wrap(formAction)),
      Action(name: "create", body: wrap(formSubmissionAction)),
      Action(name: "show", body: wrap(showAction), filters: [wrap(checkHat)]),
      Action(name: "edit", body: wrap(formAction), filters: [wrap(checkHat)]),
      Action(name: "update", body: wrap(formSubmissionAction), filters: [wrap(checkHat)])
    ]
  }

  /**
    This method gets a hat from the request parameters.
    */
  func hat() -> Hat! {
    if let id = request.requestParameters["id"]?.toInt {
      if let hat = Query<Hat>find(id) {
        return hat
      }
      else {
        return nil
      }
    }
    else {
      return Hat()
    }
  }

  /**
    This method checks whether we have a valid hat specified in the request
    parameters.
    */
  func checkHat() -> Bool {
    if hat() {
      return true
    }
    else {
      redirectTo(action: "index")
      return false
    }
  }

  func indexAction() {
    respondWith(IndexTemplate(controller: self, hats: Query<Hat>().all()))
  }

  func showAction() {
    respondWith(ShowTemplate(controller: self, hat: hat())
  }

  func formAction() {
    respondWith(FormTemplate(controller: self, hat: hat()))
  }

  func formSubmissionAction() {
    let hat = Hat()
    hat.brimSize = request.requestParameters["hat[brimSize]"]
    hat.color = request.requestParameters["hat[color]"]

    if hat.save() {
      session.setFlash("sucess", "Hat saved")
      redirectTo(action: "index")
    }
    else {
      respondWith(FormTemplate(controller: self, hat: hat))
    }
  }
}

Testing

When you're testing controllers, it can be tedious to manually create request objects for all the types of requests you want to simulate. To help out with this, the Controller class has a class method called callAction which will build a request, build a controller with it, call its action, and give the controller and the repsonse to a callback. There are several methods in this family; all of them take an action and a callback, and the variants take different combinations of request parameters, a hardcoded request, and a user.

Here's an example of how you might use this in a test:

let params = /* Valid request parameters */
HatsController.callAction("create", parameters: params) {
  response,controller in
  XCTAssertEqual(response.headers["Location"], controller.urlFor(actionName: "index"), "redirects to the index page")
}

Defining Routes

The RouteSet class collects the routes for an application. A route is a description of how to handle a kind of request, based on the request's path and the HTTP method. The route matches the request with a handler, which is just a block that takes a request and provides a response. An application starts out with an empty route set. You can provide the routes in the application's initializer, either by calling methods to add routes or by replacing the route set entirely with the result of another method.

The lowest-level method for adding a route is addRoute, which takes a regular expression to match the path against, an HTTP method the request must have, a description of the route for debugging, and a block for handling the request. The description is optional. The regular expression must match the entire path for the route to be used. You can also create nested sections of routes that have a common part of their path., using the withPrefix method. For instance, if you had a few routes that should all start with "admin", you could use the method like this: withPrefix("admin") { /* route details */ }.

You will often want to handle a request by creating a controller to process it, so there is another version of addRoute to create these kind of routes. This version takes a path pattern, an HTTP method, a controller class, and a controller action. You can also set a controller to apply to an entire prefix block, so that you can omit the controller from the individual routes.

You can also have certain sections of a path correspond to request parameters, by having that section start with a colon. It will capture the entire section between two forward slashes. For instance, if the pattern for the route is "hats/:id/edit", it will match a path of "hats/25/edit", and the request will have the id request parameter set to 25. You can also capture request parameters for entire blocks of routes. For instance, if you have a block like withPrefix(":locale") { /* Routes */ }, the locale section parameter will be set on the requests for any of the routes within that block.

These techniques can be combined to add a set of routes for restful actions, using the addRestfulRoutes method. It will add routes mapping the "index" action to a bare path as a GET request, mapping "show" to ":id" as a GET request, mapping "new" to "new" as a GET request, mapping "create" to a bare path as a POST request, mapping "edit" to ":id/edit" as a GET request, and mapping "create" to ":id" as a POST request. You will generally want to use this within a prefix block that maps the routes to a controller. This method also has optional only and except parameters for including or excluding some of the actions, like the parameters for defining filters on controllers.

There is also a simple way to have the route set serve up static assets from your application bundle. The staticAssets method takes a route prefix, a prefix for the path on disk relative to the application root, and a list of filenames.

Example Routes

let routeSet = RouteSet()

routeSet.withPrefix("hats", controller: HatsController.self) {
  /*
    Adds these routes:

    GET /hats          | HatsController#index
    GET /hats/:id      | HatsController#show
    GET /hats/new      | HatsController#new
    POST /hats         | HatsController#create
    GET /hats/:id/edit | HatsController#edit
    POST /hats/:id     | HatsController#update
    */
  routeSet.addRestfulRoutes()

  /** Adds this route: GET /hats/slideshow   | HatsController#slideshow */
  routeSet.addRoute("slideshow", method: "GET", action: "slideshow")
}

routeSet.addRoute("/login", method: "GET", controller: SessionsController.self, actionName: "new")
routeSet.addRoute("/login", method: "POST", controller: SessionsController.self, actionName: "create")