File Coverage

blib/lib/Catalyst/ActionRole/MatchRequestAccepts.pm
Criterion Covered Total %
statement 30 37 81.0
branch 4 10 40.0
condition 0 5 0.0
subroutine 9 10 90.0
pod n/a
total 43 62 69.3


line stmt bran cond sub pod time code
1             package Catalyst::ActionRole::MatchRequestAccepts;
2              
3             our $VERSION = '0.06';
4              
5 2     2   1601431 use 5.008008;
  2         6  
6 2     2   386 use Moose::Role;
  2         311559  
  2         10  
7 2     2   8332 use Perl6::Junction 'all', 'any';
  2         10113  
  2         137  
8 2     2   840 use HTTP::Headers::Util 'split_header_words';
  2         1264  
  2         114  
9 2     2   421 use namespace::autoclean;
  2         5824  
  2         11  
10              
11             requires 'attributes';
12              
13             has http_accept => (is=>'ro', required=>1, default=>'http-accept');
14              
15             sub _http_accept {
16 29     29   692 my ($self, $request) = @_;
17             return map {
18 29         595 $self->_split_accept_header($_);
  24         622  
19             } $request->headers->header('Accept');
20             }
21              
22             sub _split_accept_header {
23 24     24   33 my ($self, $accept_header) = @_;
24 24         18 my %types;
25 24         55 foreach my $pair (split_header_words $accept_header) {
26 26         585 my ($type) = @{$pair}[0];
  26         50  
27 26 50       56 next if $types{$type};
28 26         49 $types{$type}=1;
29             }
30 24         109 return keys %types;
31             }
32              
33             sub _query_accept {
34 0     0   0 my ($self, $request) = @_;
35 0   0     0 my $accept = $request->query_parameters->{$self->http_accept} || undef;
36 0 0 0     0 (defined($accept) && ref($accept)) ? @$accept : $accept;
37             }
38              
39             sub _resolve_http_accept {
40 29     29   29 my ($self, $ctx) = @_;
41 29 50       67 if($ctx->debug) {
42 0         0 my @hdr_accepts = $self->_query_accept($ctx->req);
43 0 0       0 if($hdr_accepts[0]) {
44 0         0 return @hdr_accepts;
45             } else {
46 0         0 return $self->_http_accept($ctx->req);
47             }
48             } else {
49 29         115 return $self->_http_accept($ctx->req);
50             }
51             }
52              
53             sub _resolve_accept_attr {
54 29 100   29   24 @{shift->attributes->{Accept} || []};
  29         663  
55             }
56              
57             around 'match', sub {
58             my ($orig, $self, $ctx) = @_;
59             my @attr_accepts = $self->_resolve_accept_attr;
60             my @hdr_accepts = $self->_resolve_http_accept($ctx);
61              
62             if(@attr_accepts) {
63             if(any(@attr_accepts) eq any(@hdr_accepts)) {
64             return $self->$orig($ctx);
65             } else {
66             return 0;
67             }
68             } else {
69             return $self->$orig($ctx);
70             }
71             };
72              
73             1;
74              
75             =head1 NAME
76              
77             Catalyst::ActionRole::MatchRequestAccepts - Dispatch actions based on HTTP Accept Header
78              
79             =head1 SYNOPSIS
80              
81             package MyApp::Controller::Foo;
82              
83             use Moose;
84             use namespace::autoclean;
85              
86             BEGIN {
87             extends 'Catalyst::Controller::ActionRole';
88             }
89              
90             ## Add the ActionRole to all the Controller's actions. You can also
91             ## selectively add the ActionRole with the :Does action attribute or in
92             ## controller configuration. See Catalyst::Controller::ActionRole for
93             ## more information.
94              
95             __PACKAGE__->config(
96             action_roles => ['MatchRequestAccepts'],
97             );
98              
99             ## Match for incoming requests with HTTP Accepts: plain/html
100             sub for_html : Path('foo') Accept('plain/html') { ... }
101              
102             ## Match for incoming requests with HTTP Accepts: application/json
103             sub for_json : Path('foo') Accept('application/json') { ... }
104              
105             =head1 DESCRIPTION
106              
107             Lets you specify a match for the HTTP C<Accept> Header, which is provided by
108             the L<Catalyst> C<< $ctx->request->headers >> object. You might wish to instead
109             look at L<Catalyst::Action::REST> if you are doing complex applications that
110             match different incoming request types, but if you are very fussy about how
111             your actions match, or if you are doing some simple ajaxy bits you might like
112             to use this instead of a full on package (like L<Catalyst::Action::REST> is.)
113              
114             Currently the match performed is a pure equalty, no attempt to guess or infer
115             matches based on similarity are done. If you need to match several variations
116             you can specify all the variations with multiple attribute declarations. Right
117             now we don't support expression based matching, such as C<text/*>, although
118             adding such would probably not be very hard (although I don't want to make the
119             logic here slow down our dispatch matching too much).
120              
121             Please note that if you specify multiple C<Accept> attributes on a single
122             action, those will be matched via an OR condition and not an AND condition. In
123             other words we short circuit match the first action with at least one of the
124             C<Accept> values appearing in the requested HTTP headers. I think this is
125             correct since I imagine the purpose of multiple C<Accept> attributes would be
126             to match several acceptable variations of a given type, not to match any of
127             several unrelated types. However if you have a use case for this please let
128             me know.
129              
130             If an action consumes this role, but no C<Accept> attributes are found, the
131             action will simple accept all types.
132              
133             For debugging purposes, if the L<Catalyst> debug flag is enabled, you can
134             override the HTTP Accept header with the C<http-accept> query parameter. This
135             makes it easy to force detect in testing or in your browser. This feature is
136             NOT available when the debug flag is off.
137              
138             Also, as usual you can specify attributes and information in th configuration
139             of your L<Catalyst::Controller> subclass:
140              
141             ## Set the 'our_action_json' action to consume this ActionRole. In this
142             ## example GET '/json' would only match if the client request HTTP included
143             ## an Accept: application/json.
144              
145             __PACKAGE__->config(
146             action_roles => ['MatchRequestAccepts'],
147             action => {
148             our_action_json => { Path => 'json', Accept => 'application/json' },
149             });
150              
151             ## GET '/foo' will dispatch to either action 'our_action_json' or action
152             ## 'our_action_html' depending on the incoming HTTP Accept.
153              
154             __PACKAGE__->config(
155             action => {
156             our_action_json => {
157             Does => 'MatchRequestAccepts',
158             Path => 'foo',
159             Accept => 'application/json',
160             },
161             our_action_html => {
162             Does => 'MatchRequestAccepts',
163             Path => 'foo',
164             Accept => 'text/html',
165             },
166             });
167              
168             There's a functioning L<Catalyst> example application in the test directory for
169             your review as well.
170              
171             =head1 EXAMPLE WITH CHAINED ACTIONS
172              
173             The following example uses L<Catalyst> chaining to match one of two different
174             types of C<Accept> headers, and to return the correct HTTP error message if
175             nothing is matched. This is probably my most common use pattern.
176              
177             package MyApp::Web::Controller::Chained;
178              
179             use Moose;
180             use namespace::autoclean;
181              
182             BEGIN {
183             extends 'Catalyst::Controller::ActionRole';
184             }
185              
186             __PACKAGE__->config(
187             action_roles => ['MatchRequestAccepts'],
188             );
189              
190             sub root : Chained('/') PathPrefix CaptureArgs(0) {}
191              
192             sub text_html
193             : Chained('root') PathPart('') Accept('text/html') Args(0)
194             {
195             my ($self, $ctx) = @_;
196             $ctx->response->body('text_html');
197             }
198              
199             sub json
200             : Chained('root') PathPart('') Accept('application/json') Args(0)
201             {
202             my ($self, $ctx) = @_;
203             $ctx->response->body('json');
204             }
205              
206             sub not_accepted
207             : Chained('root') PathPart('') Args
208             {
209             my ($self, $ctx) = @_;
210             $ctx->response->status(406);
211             $ctx->response->body('error_not_accepted');
212             }
213              
214             __PACKAGE__->meta->make_immutable;
215              
216             In the given example, a C<GET> request to C<http://www.myapp.com/chained> will
217             match for C<Accept> values of HTML and JSON, and will return a status 406 error
218             to all other requests.
219              
220             =head1 AUTHOR
221              
222             John Napiorkowski L<email:jjnapiork@cpan.org>
223              
224             =head1 THANKS
225              
226             Shout out to Florian Ragwitz <rafl@debian.org> for providing such a great
227             example in L<Catalyst::ActionRole::MatchRequestMethod>. Source code and tests
228             are pretty much copied from his stuff.
229              
230             I also cargo culted a chuck of code from L<Catalyst::TraitFor::Request::REST>
231             which let me parse HTTP Accept lines.
232              
233             =head1 SEE ALSO
234              
235             L<Catalyst::ActionRole::MatchRequestMethod>, L<Catalyst::Action::REST>,
236             L<Catalyst>, L<Catalyst::Controller::ActionRole>, L<Moose>.
237              
238             =head1 COPYRIGHT & LICENSE
239              
240             Copyright 2011, John Napiorkowski L<email:jjnapiork@cpan.org>
241              
242             This library is free software; you can redistribute it and/or modify it under
243             the same terms as Perl itself.
244              
245             =cut