File Coverage

blib/lib/Dancer2/Plugin/HTTP/ContentNegotiation.pm
Criterion Covered Total %
statement 15 58 25.8
branch 0 32 0.0
condition n/a
subroutine 5 8 62.5
pod n/a
total 20 98 20.4


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::HTTP::ContentNegotiation;
2              
3             =head1 NAME
4              
5             Dancer2::Plugin::HTTP::ContentNegotiation - Server-driven negotiation
6              
7             =head1 VERSION
8              
9             Version 0.01
10              
11             =cut
12              
13             our $VERSION = '0.02';
14              
15 1     1   21611 use warnings;
  1         2  
  1         32  
16 1     1   5 use strict;
  1         2  
  1         22  
17              
18 1     1   6 use Carp;
  1         6  
  1         75  
19 1     1   839 use Dancer2::Plugin;
  1         150714  
  1         7  
20              
21             =head1 SYNOPSIS
22              
23             HTTP specifies two types of content negotiation. These are server-driven
24             negotiation and agent-driven negotiation. Server-driven negotiation uses request
25             headers to select a variant, and agent-driven negotiation uses a distinct URI
26             for each variant.
27              
28             This plugin handles server-driven negotiation.
29              
30             use Dancer2;
31            
32             use Dancer2::Plugin::HTTP::ContentNegotiation;
33            
34             get '/greetings' => sub {
35             http_choose_language (
36             'en' => sub { 'Hello World' },
37             'en-GB' => sub { 'Hello London' },
38             'en-US' => sub { 'Hello Washington' },
39             'nl' => sub { 'Hallo Amsterdam' },
40             'de' => sub { 'Hallo Berlin' },
41             # default is first in the list
42             );
43             };
44            
45             get '/choose/:id' => sub {
46             my $data = SomeResource->find(param('id'));
47             http_choose_media_type (
48             'application/json' => sub { to_json $data },
49             'application/xml ' => sub { to_xml $data },
50             { default => undef }, # default is 406: Not Acceptable
51             );
52             };
53            
54             get '/thumbnail/:id' => sub {
55             http_choose_media_type (
56             [ 'image/png', 'image/gif', 'image/jpeg' ]
57             => sub { Thumbnail->new(param('id'))->to(http_chosen->minor) },
58             { default => 'image/png' }, # must be one listed above
59             );
60             };
61            
62             dance;
63              
64             =head1 HTTP ContentNegotiation
65              
66             Clients that make an HTTP request can specify what kind of response they prefer.
67             This can be a specific MIME-type, a different language, the text-encoding (if it
68             applies to text documents) and wether it should be compressed or not. For this,
69             the HTTP specifications in RFC 7231 (HTTP/1.1 Semantics and Content) Section 5.3
70             explains how to use resp. Accept, Accept-Language, Accept-Charset and
71             Accept-Encoding header fields.
72              
73             The server can try to send a response that the client would accept, but if there
74             is no respresentation avaialbe in that format or language, it has three options.
75             Either give a response in a different way, or respond with a status message 406,
76             Not Accaptable. Another option would provide a list of available formats.
77              
78             =cut
79              
80 1     1   1056 use HTTP::Headers::ActionPack;
  1         1020  
  1         1164  
