use actionrole instead of core for the new http method support
John Napiorkowski [Fri, 15 Feb 2013 19:29:22 +0000 (14:29 -0500)]
Changes
lib/Catalyst/Action.pm
lib/Catalyst/ActionRole/HTTPMethods.pm [new file with mode: 0644]
lib/Catalyst/Controller.pm

diff --git a/Changes b/Changes
index a1eb91b..c885343 100644 (file)
--- a/Changes
+++ b/Changes
@@ -7,7 +7,10 @@
     your code to use a method modifier (such as 'around').
   - New match method "Method($HTTP_METHOD)" where $HTTP_METHOD in (GET, POST,
     PUT, HEAD, DELETE, OPTION) and shortcuts in controllers called "GET, POST
-    PUT, HEAD, DELETE, OPTION").  Tests and superficial docs (sorry)
+    PUT, HEAD, DELETE, OPTION").  Tests and documentation.  Please note if you
+    are currently using Catalyst::ActionRole::MatchRequestMethods there may
+    be compatibility issues.  You should remove that actionrole since the built
+    in behavior is compatible on its own.
   - security fixes in the way we handle redirects
   - Make Catalyst::Engine and Catalyst::Base immutable
   - Some test and documentation improvements
index b5b2f48..ebedf59 100644 (file)
@@ -65,48 +65,18 @@ sub execute {
   $self->code->(@_);
 }
 
-sub match_captures { 
-  my ( $self, $c, $captures ) = @_;
-  ## It would seem that now that we can match captures, we could remove a lot
-  ## of the capture_args to args mapping all around.  I gave it a go, but was
-  ## not trival, contact jnap on irc for what I tried if you want to try.
-  ##  return $self->_match_has_expected_capture_args($captures) &&
-    return $self->_match_has_expected_http_method($c->req->method);
-}
-
 sub match {
-  my ( $self, $c ) = @_;
-  return $self->_match_has_expected_args($c->req->args) &&
-    $self->_match_has_expected_http_method($c->req->method);
-}
-
-sub _match_has_expected_args {
-  my ($self, $req_args) = @_;
-  return 1 unless exists $self->attributes->{Args};
-  my $args = $self->attributes->{Args}[0];
-  return 1 unless defined($args) && length($args);
-  return scalar( @{$req_args} ) == $args;
+    my ( $self, $c ) = @_;
+    #would it be unreasonable to store the number of arguments
+    #the action has as its own attribute?
+    #it would basically eliminate the code below.  ehhh. small fish
+    return 1 unless exists $self->attributes->{Args};
+    my $args = $self->attributes->{Args}[0];
+    return 1 unless defined($args) && length($args);
+    return scalar( @{ $c->req->args } ) == $args;
 }
 
