File Coverage

blib/lib/REST/Application.pm
Criterion Covered Total %
statement 236 245 96.3
branch 79 96 82.2
condition 26 34 76.4
subroutine 49 51 96.0
pod 19 35 54.2
total 409 461 88.7


line stmt bran cond sub pod time code
1             # vi:ai:sm:et:sw=4:ts=4:tw=0
2             #
3             # REST::Application - A framework for building RESTful web-applications.
4             #
5             # Copyright 2005 Matthew O'Connor
6             package REST::Application;
7 2     2   36265 use strict;
  2         3  
  2         67  
8 2     2   11 use warnings;
  2         4  
  2         49  
9 2     2   11 use Carp;
  2         7  
  2         153  
10 2     2   1816 use Tie::IxHash;
  2         10254  
  2         53  
11 2     2   1717 use UNIVERSAL;
  2         27  
  2         11  
12 2     2   4432 use CGI;
  2         33703  
  2         16  
13              
14             our $VERSION = '0.992';
15              
16             ####################
17             # Class Methods
18             ####################
19              
20             sub new {
21 101     101 1 308698 my ($proto, %args) = @_;
22 101 50       266 my $class = ref($proto) ? ref($proto) : $proto;
23 101         363 my $self = bless({ __defaultQuery => CGI->new() }, $class);
24 101         47243 $self->setup(%args);
25 101         282 return $self;
26             }
27              
28             ##################################
29             # Instance Methods - Object State
30             ##################################
31              
32             sub query {
33 379     379 1 6482 my $self = shift;
34              
35             # Set default value if this method hasn't been called yet.
36 379 100       936 if (not exists $self->{__query}) {
37 66         154 $self->{__query} = $self->defaultQueryObject();
38             }
39              
40             # Set the field if we got any arguments.
41 379 100       679 $self->{__query} = shift if @_;
42              
43 379         6077 return $self->{__query};
44             }
45              
46             sub defaultQueryObject {
47 72     72 1 118 my $self = shift;
48              
49             # Set the field if we got any arguments.
50 72 100       164 if (@_) {
51 4         12 $self->{__defaultQuery} = shift;
52             }
53              
54 72         191 return $self->{__defaultQuery};
55             }
56              
57             sub resourceHooks {
58 177     177 1 185365 my $self = shift;
59              
60             # Set default value if this method hasn't been called yet.
61 177 100       530 if (not exists $self->{__resourceHooks}) {
62 58         70 my %hooks;
63 58         306 tie(%hooks, "Tie::IxHash"); # For keeping hash key order preserved.
64 58         783 $self->{__resourceHooks} = \%hooks;
65             }
66              
67             # If we got arguments then they should be an even sized list, otherwise a
68             # hash reference.
69 177 100 100     806 if (@_ and @_%2 == 0) {
    100          
70 59         184 %{ $self->{__resourceHooks} } = @_;
  59         8690  
71             } elsif (@_ == 1) {
72 2         31 my $value = shift;
73 2 50       8 if (ref($value) ne 'HASH') {
74 0         0 croak "Expected hash reference or even-sized list.";
75             }
76 2         6 %{ $self->{__resourceHooks} } = %$value;
  2         16  
77             }
78              
79 177         170260 return $self->{__resourceHooks};
80             }
81              
82             sub extraHandlerArgs {
83 65     65 0 109 my $self = shift;
84              
85             # Set default value for method if it hasn't been called yet.
86 65 100       162 if (not exists $self->{__extraHandlerArgs}) {
87 47         111 $self->{__extraHandlerArgs} = [];
88             }
89              
90             # If got arguments then process them. We expect either a single array
91             # reference or a list
92 65 100       131 if (@_) {
93 7 100 66     29 if (@_ == 1 and ref($_[0]) eq 'ARRAY') {
94 1         2 $self->{__extraHandlerArgs} = shift;
95             } else {
96 6         19 $self->{__extraHandlerArgs} = [ @_ ];
97             }
98             }
99              
100 65         69 return @{ $self->{__extraHandlerArgs} };
  65         194  
101             }
102              
103             ##################################
104             # Instance Methods - Proxies
105             ##################################
106              
107             sub getPathInfo {
108 45     45 0 141 my $self = shift;
109 45         96 return $self->query->path_info();
110             }
111              
112             sub getRealRequestMethod {
113 81     81 0 109 my $self = shift;
114 81   100     184 return uc( $self->query->request_method() || "" );
115             }
116              
117             sub getRequestMethod {
118 75     75 1 117 my $self = shift;
119 75         193 my $real_method = $self->getRealRequestMethod();
120 75   100     884 my $tunnel_method = uc(
121             $self->query->http('X-HTTP-Method')
122             || $self->query->url_param('http_method')
123             || $self->query->param('http_method')
124             || $real_method
125             );
126              
127             # POST can tunnel any method.
128 75 100       1699 return $tunnel_method if $real_method eq 'POST';
129              
130             # GET can only tunnel GET/HEAD
131 67 100 100     698 if ( $real_method eq 'GET' and $tunnel_method =~ /^(GET|HEAD)$/ ) {
132 17         59 return $tunnel_method;
133             }
134              
135 50         204 return $real_method;
136             }
137              
138             #############################
139             # Instance Methods - Public
140             #############################
141              
142             sub loadResource {
143 29     29 1 101 my ($self, $path, @extraArgs) = @_;
144 29   100     110 $path ||= $self->getMatchText();
145 29     2   3765 my $handler = sub { $self->defaultResourceHandler(@_) };
  2         15  
146 29         58 my $matches = [];
147              
148             # Loop through the keys of the hash returned by resourceHooks(). Each of
149             # the keys is a regular expression, see if the current path info matches
150             # that regular expression. Save the parent matches for passing into the
151             # handler.
152 29         38 for my $pathRegex (keys %{ $self->resourceHooks() }) {
  29         66  
153 30 100       389 if ($self->checkMatch($path, $pathRegex)) {
154 27         80 $handler = $self->_getHandlerFromHook($pathRegex);
155 27         85 last;
156             }
157             }
158              
159 29         97 return $self->callHandler($handler, @extraArgs);
160             }
161              
162             sub getHandlerArgs {
163 29     29 0 40 my ($self, @extraArgs) = @_;
164 29         69 my @args = ($self,
165             $self->_getLastRegexMatches(),
166             $self->extraHandlerArgs(),
167             @extraArgs);
168              
169             # Don't make $self the first argument if the handler is a method on $self,
170             # because in that case it'd be redundant. Also see _getHandlerFromHook().
171 29 100       76 shift @args if $self->{__handlerIsOurMethod};
172              
173 29         110 return @args;
174             }
175              
176             sub callHandler {
177 58     58 0 206 my ($self, $handler, @extraArgs) = @_;
178 58         160 my @args = $self->getHandlerArgs(@extraArgs);
179              
180             # Call the handler, carp if something goes wrong.
181 58         70 my $result;
182 58         76 eval {
183 58         154 $self->preHandler(\@args); # no-op by default.
184 58         200 $result = $handler->(@args);
185 58         416 $self->postHandler(\$result, \@args); # no-op by default.
186             };
187 58 50       190 carp "Handler failed: $@\n" if $@;
188              
189             # Convert the result to a scalar result if it isn't already.
190 58 50       145 my $ref = (ref($result) eq 'scalar') ? $result : \$result;
191              
192 58         325 return $ref;
193             }
194              
195             sub getMatchText {
196 31     31 0 42 my $self = shift;
197 31         96 return $self->getPathInfo();
198             }
199              
200             sub checkMatch {
201 68     68 0 217 my ($self, $a, $b) = @_;
202 68         94 my $match = 0;
203              
204 68 100       979 if ($a =~ /$b/) {
205 49         146 $self->_setLastRegexMatches();
206 49         89 $self->{__last_match_path} = $a;
207 49         78 $self->{__last_match_pattern} = $b;
208 49         64 $match = 1;
209             }
210              
211 68         234 return $match;
212             }
213              
214             sub getLastMatchPath {
215 2     2 1 4 my $self = shift;
216 2         11 return $self->{__last_match_path};
217             }
218              
219             sub getLastMatchPattern {
220 2     2 1 452 my $self = shift;
221 2         16 return $self->{__last_match_pattern};
222             }
223              
224             sub run {
225 19     19 1 89 my $self = shift;
226              
227             # Get resource.
228 19         54 $self->preRun(); # A no-op by default.
229 19         87 my $repr = $self->loadResource(@_);
230 19         63 $self->postRun($repr); # A no-op by default.
231              
232             # Get the headers and then add the representation to to the output stream.
233 19         88 my $output = $self->getHeaders();
234 19         72 $self->addRepresentation($repr, \$output);
235              
236             # Send the output unless we're told not to by the environment.
237 19 50       51 print $output if not $ENV{REST_APP_RETURN_ONLY};
238              
239 19         51 return $output;
240             }
241              
242             sub getHeaders {
243 23     23 0 46 my $self = shift;
244 23   50     55 my $type = $self->headerType() || "";
245 23         27 my $header = "";
246              
247 23 100       53 if ($type eq 'header') {
    50          
    0          
248 21         43 $header = $self->query->header($self->header());
249             } elsif ($type eq 'redirect') {
250 2         6 $header = $self->query->redirect($self->header());
251             } elsif ($type ne 'none') {
252 0         0 croak "Unexpected header type: \"$type\".";
253             }
254              
255 23         11521 return $header;
256             }
257              
258             sub addRepresentation {
259 23     23 0 1275 my ($self, $repr, $outputRef) = @_;
260              
261             # Make sure $outputRef is a scalar ref and the scalar it references is
262             # defined.
263 23 50       66 return if ref($outputRef) ne 'SCALAR';
264 23 50       57 return if not defined $$outputRef;
265              
266             # If we're given a scalar reference then dereference it first, otherwise
267             # just treat what we got as though it's a string.
268 23 100       50 if (ref($repr) eq 'SCALAR') {
269 21 100       73 $$outputRef .= $$repr if defined $$repr;
270             } else {
271 2 50       17 $$outputRef .= $repr if defined $repr;
272             }
273             }
274              
275             sub headerType {
276 39     39 1 68 my $self = shift;
277              
278             # Set the default value if this method has not been called yet.
279 39 100       107 if (not exists $self->{__headerType}) {
280 27         58 $self->{__headerType} = "header";
281             }
282              
283             # If an argument was passed in then use them to set the header type.
284 39 100       91 if (@_) {
285 10   50     33 my $type = lc(shift || "");
286 10 100       52 if ($type =~ /^(redirect|header|none)$/) {
287 8         16 $self->{__headerType} = $type;
288             } else {
289 2         368 croak "Invalid header type specified: \"$type\"";
290             }
291             }
292              
293 37         110 return $self->{__headerType};
294             }
295              
296             sub header {
297 41     41 1 1301 my $self = shift;
298              
299             # Set the default value if this method has not been called yet.
300 41 100       141 if (not exists $self->{__header}) {
301 27         58 $self->{__header} = {};
302             }
303              
304             # If arguments were passed in then use them to set the header type.
305             # Arguments can be passed in as a hash-ref or as an even sized list.
306 41 100       84 if (@_) {
307 10 50       32 if (@_%2 == 0) { # even-sized list, must be hash
    0          
308 10         15 %{ $self->{__header} } = @_;
  10         69  
309             } elsif (ref($_[0]) eq 'HASH') { # First item must be a hash reference
310 0         0 $self->{__header} = shift;
311             } else {
312 0         0 croak "Expected even-sized list or hash reference.";
313             }
314             }
315            
316 41         39 return %{$self->{__header}};
  41         563  
317             }
318              
319             sub resetHeader {
320 2     2 1 16 my $self = shift;
321 2         7 my %old = $self->header();
322 2         7 $self->headerType('header');
323 2         5 $self->{__header} = {};
324 2         11 return %old;
325             }
326              
327             sub setRedirect {
328 2     2 0 14 my ($self, $url) = @_;
329 2         7 $self->headerType('redirect');
330 2   50     13 $self->header(-url => $url || "");
331             }
332              
333             ##############################################
334             # Instance Methods - Intended for Overloading
335             ##############################################
336              
337 101     101 1 154 sub setup { return }
338 6     6 0 14 sub preRun { return }
339 6     6 0 10 sub postRun{ return }
340 41     41 0 55 sub preHandler { return }
341 41     41 0 57 sub postHandler { return }
342 10     10 1 20 sub defaultResourceHandler { return }
343              
344             #############################
345             # Instance Methods - Private
346             #############################
347              
348             # CodeRef _getHandlerFromHook(String $pathRegex)
349             #
350             # This method retrieves a code reference which will yield the resource of the
351             # given $pathRegex, where $pathRegex is a key into the resource hooks hash (it
352             # isn't used as a regex in this method, just a hash key).
353             sub _getHandlerFromHook {
354 52     52   79 my ($self, $pathRegex) = @_;
355 52         103 my $ref = $self->resourceHooks()->{$pathRegex};
356 52         414 my $refType = ref($ref);
357 52     4   187 my $handler = sub { $self->defaultResourceHandler(@_) };
  4         19  
358 52   100     133 my $method = $self->getRequestMethod() || "getResource";
359              
360             # If we get a hash, then use the request method to narrow down the choice.
361             # We do this first because we allow the same range of handler types for a
362             # particular HTTP method that we do for a more generic handler.
363 52 100       173 if ($refType eq 'HASH') {
364 15         40 %$ref = map { uc($_) => $ref->{$_} } keys %$ref; # Uppercase the keys
  42         129  
365 15         47 my $http_method = $self->getRequestMethod();
366 15 100       39 if (exists $ref->{$http_method}) {
    50          
367 14         20 $ref = $ref->{$http_method}
368             } elsif (exists $ref->{'*'}) {
369 1         33 $ref = $ref->{'*'};
370             } else {
371 0         0 return $handler; # Just bail now with the default handler.
372             }
373 15         20 $refType = ref($ref);
374             }
375              
376             # If we still have a hash then assume we're doing Content Negotation
377 52 100       125 if ($refType eq 'HASH') {
378 6         18 my $type = $self->bestContentType(keys %$ref);
379 6         14 $ref = $ref->{$type};
380 6         11 $refType = ref($ref);
381             }
382              
383             # Based on the the reference's type construct the handler.
384 52 100       140 if ($refType eq "CODE") {
    100          
    100          
    100          
385             # A code reference
386 36         52 $handler = $ref;
387             } elsif ($refType eq "ARRAY") {
388             # Array reference which holds a $object and "method name" pair.
389 6         12 my ($thing, $smethod) = @$ref;
390 6   33     15 $smethod ||= $method;
391 6 100       15 if (ref $thing) {
392 5         24 $handler = $self->makeHandlerFromRef($thing, $smethod);
393             } else {
394 1         6 $handler = $self->makeHandlerFromClass($thing, $smethod);
395             }
396             } elsif ($refType) {
397             # Object with GET, PUT, etc, or getResource method.
398 2         17 $handler = $self->makeHandlerFromRef($ref, $method);
399             } elsif ($ref) {
400             # A bare string signifying a method name
401 4     4   22 $handler = sub { $self->$ref(@_) };
  4         15  
402 4         17 $self->{__handlerIsOurMethod} = 1; # See callHandler().
403             }
404              
405 52         233 return $handler;
406             }
407              
408             sub makeHandlerFromRef {
409 6     6 0 13 my ($self, $ref, $method) = @_;
410 6     6   30 return sub { $ref->$method(@_) };
  6         26  
411             }
412              
413             sub makeHandlerFromClass {
414 0     0 0 0 my ($self, $class, $method) = @_;
415 0     0   0 return sub { $class->$method(@_) };
  0         0  
416             }
417              
418             sub bestContentType {
419 6     6 1 8 my ($self, @types) = @_;
420 6   50     14 return ($self->simpleContentNegotiation(@types))[0] || '*/*';
421             }
422              
423             # We don't support the full range of content negtiation because a) it's
424             # overkill and b) it makes it hard to specify the hooks cleanly, also see (a).
425             sub simpleContentNegotiation {
426 6     6 1 11 my ($self, @types) = @_;
427 6         11 my @accept_types = $self->getContentPrefs();
428 6     36   23 my $score = sub { $self->scoreType(shift, @accept_types) };
  36         68  
429 6         20 return sort {$score->($b) <=> $score->($a)} @types;
  18         28  
430             }
431              
432             # The pattern matching stuff was stolen from CGI.pm
433             sub scoreType {
434 36     36 1 84 my ($self, $type, @accept_types) = @_;
435 36         41 my $score = scalar(@accept_types);
436 36         43 for my $accept_type (@accept_types) {
437 186 100       391 return $score if $type eq $accept_type;
438 160         109 my $pat;
439 160         641 ($pat = $accept_type) =~ s/([^\w*])/\\$1/g; # escape meta characters
440 160         207 $pat =~ s/\*/.*/g; # turn it into a pattern
441 160 100       1146 return $score if $type =~ /$pat/;
442 150         223 $score--;
443             }
444 0         0 return 0;
445             }
446              
447             # Basic idea stolen from CGI.pm. Its semantics made it hard to pull out the
448             # information I wanted without a lot of trickery, so I improved upon the
449             # original. Same with libwww's HTTP::Negotiate algorithim, it's also hard to
450             # make go with what we want.
451             sub getContentPrefs {
452 6     6 1 7 my $self = shift;
453 6         5 my $default_weight = 1;
454 6         7 my @prefs;
455              
456             # Parse the Accept header, and save type name, score, and position.
457 6         11 my @accept_types = split /,/, $self->getAcceptHeader();
458 6         243 my $order = 0;
459 6         7 for my $accept_type (@accept_types) {
460 42         94 my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
461 42         120 my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
462 42 50       70 next unless $name;
463 42         95 push @prefs, { name => $name, order => $order++};
464 42 100       58 if (defined $weight) {
465 18         43 $prefs[-1]->{score} = $weight;
466             } else {
467 24         39 $prefs[-1]->{score} = $default_weight;
468 24         35 $default_weight -= 0.001;
469             }
470             }
471              
472             # Sort the types by score, subscore by order, and pull out just the name
473 6 50       22 @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
  42         84  
  72         166  
474             $a->{order} <=> $b->{order}} @prefs;
475 6         31 return @prefs, '*/*'; # Allows allow for */*
476             }
477              
478             sub getAcceptHeader {
479 6     6 1 7 my $self = shift;
480 6   50     9 return $self->query->http('accept') || "";
481             }
482              
483             # List _getLastRegexMatches(void)
484             #
485             # Returns a list of all the paren matches in the last regular expression who's
486             # matches were saved with _saveLastRegexMatches().
487             sub _getLastRegexMatches {
488 54     54   64 my $self = shift;
489 54   100     158 my $matches = $self->{__lastRegexMatches} || [];
490 54         189 return @$matches;
491             }
492              
493             # ArrayRef _setLastRegexMatches(void)
494             #
495             # Grabs the values of $1, $2, etc. as set by the last regular expression to run
496             # in the current dyanamic scope. This of course exploits that $1, $2, etc. and
497             # @+ are dynamically scoped. A reference to an array is returned where the
498             # array values are $1, $2, $3, etc. _getLastRegexMatches() can also be used to
499             # retrieve the values saved by this method.
500             sub _setLastRegexMatches {
501 49     49   65 my $self = shift;
502 2     2   5851 no strict 'refs'; # For the $$_ symbolic reference below.
  2         4  
  2         228  
503 49         308 my @matches = map $$_, (1 .. scalar(@+)-1); # See "perlvar" for @+.
504 49         140 $self->{__lastRegexMatches} = \@matches;
505             }
506              
507             1;
508             __END__