Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
allow _method query parameter to override the request method
  • Loading branch information
kraih committed Mar 14, 2015
1 parent 0dbb032 commit 1febff4
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 43 deletions.
5 changes: 4 additions & 1 deletion Changes
@@ -1,5 +1,8 @@

6.03 2015-03-13
6.03 2015-03-14
- Added support for overriding the HTTP request method with the _method query
parameter.
- Added suggested_method method to Mojolicious::Routes::Route.
- Improved portability of content negotiation tests.

6.02 2015-03-09
Expand Down
53 changes: 31 additions & 22 deletions lib/Mojolicious/Guides/Routing.pod
Expand Up @@ -236,6 +236,10 @@ but content will not be sent with the response even if it is present.
# HEAD /test -> {controller => 'bar', action => 'test'}
$r->get('/test')->to(controller => 'bar', action => 'test');

You can also use the C<_method> query parameter to override the request method,
this can be very useful when submitting forms with browsers that only support
C<GET> and C<POST>.

=head2 IRIs

IRIs are handled transparently, that means paths are guaranteed to be unescaped
Expand Down Expand Up @@ -706,13 +710,6 @@ Post-processing the response to add or remove headers is a very common use.

Same for pre-processing the request.

# Allow "_method" query parameter to override request method
$app->hook(before_dispatch => sub {
my $c = shift;
return unless my $method = $c->req->url->query->param('_method');
$c->req->method($method);
});

