File Coverage

blib/lib/Web/Dispatch/HTTPMethods.pm
Criterion Covered Total %
statement 47 48 97.9
branch 10 12 83.3
condition 2 3 66.6
subroutine 22 23 95.6
pod 0 9 0.0
total 81 95 85.2


line stmt bran cond sub pod time code
1             package Web::Dispatch::HTTPMethods;
2              
3 1     1   882 use strictures 1;
  1         11  
  1         78  
4 1     1   855 use Web::Dispatch::Predicates qw(match_method);
  1         3  
  1         132  
5 1     1   11 use Scalar::Util qw(blessed);
  1         3  
  1         76  
6 1     1   8 use Exporter 'import';
  1         2  
  1         1211  
7              
8             our @EXPORT = qw(GET HEAD POST PUT DELETE OPTIONS);
9              
10 4     4 0 6 sub HEAD(&;@) { method_helper(HEAD => @_) }
11 13     13 0 34 sub GET(&;@) { method_helper(GET => @_) }
12 6     6 0 13 sub POST(&;@) { method_helper(POST => @_) }
13 6     6 0 77 sub PUT(&;@) { method_helper(PUT => @_) }
14 0     0   0 sub DELETE(&;@) { method_helper(DELETE => @_) }
15 4     4 0 34 sub OPTIONS(&;@) { method_helper(OPTIONS => @_) }
16              
17             {
18             package Web::Dispatch::HTTPMethods::Endpoint;
19              
20 13     13   24 sub new { bless { map { $_=>0 } @EXPORT }, shift }
  78         166  
21 4     4   12 sub hdrs { 'Content-Type' => 'text/plain' }
22              
23             sub create_implicit_HEAD {
24 6     6   8 my $self = shift;
25 6 100 66     35 if($self->{GET} && not $self->{HEAD}) {
26 5     2   29 $self->{HEAD} = sub { [ @{$self->{GET}->(@_)}[0,1], []] };
  2         5  
  2         9  
27             }
28             }
29              
30             sub create_implicit_OPTIONS {
31 6     6   7 my $self = shift;
32             $self->{OPTIONS} = sub {
33 1     1   4 [200, [$self->hdrs, Allow=>$self->allowed] , [] ];
34 6         18 };
35             }
36              
37 4     4   10 sub allowed { join ',', grep { $_[0]->{$_} } @EXPORT }
  24         45  
38              
39             sub to_app {
40 6     6   9 my $self = shift;
41 6         15 my $implicit_HEAD = $self->create_implicit_HEAD;
42 6         16 my $implicit_OPTIONS = $self->create_implicit_OPTIONS;
43              
44             return sub {
45 6     6   9 my $env = shift;
46 6 100       19 if($env->{REQUEST_METHOD} eq 'HEAD') {
    100          
47 2         23 $implicit_HEAD->($env);
48             } elsif($env->{REQUEST_METHOD} eq 'OPTIONS') {
49 1         3 $implicit_OPTIONS->($env);
50             } else {
51 3         6 [405, [$self->hdrs, Allow=>$self->allowed] , ['Method Not Allowed'] ];
52             }
53 6         27 };
54             }
55             }
56              
57             sub isa_endpoint {
58 33 50   33 0 540 blessed($_[0]) &&
59             $_[0]->isa('Web::Dispatch::HTTPMethods::Endpoint')
60             }
61              
62 33     33 0 83 sub endpoint_from { return $_[-1] }
63 13     13 0 90 sub new_endpoint { Web::Dispatch::HTTPMethods::Endpoint->new(@_) }
64              
65             sub method_helper {
66 33     33 0 89 my $predicate = match_method(my $method = shift);
67 33         59 my ($code, @following ) = @_;
68             endpoint_from( my @dispatchers =
69             scalar(@following) ? ($predicate, @_) : ($predicate, @_, new_endpoint)
70 33 100       112 )->{$method} = $code;
71              
72 33 50       72 die "Non HTTP Method dispatcher detected in HTTP Method scope"
73             unless(isa_endpoint($dispatchers[-1]));
74              
75 33         187 return @dispatchers;
76             }
77              
78              
79             1;
80              
81             =head1 NAME
82              
83             Web::Dispatch::HTTPMethods - Helpers to make RESTFul Dispatchers Easier
84              
85             =head1 SYNOPSIS
86              
87             package MyApp:WithHTTPMethods;
88              
89             use Web::Simple;
90             use Web::Dispatch::HTTPMethods;
91              
92             sub as_text {
93             [200, ['Content-Type' => 'text/plain'],
94             [$_[0]->{REQUEST_METHOD}, $_[0]->{REQUEST_URI}] ]
95             }
96              
97             sub dispatch_request {
98             sub (/get) {
99             GET { as_text(pop) }
100             },
101             sub (/get-head) {
102             GET { as_text(pop) }
103             HEAD { [204,[],[]] },
104             },
105             sub (/get-post-put) {
106             GET { as_text(pop) } ## NOTE: no commas separating http methods
107             POST { as_text(pop) }
108             PUT { as_text(pop) }
109             },
110             }
111              
112             =head1 DESCRIPTION
113              
114             Exports the most commonly used HTTP methods as subroutine helpers into your
115             L based application.
116             Use of these methods additionally adds an automatic HTTP code 405
117             C response if none of the HTTP methods match for a given dispatch and
118             also adds a dispatch rule for C if no C exists but a C does
119             (in which case the C returns the C dispatch with an empty body.)
120              
121             We also add support at the end of the chain for the OPTIONS method.
122             This defaults to HTTP 200 OK + Allows http headers.
123              
124             We also try to set correct HTTP headers such as C as makes sense based
125             on your dispatch chain.
126              
127             The following dispatch chains are basically the same:
128              
129             sub dispatch_request {
130             sub (/get-http-methods) {
131             GET { [200, ['Content-Type' => 'text/plain'], ['Hello World']] }
132             },
133             sub(/get-classic) {
134             sub (GET) { [200, ['Content-Type' => 'text/plain'], ['Hello World']] },
135             sub (HEAD) { [200, ['Content-Type' => 'text/plain'], []] },
136             sub (OPTIONS) {
137             [200, ['Content-Type' => 'text/plain', Allows=>'GET,HEAD,OPTIONS'], []];
138             },
139             sub () {
140             [405, ['Content-Type' => 'text/plain', Allows=>'GET,HEAD,OPTIONS'],
141             ['Method Not Allowed']]
142             },
143             }
144             }
145              
146             The idea here is less boilerplate to distract the reader from the main point of
147             the code and also to encapsulate some best practices.
148              
149             B You currently cannot mix http method style and prototype sub style in
150             the same scope, as in the following example:
151              
152             sub dispatch_request {
153             sub (/get-head) {
154             GET { ... }
155             sub (HEAD) { ... }
156             },
157             }
158              
159             If you try this our code will notice and issue a C. If you have a good use
160             case please bring it to the authors.
161              
162             =head2 EXPORTS
163              
164             This automatically exports the following subroutines:
165              
166             GET
167             PUT
168             POST
169             HEAD
170             DELETE
171             OPTIONS
172              
173             =head1 AUTHOR
174              
175             See L for AUTHOR
176              
177             =head1 CONTRIBUTORS
178              
179             See L for CONTRIBUTORS
180              
181             =head1 COPYRIGHT
182              
183             See L for COPYRIGHT
184              
185             =head1 LICENSE
186              
187             See L for LICENSE
188              
189             =cut
190