Middleware

This guide gives an overview of the different middleware used by Utopia.

Static

The module Utopia::Static middleware services static files efficiently and supports ETag based caching. Normally, you'd prefer to put static files into public/_static but it's also acceptable to put static content into pages/ if it makes sense.

use Utopia::Static,
	# The root path to serve files from:
	root: "path/to/root",
	# The file extension groups to recognize/serve:
	types: [:default, :xiph],
	# Cache-Control header for files:
	cache_control: 'public, max-age=7200'

Redirection

The module Utopia::Redirection middleware is used for redirecting requests based on patterns and status codes.

# String (fast hash lookup) rewriting:
use Utopia::Redirection::Rewrite,
	'/' => '/welcome/index'

# Redirect directories (e.g. /) to an index file (e.g. /index):
use Utopia::Redirection::DirectoryIndex,
	index: 'index.html'

Place class Utopia::Redirection::Errors after all client-visible redirection middleware. It maps unhandled error responses to internal error documents while retaining the original response status. Because its internal requests invoke the downstream application directly, redirects configured before it are bypassed:

use Utopia::Redirection::Errors,
	404 => '/errors/file-not-found'

Localization

The module Utopia::Localization middleware computes immutable localization preferences from the request path, host, and accept-language header. Localization-aware resource middleware, including module Utopia::Static and module Utopia::Content, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.

use Utopia::Localization,
	:default_locale => 'en',
	:locales => ['en', 'de', 'ja', 'zh']

To localize a specific xnode, append the locale as a postfix:

pages/index.xnode
pages/index.de.xnode
pages/index.ja.xnode
pages/index.zh.xnode

You can access the selected locale in a view using localization.locale. Controllers can inspect the request preferences using request.localization.

Controller

The module Utopia::Controller middleware provides flexible nested controllers with efficient behaviour. Controllers are nested in the pages directory and are matched against the incoming request path recursively, from outer most to inner most.

use Utopia::Controller,
	# The root directory where `controller.rb` files can be found.
	root: "path/to/root",
	# The base class to use for all controllers:
	base: Utopia::Controller::Base

A controller is a file within the specified root directory (typically pages) with the name controller.rb. This code is dynamically loaded into an anonymous class and executed. The default controller has only a single function:

def passthrough(request, path)
	# Call one of:
	
	# Respond immediately with a complete Protocol::HTTP::Response.
	# def respond!(response)
	
	# This will cause the controller to skip the request.
	# def ignore!
	
	# Request relative redirect. Respond with a redirect to the given target.
	# def redirect! (target, status = 302)
	
	# Controller relative redirect.
	# def goto!(target, status = 302)
	
	# Respond with an error which indicates some kind of failure.
	# def fail!(error = 400, message = nil)
	
	# Succeed with a semantic value which the Respond layer serializes.
	# def succeed!(value = nil, status: 200, headers: {})
	
	succeed!
end

Controllers which return semantic values should prepend module Utopia::Controller::Respond and configure serializers. Serializer blocks return wire-ready response bodies, so they can return streaming Protocol::HTTP::Body::Readable objects without buffering them.

prepend Utopia::Controller::Respond, Utopia::Controller::Actions

responds.with("application/json") do |media_range, value|
	JSON.dump(value)
end

on "show" do
	succeed!({"name" => "Samuel"})
end

Use respond! when a controller has already constructed a complete response and no content negotiation is required:

respond! Utopia::Response[200, {"content-type" => "text/plain"}, ["Hello World"]]

The controller layer can do more complex operations by prepending modules into it.

prepend Rewrite, Actions

# Extracts an Integer
rewrite.extract_prefix id: Integer do
	@user = User.find_by_id(@id)
end

on "edit" do |request, path|
	if request.post?
		@user.update_attributes(parse_body(request)["user"])
	end
end

otherwise do |request, path|
	# Executed if no specific named actions were executed.
	succeed!
end

The incoming path is relative to the path of the controller itself.

Content

The module Utopia::Content middleware parses XML-style templates with using attributes provided by the controller layer. Dynamic tags can be used to build modular content.

use Utopia::Content

A basic template create.xnode looks something like:

<content:page>
	<content:heading>Create User</content:heading>
	<form action="#">
		<input name="name" />
		<input type="submit" />
	</form>
</content:page>

This template would typically be designed with supporting _page.xnode and _heading.xnode in the same directory or, more typically, somewhere further up the directory hierarchy.

Session

The module Utopia::Session middleware provides session storage using encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and avoid tampering.

use Utopia::Session,
	expires_after: 3600 * 24,
	# The private key is retried from the `environment.yaml` file:
	secret: UTOPIA.secret_for(:session),
	secure: true

All session data is stored on the client, but it's encrypted with a salt and the secret key. It is impossible for the client to decrypt the data without the secret stored on the server.

When the middleware is installed, the session is available on the request:

request.session[:user_id] = user.id