81              
82             # use List::MoreUtils 'first_index';
83              
84             our $negotiator = HTTP::Headers::ActionPack->new->get_content_negotiator;
85             our %http_headers = (
86             'media_type' => "Accept",
87             'language' => "Accept-Language",
88             'charset' => "Accept-Charset",
89             'encoding' => "Accept-Encoding",
90             );
91              
92             =head1 DANCER2 KEYWORDS
93              
94             Each of the 'http_choose_...' keywords take the following arguments:
95              
96             =over
97              
98             =item a paired list with 'selectors' and coderefs.
99              
100             those selectors, be it a single one or a anonymous array ref, numerate the
101             available choices, the coderef following will be executed if there would be a
102             match.
103              
104             =item an optional hashref with options.
105              
106             The only option there is at this moment is 'default'. If not present and there
107             is no match, it will use the first mentioned selector. If spcified, it will take
108             that selector. Set to undef will return a status code of 406, Not Acceptable.
109              
110             =back
111              
112             http_choose_selector (
113             selection_1
114             => sub { ... },
115             [ selction_2, selection_3, selection_4 ]
116             => sub { ... },
117             { default => selection_3 }
118             );
119              
120             =cut
121              
122             =head2 http_choose_media_type
123              
124             This keyword is used to make a selection between different MIME-types. Please
125             use this explicit version, as there is also http_choose (there is no
126             Accept-MediaType, it's simply Accept)
127              
128             =cut
129              
130             register 'http_choose_media_type' => sub {
131             return _http_choose ( @_, 'media_type' );
132             };
133              
134             =head2 http_choose_language
135              
136             This keyword works in conjunction with the Accept-Language.
137              
138             =cut
139              
140             register 'http_choose_language' => sub {
141             return _http_choose ( @_, 'language' );
142             };
143              
144             =head2 http_choose_charset
145              
146             This keyword should only be used with non-binary media-types, like XML or JSON.
147             It is used to select in what 'encoding' the representation should be delivered.
148              
149             NOTE: not sure yet how this word with the default UTF-8 Encoding of Dancer2.
150              
151             =cut
152              
153             register 'http_choose_charset' => sub {
154             return _http_choose ( @_, 'charset' );
155             };
156              
157             =head2 http_choose_encoding
158              
159             Mainly used for specifying compressed or uncompressed content. It has nothing to
160             do whith character encoding though!
161              
162             NOTE: not sure if this is the right place to compress files or not - maybe it
163             would be better of to do this in Middleware.
164              
165             =cut
166              
167             register 'http_choose_encoding' => sub {
168             return _http_choose ( @_, 'encoding' );
169             };
170              
171             =head2 http_choose
172              
173             Naming compatability with the HTTP Headers, please use te explicit
174             'http_choose_media_type'
175              
176             =cut
177              
178             register 'http_choose' => sub {
179             return _http_choose ( @_, 'media_type' );
180             };
181              
182             sub _http_choose {
183 0     0     my $dsl = shift;
184 0           my $switch = pop;
185 0 0         my $options = (@_ % 2) ? pop : undef;
186            
187 0           my @choices = _parse_choices(@_);
188            
189             # prepare for default behaviour
190             # default ... if none match, pick first in definition list
191             # default => 'choice' ... takes this as response, must be defined!
192             # default => undef ... do not make assumptions, return 406
193 0 0         my $choice_first = ref $_[0] eq 'ARRAY' ? $_[0]->[0] : $_[0];
194 0 0         my $choice_default = $options->{'default'} if exists $options->{'default'};
195            
196             # # make sure that a 'default' is actually in the list of choices
197             #
198             # if ( $choice_default and not exists $choices{$choice_default} ) {
199             # $dsl->app->log ( warning =>
200             # qq|Invallid http_choose usage: |
201             # . qq|'$choice_default' does not exist in choices|
202             # );
203             # $dsl->status(500);
204             # $dsl->halt;
205             # }
206            
207             # choose from the provided definition
208 0           my $selected = undef;
209 0           my $method = 'choose' . '_' . $switch;
210 0 0         if ( $dsl->request->header($http_headers{$switch}) ) {
211             $selected = $negotiator->$method (
212 0           [ map { $_->{selector} } @choices ],
213 0           $dsl->request->header($http_headers{$switch})
214             );
215             };
216            
217             # if nothing selected, use sensible default
218             # $selected ||= exists $options->{'default'} ? $options->{'default'} : $choice_first;
219 0 0         unless ($selected) {
220             $selected = $negotiator->$method (
221 0           [ map { $_->{selector} } @choices ],
222 0 0         exists $options->{'default'} ? $options->{'default'} : $choice_first
223             );
224             };
225            
226             # if still nothing selected, return 406 error
227 0 0         unless ($selected) {
228 0           $dsl->status(406); # Not Acceptable
229 0           $dsl->halt;
230             };
231            
232 0           $dsl->vars->{"http_chosen_$switch"} = $selected;
233            
234             # set the apropriate headers for Content-Type and Content-Language
235             # XXX Content-Type could consist of type PLUS charset if it's text-based
236 0 0         if ($switch eq 'media_type') {
237 0           $dsl->header('Content-Type' => "$selected" );
238             };
239 0 0         if ($switch eq 'language') {
240 0           $dsl->header('Content-Language' => "$selected" );
241             };
242            
243             $dsl->header('Vary' =>
244 0 0         join ', ', $http_headers{$switch}, $dsl->header('Vary')
245             ) if @choices > 1 ;
246            
247 0           my @coderefs = grep {$_->{selector} eq $selected} @choices;
  0            
248 0           return $coderefs[0]{coderef}->($dsl);
249             };
250              
251             =head2 http_chosen_media_type
252              
253             returns a MediaType object that has been chosen.
254              
255             This feature is experimental, but provides methods like type, major and minor
256              
257             =cut
258              
259             register 'http_chosen_media_type' => sub {
260             return _http_chosen ( @_, 'media_type' );
261             };
262              
263             =head2 http_chosen_language
264              
265             returns the LanguageTag being chosen from the selectors.
266              
267             Experimental too and should privde methods like language, primary, extlang,
268             script, region and variant
269              
270             =cut
271              
272             register 'http_chosen_language' => sub {
273             return _http_chosen ( @_, 'language' );
274             };
275              
276             =head2 http_chosen_charset
277              
278             returns the chosen Charset.
279              
280             =cut
281              
282             register 'http_chosen_charset' => sub {
283             return _http_chosen ( @_, 'charset' );
284             };
285              
286             =head2 http_chose_encoding
287              
288             returns wether or not the resouce should be compressed and how.
289              
290             =cut
291              
292             register 'http_chosen_encoding' => sub {
293             return _http_chosen ( @_, 'encoding' );
294             };
295              
296             =head2 http_chosen
297              
298             Naming compatability with the HTTP Headers, please use te explicit
299             'http_chosen_media_type'
300              
301             =cut
302              
303             register 'http_chosen' => sub {
304             return _http_chosen ( @_, 'media_type' );
305             };
306              
307             sub _http_chosen {
308 0     0     my $dsl = shift;
309 0           my $switch = pop;
310            
311             $dsl->app->log ( error =>
312             "http_chosen_$switch does not exist"
313 0 0         ) unless exists $dsl->vars->{"http_chosen_$switch"};
314            
315 0 0         $dsl->app->log( error =>
316             "http_chosen_$switch is designed for read-only"
317             ) if (@_ >= 1);
318            
319 0 0         return unless exists $dsl->vars->{"http_chosen_$switch"};
320 0           return $dsl->vars->{"http_chosen_$switch"};
321             };
322              
323             on_plugin_import {
324             my $dsl = shift;
325             my $app = $dsl->app;
326             };
327              
328             sub _parse_choices {
329             # _parse_choices
330             # unraffles a paired list into a list of hashes,
331             # each hash containin a 'selector' and associated coderef.
332             # since the 'key' can be an arrayref too, these are added to the list with
333             # seperate values
334            
335 0     0     my @choices;
336 0           while ( @_ ) {
337 0           my ($choices, $coderef) = @{[ shift, shift ]};
  0            
338 0 0         last unless $choices;
339             # turn a single value into a ARRAY REF
340 0 0         $choices = [ $choices ] unless ref $choices eq 'ARRAY';
341             # so we only have ARRAY REFs to deal with
342 0           foreach ( @$choices ) {
343 0 0         if ( ref $coderef ne 'CODE' ) {
344 0           die
345             qq{Invallid http_choose usage: }
346             . qq{'$_' needs a CODE ref};
347             }
348             # if ( exists $choices{$_} ) {
349             # die
350             # qq{Invallid http_choose usage: }
351             # . qq{Duplicated choice '$_'};
352             # }
353 0           push @choices,
354             {
355             selector => $_,
356             coderef => $coderef,
357             };
358             }
359             }
360 0           return @choices;
361             }; # _parse_choices
362              
363             register_plugin;
364              
365             =head1 CAVEATS
366              
367             the underlying HTTP::ActionPack has it's own bugs - for the time being this
368             module uses those modules and will suffer from many of the shortcommings that
369             come from using ActionPack.
370              
371             =head1 AUTHOR
372              
373             Theo van Hoesel, C<< >>
374              
375             =head1 BUGS
376              
377             Please report any bugs or feature requests to
378             C, or through the web
379             interface at
380             L.
381             I will be notified, and then you'll automatically be notified of progress on
382             your bug as I make changes.
383              
384              
385              
386             =head1 SUPPORT
387              
388             You can find documentation for this module with the perldoc command.
389              
390             perldoc Dancer2::Plugin::HTTP::ContentNegotiation
391              
392              
393             You can also look for information at:
394              
395             =over 4
396              
397             =item * RT: CPAN's request tracker (report bugs here)
398              
399             L
400              
401             =item * AnnoCPAN: Annotated CPAN documentation
402              
403             L
404              
405             =item * CPAN Ratings
406              
407             L
408              
409             =item * Search CPAN
410              
411             L
412              
413             =back
414              
415              
416             =head1 ACKNOWLEDGEMENTS
417              
418              
419             =head1 LICENSE AND COPYRIGHT
420              
421             Copyright 2015 Theo van Hoesel.
422              
423             This program is free software; you can redistribute it and/or modify it
424             under the terms of the the Artistic License (2.0). You may obtain a
425             copy of the full license at:
426              
427             L
428              
429             Any use, modification, and distribution of the Standard or Modified
430             Versions is governed by this Artistic License. By using, modifying or
431             distributing the Package, you accept this license. Do not use, modify,
432             or distribute the Package, if you do not accept this license.
433              
434             If your Modified Version has been derived from a Modified Version made
435             by someone other than you, you are nevertheless required to ensure that
436             your Modified Version complies with the requirements of this license.
437              
438             This license does not grant you the right to use any trademark, service
439             mark, tradename, or logo of the Copyright Holder.
440              
441             This license includes the non-exclusive, worldwide, free-of-charge
442             patent license to make, have made, use, offer to sell, sell, import and
443             otherwise transfer the Package with respect to any patent claims
444             licensable by the Copyright Holder that are necessarily infringed by the
445             Package. If you institute patent litigation (including a cross-claim or
446             counterclaim) against any party alleging that the Package constitutes
447             direct or contributory patent infringement, then this Artistic License
448             to you shall terminate on the date that such litigation is filed.
449              
450             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
451             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
452             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
453             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
454             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
455             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
456             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
457             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
458              
459             =cut
460              
461             1;