Controllers and Routing

Controllers

Requests in Tailor are generally handled by Controllers. A controller is a type that conforms to the ControllerType protocol. 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 type and action the request is for, and creates a controller to handle it. A controller is created with a Request, an initial Response, a string with the action name, and a callback that it should call with the final Response object. Once it is created, the action is run. Actions are described in more detail below.

The ControllerType protocol has multiple data points it needs for its helper methods, but to make it easier to create new controllers, this is all wrapped up in the ControllerState type. Your controllers must have a field called state of this type, and it must have an initializer that takes in just a ControllerState. It must also provide a static method called defineRoutes, which adds all the actions for the controller into the application's route set.

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, session information, 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 build a Response structure. There are four parts to a response: 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 responses by hand, though. The ControllerType protocol provides 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 type 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 out 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 there are two kinds of responses that Tailor provides more specific helpers for. The first is rendering a template. You can call the respondWith method, giving it a Template instance, and it will render the template into a response. You don't need to build the response, 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 type, 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.

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

If you are going to make a completely custom response, there are still some helpers you can make use of. The controller will be initialized with an initial response, which may have details filled in by filters. Filters are explained more below. You can get that response from the controller's state field. Once your response is ready, you can send it out by calling the respondWith method and giving it the response. This will set some default fields on the response and set the session information before giving the response to the system's response handling callback.

Actions and Filters

An action is an instance method on a controller that handles a request. It does not have any input or output parameters; the request will be provided during controller initialization, and the response will be given to a callback that is provided during initialization. Every controller action must create a response for every request. If you do not provide a response, the client will be left waiting for a response until it gives up and closes the connection. Aside from being rude, this will waste resources on both ends by leaving the connection open.

You can use filters alongside your controller actions to respond to requests. Filters conform to the RequestFilter protocol. There are two parts to a request filter: the pre-processing and the post-processing. The pre-processing is run before the controller action, and can modify either the request or the initial response before they reach the controller. Pre-processors can also provide a complete response and halt processing without the controller being involved at all. Post-processors receive the response from the controller, and can make changes to the response before it reaches the client. You can add one or more filters when you define your routes, as explained below. The pre-processors for the filters are run in the order that they are added, and the post-processors are run in the reverse order.

There is one filter that is added by default to all requests: CsrfFilter. This filter puts a special value called csrfToken in the session, and it requires that this value be provided in both the session and the request parameters for every POST request. The field will be added to any forms you create with the built-in form building tools. This filter protects against forms being submitted to your site from other domains, which could be used to execute a cross-site request forgery attack. If you have pages where you want to remove this prevention, you can do so by adding those routes a call to withoutFilter on the route set.

Authentication

Tailor provides support for authenticating users with an email address and password out of the box. This authentication is built around the UserType protocol. This protocol describes a record that is persisted to the database and has fields for emailAddress and encryptedPassword.

This protocol also provides a few helper methods for handling authentication. You can set a value in the password field on the user, and the system will take a cryptographic hash of the password and store it in the encryptedPassword field. The password will be hashed with SHA-512. The hasPassword method determines whether a user has the password provided, using the same hash logic. The authenticate static method looks for a user with a combination of email address and password. It will throw an exception if the authentication fails, so that it can communicate exactly why the authentication failed.

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 signOut method, which remove the user information from the session. Because the controller is stateless, all of these methods are stateless, and return a new session information with the user information changed. You can give that new session to redirectTo or respondWith to include it with the response. There is also a currentUser field on controllers, which tries to fetch a user based on the user ID in the session.

In order for these methods to work, the application needs to know what type implements the UserType protocol in your system. This is specified by the userType field on the application's configuration. You can see the configuration guide for more information on configuring your app.

If you want to require that the user be logged in to access a page, you can use the AuthenticationFilter filter, which checks the session for the logged in user before the controller action is called. If it does not have a valid user, it will redirect to the sign-in page that you specify when adding the filter.

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. The path and HTTP method are combined into a single field, using the RoutePath enum. To create a route for handling a GET request to the path /foo/ba[rz], you would specify it as .Get("/foo/ba[rz]").

You can also create nested sections of routes that have a common part of their path., using the withScope method. For instance, if you had a few routes that should all start with "admin", you could use the method like this: withScope(path: "admin") { /* route details */ }. The withScope method also allows you to add filters to the routes. Within the block you give to withScope, you should add more routes, and all the routes you add there will have the scope you provided.

You will often want to handle a request by creating a controller to process it, so there is a special route method to create these kind of routes. This version takes a path pattern, an HTTP method, a controller action, and an action name. The controller action is provided in the form of a function that takes in a controller and responds with a new function, which takes in no parameters and returns no parameters. This type signature may seem odd, but it makes the syntax very concise, as shown below. You will generally want to use this to add your controller routes inside the static defineRoutes method. The defineRoutes method runs on your controller type, so it can access all of your instance methods as curried functions with the method signature for the actions in the route method.

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 withScope(path: ":locale") { /* Routes */ }, the locale section parameter will be set on the requests for any of the routes within that block.

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 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 : ControllerType {
  let state: ControllerState

  static func defineRoutes(routes: RouteSet) {
    routes.withScope(path: "hats") {
      routes.route(.Get(""), to: indexAction, name: "index")
      routes.route(.Get("new"), to: newAction, name: "new")
      routes.route(.Post(""), to: createAction, name: "create")
      routes.withScope(path: ":id") {
        routes.route(.Get(""), to: showAction, name: "show")
        routes.route(.Get("edit"), to: editAction, name: "edit")
        routes.route(.Post(""), to: createAction, name: "update")
      }
    }
  }
  /**
    This method gets a hat from the request parameters.
    */
  var hat: Hat? {
    if let id = request.requestParameters["id"]?.toInt {
      if let hat = Query<Hat>find(id) {
        return hat
      }
      else {
        return nil
      }
    }
    else {
      return Hat()
    }
  }

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

  func showAction() {
    guard let hat = self.hat else { render404(); return }
    respondWith(ShowTemplate(controller: self, hat: hat)
  }

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

  func createAction() {
    self.createOrUpdateHat()
  }

  func editAction() {
    guard let hat = self.hat else { render404(); return }
    respondWith(FormTemplate(controller: self, hat: hat))
  }

  func updateAction() {
    createOrUpdateAction()
  }

  func createOrUpdateHat() {
    guard var hat = hat() else { render404(); return }
    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))
    }
  }
}