Skip to content

Commit

Permalink
added accepts helper to Mojolicious::Plugin::DefaultHelpers
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Jan 29, 2014
1 parent b26a795 commit 5562abb
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 16 deletions.
2 changes: 2 additions & 0 deletions Changes
@@ -1,6 +1,8 @@

4.72 2014-01-29
- Added template_for and template_handler methods to Mojolicious::Renderer.
- Added accepts method to Mojolicious::Types.
- Added accepts helper to Mojolicious::Plugin::DefaultHelpers.
- Added before_render hook.
- Fixed bug in Mojo::Transaction::WebSocket that prevented decompression
errors from being handled gracefully.
Expand Down
18 changes: 5 additions & 13 deletions lib/Mojolicious/Controller.pm
Expand Up @@ -266,28 +266,20 @@ sub respond_to {
my $self = shift;
my $args = ref $_[0] ? $_[0] : {@_};

# Detect formats
my $app = $self->app;
my $req = $self->req;
my @formats = @{$app->types->detect($req->headers->accept, $req->is_xhr)};
my $stash = $self->stash;
unless (@formats) {
my $format = $stash->{format} || $req->param('format');
push @formats, $format ? $format : $app->renderer->default_format;
}

# Find target
my $target;
for my $format (@formats) {
my $app = $self->app;
my @formats = @{$app->types->accepts($self)};
for my $format (@formats ? @formats : ($app->renderer->default_format)) {
next unless $target = $args->{$format};
$stash->{format} = $format;
$self->stash->{format} = $format;
last;
}

# Fallback
unless ($target) {
return $self->rendered(204) unless $target = $args->{any};
delete $stash->{format};
delete $self->stash->{format};
}

# Dispatch
Expand Down
14 changes: 14 additions & 0 deletions lib/Mojolicious/Guides/Rendering.pod
Expand Up @@ -352,6 +352,20 @@ single render call.
And if no viable representation could be found, the C<any> fallback will be
used or an empty C<204> response rendered automatically.

# /hello -> "html"
# /hello (Accept: text/html) -> "html"
# /hello (Accept: text/xml) -> "xml"
# /hello.html -> "html"
# /hello.xml -> "xml"
# /hello?format=html -> "html"
# /hello?format=xml -> "xml"
if (my $format = $self->accepts('html', 'xml')) {
...
}

For even more advanced negotiation logic you can also use the helper
L<Mojolicious::DefaultHelpers/"accepts"> directly.

=head2 Rendering C<exception> and C<not_found> pages

By now you've probably already encountered the built-in 404 (Not Found) and
Expand Down
25 changes: 23 additions & 2 deletions lib/Mojolicious/Plugin/DefaultHelpers.pm
Expand Up @@ -26,8 +26,9 @@ sub register {
);
}

$app->helper(b => sub { shift; Mojo::ByteStream->new(@_) });
$app->helper(c => sub { shift; Mojo::Collection->new(@_) });
$app->helper(accepts => \&_accepts);
$app->helper(b => sub { shift; Mojo::ByteStream->new(@_) });
$app->helper(c => sub { shift; Mojo::Collection->new(@_) });
$app->helper(config => sub { shift->app->config(@_) });
$app->helper(content => \&_content);
$app->helper(content_for => \&_content_for);
Expand All @@ -39,6 +40,11 @@ sub register {
$app->helper(url_with => \&_url_with);
}

sub _accepts {
my $self = shift;
return $self->app->types->accepts($self, @_);
}

sub _content {
my ($self, $name, $content) = @_;
$name ||= 'content';
Expand Down Expand Up @@ -120,6 +126,21 @@ example for learning to build new plugins, you're welcome to fork it.
L<Mojolicious::Plugin::DefaultHelpers> implements the following helpers.
=head2 accepts
%= accepts->[0] // 'html'
%= accepts('html', 'json', 'txt')
Select best possible representation for resource from C<Accept> request
header, C<format> stash value or C<format> GET/POST parameter with
L<Mojolicious::Types/"accepts">, defaults to returning the first extension if
no preference could be detected. Since browsers often don't really know what
they actually want, unspecific C<Accept> request headers with more than one
MIME type will be ignored, unless the C<X-Requested-With> header is set to the
value C<XMLHttpRequest>.
return $self->render(json => {hello => 'world'}) if $self->accepts('json');
=head2 app
%= app->secrets->[0]
Expand Down
33 changes: 33 additions & 0 deletions lib/Mojolicious/Types.pm
@@ -1,6 +1,8 @@
package Mojolicious::Types;
use Mojo::Base -base;

use List::Util 'first';

has types => sub {
{
appcache => ['text/cache-manifest'],
Expand Down Expand Up @@ -32,6 +34,25 @@ has types => sub {
};
};

sub accepts {
my ($self, $c) = (shift, shift);

# List representations
my $req = $c->req;
my @exts = @{$self->detect($req->headers->accept, $req->is_xhr)};
if (!@exts && (my $format = $c->stash->{format} || $req->param('format'))) {
push @exts, $format;
}
return \@exts unless @_;

# Find best representation
for my $ext (@exts) {
next unless my $first = first { $ext eq $_ } @_;
return $first;
}
return shift;
}

sub detect {
my ($self, $accept, $prioritize) = @_;

Expand Down Expand Up @@ -125,6 +146,18 @@ List of MIME types.
L<Mojolicious::Types> inherits all methods from L<Mojo::Base> and implements
the following new ones.
=head2 accepts
my $all = $types->accepts(Mojolicious::Controller->new);
my $best = $types->accepts(Mojolicious::Controller->new, 'html', 'json');
Select best possible representation for resource from C<Accept> request
header, C<format> stash value or C<format> GET/POST parameter, defaults to
returning the first extension if no preference could be detected.. Since
browsers often don't really know what they actually want, unspecific C<Accept>
request headers with more than one MIME type will be ignored, unless the
C<X-Requested-With> header is set to the value C<XMLHttpRequest>.
=head2 detect
my $exts = $types->detect('application/json;q=9');
Expand Down
30 changes: 29 additions & 1 deletion t/mojolicious/restful_lite_app.t
Expand Up @@ -14,6 +14,11 @@ any [qw(POST PUT)] => '/json/echo' => sub {
$self->respond_to(json => {json => $self->req->json});
};

get '/accepts' => sub {
my $self = shift;
$self->render(json => {best => $self->accepts('html', 'json', 'txt')});
};

under '/rest';

get sub {
Expand Down Expand Up @@ -59,6 +64,29 @@ $tx = $t->ua->build_tx(
$t->request_ok($tx)->status_is(200)->content_type_is('application/json')
->json_is([1, 2, 3]);

# Nothing
$t->get_ok('/accepts')->status_is(200)->json_is({best => 'html'});

# "json" format
$t->get_ok('/accepts.json')->status_is(200)->json_is({best => 'json'});

# "txt" query
$t->get_ok('/accepts?format=txt')->status_is(200)->json_is({best => 'txt'});

# Accept "txt"
$t->get_ok('/accepts' => {Accept => 'text/plain'})->status_is(200)
->json_is({best => 'txt'});

# Accept "txt" with everything
$t->get_ok('/accepts.html?format=json' => {Accept => 'text/plain'})
->status_is(200)->json_is({best => 'txt'});

# Ajax
my $ajax = 'text/html;q=0.1,application/json';
$t->get_ok(
'/accepts' => {Accept => $ajax, 'X-Requested-With' => 'XMLHttpRequest'})
->status_is(200)->json_is({best => 'json'});

# Nothing
$t->get_ok('/rest')->status_is(200)
->content_type_is('text/html;charset=UTF-8')
Expand Down Expand Up @@ -421,7 +449,7 @@ $t->post_ok('/rest.png?format=json')->status_is(201)
$t->get_ok('/nothing' => {Accept => 'image/png'})->status_is(404);

# Ajax
my $ajax = 'text/html;q=0.1,application/xml';
$ajax = 'text/html;q=0.1,application/xml';
$t->get_ok(
'/rest' => {Accept => $ajax, 'X-Requested-With' => 'XMLHttpRequest'})
->status_is(200)->content_type_is('application/xml')
Expand Down

0 comments on commit 5562abb

Please sign in to comment.