Skip to content

Content negotiation and API mode

MIME types

MVC::Keayl::Mime is the format registry. mime-type maps a format to its MIME type and mime-format maps a type (ignoring parameters and aliases) back to a format. register-mime adds or extends entries:

1
2
3
mime-type('json');                       # application/json
mime-format('application/json; q=1');     # json
register-mime('msgpack', 'application/msgpack');

parse-accept orders an Accept header's types by quality, and negotiate chooses the best of a list of available formats, honouring */* and type/* wildcards and falling back to the first available format when there is no preference:

1
negotiate(['html', 'json'], 'application/json;q=0.4, text/html;q=0.8');   # html

respond-to

A controller action serves several formats with respond-to, taking an ordered list of format-to-block pairs. The format is taken from the path extension if present, otherwise negotiated from the Accept header; the first declared format is the default. A request that matches none gets 406:

1
2
3
4
5
6
method show {
  self.respond-to([
    html => { self.render('show') },
    json => { self.render(:json($post.to-hash)) },
  ]);
}

/posts.json forces JSON; /posts with Accept: text/html serves HTML.

Variants

A variant selects a device-specific view of the same format. request.variant holds the variant, set explicitly or from a user-agent heuristic:

1
2
self.request.set-variant('phone');
self.request.set-variant(self.request.detect-variant);  # phone, tablet, or none

A format block in respond-to may be a map of variant names to blocks instead of a single block. The block for the current variant runs, falling back to any:

1
2
3
4
5
6
self.respond-to([
  html => {
    phone => { self.render('show_phone') },
    any   => { self.render('show') },
  },
]);

Variants also steer template lookup. When the variant is set, the renderer prefers a variant template before the plain one, so phone resolves show.html+phone.haml and falls back to show.html.haml.

API controllers

MVC::Keayl::APIController is a JSON-first controller. It needs no view renderer: rendering a bare object or a collection produces JSON, while string templates and explicit render options still work as on a normal controller.

1
2
3
4
5
class PostsController is MVC::Keayl::APIController {
  method show  { self.render($post) }            # {"title":"..."}
  method index { self.render($posts) }           # [{"title":"..."}, ...]
  method made  { self.render($post, :status(201)) }
}

A model is serialized through serialize, which calls the object's to-hash (recursing into collections) by default. Provide a serializer to control the shape:

1
PostsController.new(serializer => -> $model { %( id => $model.id, title => $model.title ) });