-#sub _match_has_expected_capture_args {
-#  my ($self, $req_args) = @_;
-#  return 1 unless exists $self->attributes->{CaptureArgs};
-#  my $args = $self->attributes->{CaptureArgs}[0];
-#  return 1 unless defined($args) && length($args);
-#  return scalar( @{$req_args} ) == $args;
-#}
-
-sub _match_has_expected_http_method {
-  my ($self, $method) = @_;
-  my @methods = @{ $self->attributes->{Method} || [] };
-  if(scalar @methods) {
-    my $result = scalar(grep { lc($_) eq lc($method) } @methods) ? 1:0;
-    return $result;
-  } else {
-    ## No HTTP Methods to check
-    return 1;
-  }
-}
+sub match_captures { 1 }
 
 sub compare {
     my ($a1, $a2) = @_;
diff --git a/lib/Catalyst/ActionRole/HTTPMethods.pm b/lib/Catalyst/ActionRole/HTTPMethods.pm
new file mode 100644 (file)
index 0000000..2112cb3
--- /dev/null
@@ -0,0 +1,129 @@
+package Catalyst::ActionRole::HTTPMethods;
+
+use Moose::Role;
+
+requires 'match', 'match_captures';
+
+around ['match','match_captures'], sub {
+  my ($orig, $self, $ctx, @args) = @_;
+  my $expected = $self->_normalize_expected_http_method($ctx->req);
+  return $self->_has_expected_http_method($expected) ?
+    $self->$orig($ctx, @args) :
+    0;
+};
+
+sub _normalize_expected_http_method {
+  my ($self, $req) = @_;
+  return $req->header('X-HTTP-Method') ||
+    $req->header('X-HTTP-Method-Override') ||
+    $req->header('X-METHOD-OVERRIDE') ||
+    $req->method;
+}
+
+sub _has_expected_http_method {
+  my ($self, $expected) = @_;
+  return 1 unless scalar(my @allowed = $self->allowed_http_methods);
+  return scalar(grep { lc($_) eq lc($expected) } @allowed) ?
+    1 : 0;
+}
+
+sub allowed_http_methods { @{shift->attributes->{Method}||[]} }
+
+1;
+
+=head1 NAME
+
+Catalyst::ActionRole::HTTPMethods - Match on HTTP Methods
+
+=head1 SYNOPSIS
+
+    package MyApp::Web::Controller::MyController;
+
+    use Moose;
+    use MooseX::MethodAttributes;
+
+    extends 'Catalyst::Controller';
+
+    sub user_base : Chained('/') CaptureArg(0) { ... }
+
+      sub get_user    : Chained('user_base') Args(1) GET { ... }
+      sub post_user   : Chained('user_base') Args(1) POST { ... }
+      sub put_user    : Chained('user_base') Args(1) PUT { ... }
+      sub delete_user : Chained('user_base') Args(1) DELETE { ... }
+      sub head_user   : Chained('user_base') Args(1) HEAD { ... }
+      sub option_user : Chained('user_base') Args(1) OPTION { ... }
+      sub option_user : Chained('user_base') Args(1) PATCH { ... }
+
+
+      sub post_and_put : Chained('user_base') POST PUT Args(1) { ... }
+      sub method_attr  : Chained('user_base') Method('DELETE') Args(0) { ... }
+
+    __PACKAGE__->meta->make_immutable;
+
+=head1 DESCRIPTION
+
+This is an action role that lets your L<Catalyst::Action> match on standard
+HTTP methods, such as GET, POST, etc.
+
+Since most web browsers have limited support for rich HTTP Method vocabularies
+we also support setting the expected match method via the follow non standard
+but widely used http extensions.  Our support for these should not be taken as
+an endorsement of the technique.   Rt is merely a reflection of our desire to
+work well with existing systems and common client side tools.
+
+=over 4
+
+=item X-HTTP-Method (Microsoft)
+
+=item X-HTTP-Method-Override (Google/GData)
+
+=item X-METHOD-OVERRIDE (IBM)
+
+=back 
+
+Please note the insanity of overriding a GET request with a DELETE override...
+Rational practices suggest that using POST with overrides to emulate PUT and
+DELETE can be an acceptable way to deal with client limitations and security
+rules on your proxy server. I recommend going no further.
+
+=head1 REQUIRES
+
+This role requires the following methods in the consuming class.
+
+=head2 match
+
+=head2 match_captures
+
+Returns 1 if the action matches the existing request and zero if not.
+
+=head1 METHODS
+
+This role defines the following methods
+
+=head2 match
+
+=head2 match_captures
+
+Around method modifier that return 1 if the request method matches one of the
+allowed methods (see L</http_methods>) and zero otherwise.
+
+=head2 allowed_http_methods
+
+An array of strings that are the allowed http methods for matching this action
+normalized as noted above (using X-Method* overrides).
+
+=head2 _has_expected_http_method ($expected)
+
+Private method which returns 1 if C<$expected> matches one of the allowed
+in L</http_methods> and zero otherwise.
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This library is free software. You can redistribute it and/or modify it under
+the same terms as Perl itself.
+
+=cut
index 5fcb574..7645d01 100644 (file)
@@ -333,6 +333,8 @@ sub create_action {
 
     unless ($args{name} =~ /^_(DISPATCH|BEGIN|AUTO|ACTION|END)$/) {
        my @roles = $self->gather_action_roles(%args);
+       push @roles, $self->gather_default_action_roles(%args);
+
        $class = $self->_apply_action_class_roles($class, @roles) if @roles;
     }
 
@@ -352,13 +354,20 @@ sub create_action {
 
 sub gather_action_roles {
    my ($self, %args) = @_;
-
    return (
       (blessed $self ? $self->_action_roles : ()),
       @{ $args{attributes}->{Does} || [] },
    );
 }
 
+sub gather_default_action_roles {
+  my ($self, %args) = @_;
+  my @roles = ();
+  push @roles, 'Catalyst::ActionRole::HTTPMethods'
+    if $args{attributes}->{Method};
+  return @roles;
+}
+
 sub _parse_attrs {
     my ( $self, $c, $name, @attrs ) = @_;
 
@@ -694,6 +703,12 @@ Catalyst::Action (or appropriate sub/alternative class) object.
 
 Gathers the list of roles to apply to an action with the given %action_args.
 
+=head2 $self->gather_default_action_roles(\%action_args)
+
+returns a list of action roles to be applied based on core, builtin rules.
+Currently only the L<Catalyst::ActionRole::HTTPMethods> role is applied
+this way.
+
 =head2 $self->_application
 
 =head2 $self->_app
@@ -811,7 +826,13 @@ The following is exactly the same:
 
 =head2 HEAD
 
-Sets the give action path to match the specified HTTP method.
+=head2 PATCH
+
+=head2 Method('...')
+
+Sets the give action path to match the specified HTTP method, or via one of the
+broadly accepted methods of overriding the 'true' method (see
+L<Catalyst::ActionRole::HTTPMethods>).
 
 =head2 Args