# Choose template variant based on request headers
$app->hook(before_dispatch => sub {
my $c = shift;
Expand Down Expand Up @@ -771,29 +768,41 @@ to make route generation more expressive.
$r->add_shortcut(resource => sub {
my ($r, $name) = @_;

# Generate "/$name" route
# Prefix for resource
my $resource = $r->any("/$name")->to("$name#");

# Handle POST requests
$resource->post->to('#create')->name("create_$name");
# Render a list of resources
$resource->get->to('#index')->name($name);

# Handle GET requests
$resource->get->to('#show')->name("show_$name");
# Render a form to create a new resource (submitted to "store")
$resource->get('/create')->to('#create')->name("create_$name");

# Handle OPTIONS requests
$resource->options(sub {
my $c = shift;
$c->res->headers->allow('POST, GET, OPTIONS');
$c->render(data => '', status => 204);
});
# Store newly create resource (submitted by "create")
$resource->post->to('#store')->name("store_$name");

# Render a specific resource
$resource->get('/:id')->to('#show')->name("show_$name");

# Render a form to edit a resource (submitted to "update")
$resource->get('/:id/edit')->to('#edit')->name("edit_$name");

# Store updated resource (submitted by "edit")
$resource->put('/:id')->to('#update')->name("update_$name");

# Remove a resource
$resource->delete('/:id')->to('#remove')->name("remove_$name");

return $resource;
});

# POST /user -> {controller => 'user', action => 'create'}
# GET /user -> {controller => 'user', action => 'show'}
# OPTIONS /user -> {cb => sub {...}}
$r->resource('user');
# GET /users -> {controller => 'users', action => 'index'}
# GET /users/create -> {controller => 'users', action => 'create'}
# POST /users -> {controller => 'users', action => 'store'}
# GET /users/23 -> {controller => 'users', action => 'show', id => 23}
# GET /users/23/edit -> {controller => 'users', action => 'edit', id => 23}
# PUT /users/23 -> {controller => 'users', action => 'update', id => 23}
# DELETE /users/23 -> {controller => 'users', action => 'remove', id => 23}
$r->resource('users');

=head2 Rearranging routes

Expand Down
20 changes: 9 additions & 11 deletions lib/Mojolicious/Plugin/TagHelpers.pm
Expand Up @@ -52,18 +52,15 @@ sub _form_for {
my ($c, @url) = (shift, shift);
push @url, shift if ref $_[0] eq 'HASH';

# POST detection
my @post;
# Method detection
my (@post, $method);
if (my $r = $c->app->routes->lookup($url[0])) {
my %methods = (GET => 1, POST => 1);
do {
my @via = @{$r->via || []};
%methods = map { $_ => 1 } grep { $methods{$_} } @via if @via;
} while $r = $r->parent;
@post = (method => 'POST') if $methods{POST} && !$methods{GET};
@post = (method => 'POST') if ($method = $r->suggested_method) ne 'GET';
}

return _tag('form', action => $c->url_for(@url), @post, @_);
my $url = $c->url_for(@url);
$url->query({_method => $method}) if @post && $method ne 'POST';
return _tag('form', action => $url, @post, @_);
}

sub _hidden_field {
Expand Down Expand Up @@ -366,8 +363,9 @@ Generate C<input> tag of type C<file>.
% end
Generate portable C<form> tag with L<Mojolicious::Controller/"url_for">. For
routes that allow C<POST> but not C<GET>, a C<method> attribute will be
automatically added.
routes that do not allow C<GET>, a C<method> attribute with the value C<POST>
will be automatically added. And for methods other than C<GET> or C<POST>, a
C<_method> query parameter will be added as well.
<form action="/path/to/login">
<input name="first_name" type="text">
Expand Down
2 changes: 1 addition & 1 deletion lib/Mojolicious/Routes.pm
Expand Up @@ -71,7 +71,7 @@ sub match {
else { $path = $req->url->path->to_route }

# Method (HEAD will be treated as GET)
my $method = uc $req->method;
my $method = uc($req->url->query->clone->param('_method') || $req->method);
$method = 'GET' if $method eq 'HEAD';

# Check cache
Expand Down
20 changes: 20 additions & 0 deletions lib/Mojolicious/Routes/Route.pm
Expand Up @@ -128,6 +128,19 @@ sub route {
return $route;
}

sub suggested_method {
my $self = shift;

my %via;
for my $route (@{$self->_chain}) {
next unless my @via = @{$route->via || []};
%via = map { $_ => 1 } keys %via ? grep { $via{$_} } @via : @via;
}

return 'POST' if $via{POST} && !$via{GET};
return $via{GET} ? 'GET' : (sort keys %via)[0] || 'GET';
}

sub to {
my $self = shift;

Expand Down Expand Up @@ -492,6 +505,13 @@ The L<Mojolicious::Routes> object this route is a descendant of.
Low-level generator for routes matching all HTTP request methods, returns a
L<Mojolicious::Routes::Route> object.
=head2 suggested_method
my $method = $r->suggested_method;
Suggested HTTP method for reaching this route, C<GET> and C<POST> are
preferred.
=head2 to
my $defaults = $r->to;
Expand Down
2 changes: 2 additions & 0 deletions t/mojolicious/lite_app.t
Expand Up @@ -506,6 +506,8 @@ $t->get_ok('/', '1234' x 1024)->status_is(200)
# DELETE request
$t->delete_ok('/')->status_is(200)->header_is(Server => 'Mojolicious (Perl)')
->content_is('Hello!');
$t->post_ok('/?_method=DELETE')->status_is(200)
->header_is(Server => 'Mojolicious (Perl)')->content_is('Hello!');

# POST request
$t->post_ok('/')->status_is(200)->header_is(Server => 'Mojolicious (Perl)')
Expand Down
12 changes: 8 additions & 4 deletions t/mojolicious/routes.t
Expand Up @@ -211,11 +211,11 @@ $r->route('/missing/too/*', '' => ['test'])
# /partial/*
$r->route('/partial')->detour('foo#bar');

# GET /similar/*
# POST /similar/too
# GET /similar/*
# PATCH /similar/too
my $similar = $r->route('/similar')->inline(1);
$similar->route('/:something')->via('GET')->to('similar#get');
$similar->route('/too')->via('POST')->to('similar#post');
$similar->route('/too')->via('PATCH')->to('similar#post');

# Cached lookup
my $fast = $r->route('/fast');
Expand Down Expand Up @@ -676,11 +676,13 @@ is_deeply $m->stack,
[{controller => 'method', action => 'get', format => 'html'}],
'right structure';
is $m->path_for->{path}, '/method/get', 'right path';
is $m->endpoint->suggested_method, 'GET', 'right method';
$m = Mojolicious::Routes::Match->new(root => $r);
$m->find($c => {method => 'POST', path => '/method/post'});
is_deeply $m->stack, [{controller => 'method', action => 'post'}],
'right structure';
is $m->path_for->{path}, '/method/post', 'right path';
is $m->endpoint->suggested_method, 'POST', 'right method';
$m = Mojolicious::Routes::Match->new(root => $r);
$m->find($c => {method => 'GET', path => '/method/post_get'});
is_deeply $m->stack, [{controller => 'method', action => 'post_get'}],
Expand All @@ -691,6 +693,7 @@ $m->find($c => {method => 'POST', path => '/method/post_get'});
is_deeply $m->stack, [{controller => 'method', action => 'post_get'}],
'right structure';
is $m->path_for->{path}, '/method/post_get', 'right path';
is $m->endpoint->suggested_method, 'GET', 'right method';
$m = Mojolicious::Routes::Match->new(root => $r);
$m->find($c => {method => 'DELETE', path => '/method/post_get'});
is_deeply $m->stack, [], 'empty stack';
Expand Down Expand Up @@ -934,8 +937,9 @@ is_deeply $m->stack,
[{}, {controller => 'similar', action => 'get', 'something' => 'too'}],
'right structure';
$m = Mojolicious::Routes::Match->new(root => $r);
$m->find($c => {method => 'POST', path => '/similar/too'});
$m->find($c => {method => 'PATCH', path => '/similar/too'});
is_deeply $m->stack, [{}, {controller => 'similar', action => 'post'}],
'right structure';
is $m->endpoint->suggested_method, 'PATCH', 'right method';

done_testing();
8 changes: 4 additions & 4 deletions t/mojolicious/tag_helper_lite_app.t
Expand Up @@ -313,7 +313,7 @@ EOF

# Empty selection
$t->put_ok('/selection')->status_is(200)
->content_is("<form action=\"/selection\">\n "
->content_is("<form action=\"/selection?_method=PUT\" method=\"POST\">\n "
. '<select name="a">'
. '<option value="b">b</option>'
. '<optgroup label="c">'
Expand Down Expand Up @@ -342,7 +342,7 @@ $t->put_ok('/selection')->status_is(200)

# Selection with values
$t->put_ok('/selection?a=e&foo=bar&bar=baz&yada=b')->status_is(200)
->content_is("<form action=\"/selection\">\n "
->content_is("<form action=\"/selection?_method=PUT\" method=\"POST\">\n "
. '<select name="a">'
. '<option value="b">b</option>'
. '<optgroup label="c">'
Expand Down Expand Up @@ -372,7 +372,7 @@ $t->put_ok('/selection?a=e&foo=bar&bar=baz&yada=b')->status_is(200)
# Selection with multiple values
$t->put_ok('/selection?foo=bar&a=e&foo=baz&bar=d&yada=a&yada=b')
->status_is(200)
->content_is("<form action=\"/selection\">\n "
->content_is("<form action=\"/selection?_method=PUT\" method=\"POST\">\n "
. '<select name="a">'
. '<option value="b">b</option>'
. '<optgroup label="c">'
Expand Down Expand Up @@ -401,7 +401,7 @@ $t->put_ok('/selection?foo=bar&a=e&foo=baz&bar=d&yada=a&yada=b')

# Selection with multiple values preselected
$t->put_ok('/selection?preselect=1')->status_is(200)
->content_is("<form action=\"/selection\">\n "
->content_is("<form action=\"/selection?_method=PUT\" method=\"POST\">\n "
. '<select name="a">'
. '<option selected value="b">b</option>'
. '<optgroup label="c">'
Expand Down

0 comments on commit 1febff4

Please sign in to comment.