File Coverage

blib/lib/CatalystX/CRUD/REST.pm
Criterion Covered Total %
statement 125 161 77.6
branch 45 78 57.6
condition 17 28 60.7
subroutine 29 39 74.3
pod 18 18 100.0
total 234 324 72.2


line stmt bran cond sub pod time code
1             package CatalystX::CRUD::REST;
2 5     5   220629 use strict;
  5         14  
  5         157  
3 5     5   54 use warnings;
  5         18  
  5         159  
4 5     5   34 use base qw( CatalystX::CRUD::Controller );
  5         30  
  5         617  
5 5     5   49 use Carp;
  5         14  
  5         320  
6 5     5   43 use MRO::Compat;
  5         15  
  5         171  
7 5     5   37 use mro 'c3';
  5         13  
  5         133  
8 5     5   179 use Data::Dump qw( dump );
  5         13  
  5         281  
9 5     5   32 use Try::Tiny;
  5         18  
  5         767  
10              
11             __PACKAGE__->mk_accessors(qw( enable_rpc_compat ));
12             __PACKAGE__->config( enable_rpc_compat => 0 );
13              
14             our $VERSION = '0.58';
15              
16             #warn "REST VERSION = $VERSION";
17              
18             =head1 NAME
19              
20             CatalystX::CRUD::REST - RESTful CRUD controller
21              
22             =head1 SYNOPSIS
23              
24             # create a controller
25             package MyApp::Controller::Foo;
26             use strict;
27             use base qw( CatalystX::CRUD::REST );
28             use MyForm::Foo;
29            
30             __PACKAGE__->config(
31             form_class => 'MyForm::Foo',
32             init_form => 'init_with_foo',
33             init_object => 'foo_from_form',
34             default_template => 'path/to/foo/edit.tt',
35             model_name => 'Foo',
36             primary_key => 'id',
37             view_on_single_result => 0,
38             page_size => 50,
39             enable_rpc_compat => 0,
40             );
41            
42             1;
43            
44             # now you can manage Foo objects using your MyForm::Foo form class
45             # with URIs at:
46             # foo/<pk>
47             # and use the HTTP method name to indicate the appropriate action.
48             # POST /foo -> create new record
49             # GET /foo -> list all records
50             # PUT /foo/<pk> -> update record
51             # DELETE /foo/<pk> -> delete record
52             # GET /foo/<pk> -> view record
53             # GET /foo/<pk>/edit_form -> edit record form
54             # GET /foo/create_form -> create record form
55              
56            
57             =head1 DESCRIPTION
58              
59             CatalystX::CRUD::REST is a subclass of CatalystX::CRUD::Controller.
60             Instead of calling RPC-style URIs, the REST API uses the HTTP method name
61             to indicate the action to be taken.
62              
63             See CatalystX::CRUD::Controller for more details on configuration.
64              
65             The REST API is designed with identical configuration options as the RPC-style
66             Controller API, so that you can simply change your @ISA chain and enable
67             REST features for your application.
68              
69             B<IMPORTANT:> If you are using a CatalystX::CRUD::REST subclass
70             in your application, it is important to add the following to your main
71             MyApp.pm file, just after the setup() call:
72              
73             __PACKAGE__->setup();
74            
75             # add these 3 lines
76             use MRO::Compat;
77             use mro 'c3';
78             Class::C3::initialize();
79              
80             This is required for Class::C3 to resolve the inheritance chain correctly,
81             especially in the case where your app is subclassing more than one
82             CatalystX::CRUD::Controller::* class.
83              
84             =cut
85              
86             =head1 METHODS
87              
88             =head2 edit_form
89              
90             Acts just like edit() in base Controller class, but with a RESTful name.
91              
92             =head2 create_form
93              
94             Acts just like create() in base Controller class, but with a RESTful name.
95              
96             =cut
97              
98             sub create_form : Path('create_form') {
99 0     0   0 my ( $self, $c ) = @_;
100 0         0 $self->create($c);
101 5     5   85 }
  5         12  
  5         35  
102              
103             sub edit_form : PathPart Chained('fetch') Args(0) {
104 0     0 1 0 my ( $self, $c ) = @_;
105 0         0 return $self->edit($c);
106 5     5   63414 }
  5         35  
  5         44  
107              
108             =head2 create
109              
110             Redirects to create_form().
111              
112             =cut
113              
114             # no-op to undo the superclass Local attr
115             sub create {
116 0     0 1 0 shift->next::method(@_);
117             }
118              
119             sub _rest_create : Path('create') {
120 2     2   2292 my ( $self, $c ) = @_;
121 2         9 $c->res->redirect(
122             $c->uri_for( $self->action_for('create_form'), $c->req->params ) );
123 5     5   5592 }
  5         13  
  5         28  
124              
125             =head2 rest
126              
127             Attribute: Path Args
128              
129             Calls the appropriate method based on the HTTP method name.
130              
131             =cut
132              
133             my %http_method_map = (
134             'POST' => 'save',
135             'PUT' => 'save',
136             'DELETE' => 'rm',
137             'GET' => 'view'
138             );
139              
140             my %rpc_methods
141             = map { $_ => 1 } qw( create read update delete edit save rm view );
142             my %related_methods
143             = map { $_ => 1 } qw( add remove list_related view_related view );
144              
145             sub rest : Path {
146 25     25 1 28911 my ( $self, $c, @arg ) = @_;
147              
148 25         87 my $method = $self->req_method($c);
149              
150 25 50       1543 if ( !exists $http_method_map{$method} ) {
151 0         0 $c->res->status(400);
152 0         0 $c->res->body("Bad HTTP request for method $method");
153 0         0 return;
154             }
155              
156 25 50       80 $c->log->debug( "rpc compat mode = " . $self->enable_rpc_compat )
157             if $c->debug;
158 25 50       106 $c->log->debug( "rest args : " . dump \@arg ) if $c->debug;
159 25 50       92 $c->log->debug( "rest action->name=" . $c->action->name ) if $c->debug;
160              
161 25         86 my $n = scalar @arg;
162 25 100       71 if ( $n <= 2 ) {
    100          
163 17         59 $self->_rest( $c, @arg );
164             }
165             elsif ( $n <= 4 ) {
166 6         27 $self->_rest_related( $c, @arg );
167             }
168             else {
169 2         7 $self->_set_status_404($c);
170 2         129 return;
171             }
172 5     5   6256 }
  5         13  
  5         40  
173              
174             =head2 default
175              
176             Attribute: Private
177              
178             Returns 404 status. In theory, this action is never reached,
179             and if it is, will log an error. It exists only for debugging
180             purposes.
181              
182             =cut
183              
184             sub default : Private {
185 0     0 1 0 my ( $self, $c, @arg ) = @_;
186 0         0 $c->log->error("default method reached");
187 0         0 $self->_set_status_404($c);
188 5     5   5206 }
  5         25  
  5         37  
189              
190             sub _set_status_404 {
191 5     5   14 my ( $self, $c ) = @_;
192 5         12 $c->res->status(404);
193 5         824 $c->res->body('Resource not found');
194             }
195              
196             sub _rest_related {
197 6     6   22 my ( $self, $c, @arg ) = @_;
198 6         21 my ( $oid, $rel_name, $fval, $rpc ) = @arg;
199              
200 6 50       14 $c->log->debug("rest_related OID: $oid") if $c->debug;
201 6 50       25 $c->log->debug("rest_related rel_name=$rel_name fval=$fval rpc=$rpc")
202             if $c->debug;
203              
204 6 100       23 if ($rpc) {
205 4 100 100     17 if ( !$self->enable_rpc_compat or !exists $related_methods{$rpc} ) {
206 2 50       283 $c->log->debug("unmapped rpc:$rpc") if $c->debug;
207 2         27 $self->_set_status_404($c);
208 2         116 return;
209             }
210             }
211              
212 4         303 my $http_method = $self->req_method($c);
213 4         263 my $dispatch_method = 'related';
214 4         5 my $rpc_method;
215 4 100 66     28 if ($rpc) {
    100          
    50          
    0          
216 2         5 $rpc_method = $rpc;
217              
218             # mimic PathPart
219 2 50       7 if ( $rpc_method eq 'view' ) {
220 0         0 $rpc_method = 'view_related';
221             }
222             }
223             elsif ( $http_method eq 'POST' or $http_method eq 'PUT' ) {
224 1         2 $rpc_method = 'add';
225             }
226             elsif ( $http_method eq 'DELETE' ) {
227 1         5 $rpc_method = 'remove';
228             }
229             elsif ( $http_method eq 'GET' ) {
230 0 0       0 if ( $fval eq 'list' ) {
    0          
231 0         0 $rpc_method = 'list_related';
232 0         0 $dispatch_method = 'fetch_related';
233             }
234             elsif ($fval) {
235 0         0 $rpc_method = 'view_related';
236             }
237             else {
238 0         0 $c->res->status(400);
239 0         0 $c->res->body("Bad HTTP request for method $http_method");
240 0         0 return;
241             }
242             }
243             else {
244              
245             # related() will screen for GET based on config
246             # but we do not allow that for REST
247 0         0 $c->res->status(400);
248 0         0 $c->res->body("Bad HTTP request for method $http_method");
249 0         0 return;
250             }
251 4 50       12 $c->log->debug("rest dispatch: $dispatch_method( $rel_name, $fval )")
252             if $c->debug;
253 4         40 $self->$dispatch_method( $c, $rel_name, $fval );
254 4         330 $self->_call_rpc_method_as_action( $c, $rpc_method, $oid );
255             }
256              
257             sub _rest {
258 17     17   31 my ( $self, $c, @arg ) = @_;
259              
260             # default oid to emptry string and not 0
261             # so we can test for length and
262             # still have a false value for fetch()
263 17   100     61 my $oid = shift @arg || '';
264 17         23 my $rpc = shift @arg;
265              
266 17         30 my $http_method = $self->req_method($c);
267 17 50 0     969 $c->log->debug(
268             sprintf(
269             "rest OID:%s rpc:%s http:%s",
270             $oid, ( $rpc || '[undef]' ), $http_method
271             )
272             ) if $c->debug;
273              
274 17 100 100     139 if ( length $oid and $rpc ) {
275 4 100 66     33 if ( $self->enable_rpc_compat and exists $rpc_methods{$rpc} ) {
    50 33        
    50 33        
276              
277             # do nothing - logic below
278             }
279             elsif ( $self->enable_rpc_compat and $http_method eq 'GET' ) {
280              
281             # same logic as !length $oid below:
282             # assume that $rpc is a relationship name
283             # and a 'list' is being requested
284 0 0       0 $c->log->debug(
285             "GET request with OID and unknown rpc; assuming 'list_related'"
286             ) if $c->debug;
287 0         0 $self->fetch_related( $c, $rpc );
288 0         0 $rpc = 'list_related';
289             }
290             elsif ( !$self->enable_rpc_compat or !exists $rpc_methods{$rpc} ) {
291 1         415 $self->_set_status_404($c);
292 1         75 return;
293             }
294             }
295              
296 16 100 66     509 if ( !length $oid and $http_method eq 'GET' ) {
297 4 50       13 $c->log->debug("GET request with no OID") if $c->debug;
298 4         98 $c->action->name('list');
299 4         233 $c->action->reverse( join( '/', $c->action->namespace, 'list' ) );
300 4         317 return $self->list($c);
301             }
302              
303             # what RPC-style method to call
304 12 100       47 my $rpc_method = defined($rpc) ? $rpc : $http_method_map{$http_method};
305              
306             # backwards compat naming for RPC style
307 12 50       51 if ( $rpc_method =~ m/^(create|edit)$/ ) {
308 0         0 $rpc_method .= '_form';
309             }
310              
311 12 50       79 if ( !$self->can($rpc_method) ) {
312 0         0 $c->log->warn("no such rpc method in class: $rpc_method");
313             }
314              
315 12         54 $self->_call_rpc_method_as_action( $c, $rpc_method, $oid );
316             }
317              
318             sub _call_rpc_method_as_action {
319 16     16   32 my ( $self, $c, $rpc_method, $oid ) = @_;
320              
321 16         25 my $break_chain = 0;
322             try {
323 16     16   773 $self->fetch( $c, $oid );
324             }
325             catch {
326 0 0   0   0 $c->log->debug( 'caught exception, res->status==' . $c->res->status )
327             if $c->debug;
328 0 0       0 if ( $c->res->status == 404 ) {
329 0 0       0 $c->log->debug('break chain with 404') if $c->debug;
330 0         0 $break_chain = 1;
331             }
332 16         125 };
333              
334 16 50       1038 return if $break_chain;
335              
336 16         44 my $http_method = $self->req_method($c);
337              
338 16 50       972 $c->log->debug("rpc: $http_method -> $rpc_method") if $c->debug;
339              
340             # so View::TT (others?) auto-template-deduction works just like RPC style
341 16         369 $c->action->name($rpc_method);
342 16         992 $c->action->reverse( join( '/', $c->action->namespace, $rpc_method ) );
343              
344 16         1289 return $self->$rpc_method($c);
345             }
346              
347             =head2 req_method( I<context> )
348              
349             Internal method. Returns the HTTP method name, allowing
350             POST to serve as a tunnel when the C<_http_method> or
351             C<x-tunneled-method> param is present.
352             Since most browsers do not support PUT or DELETE
353             HTTP methods, you can use the special param to tunnel
354             the desired HTTP method and then POST instead.
355              
356             =cut
357              
358             my @tunnel_param_names = qw( x-tunneled-method _http_method );
359              
360             sub req_method {
361 62     62 1 118 my ( $self, $c ) = @_;
362 62 100       130 if ( uc( $c->req->method ) eq 'POST' ) {
363 32         1822 for my $name (@tunnel_param_names) {
364 58 100       2213 if ( exists $c->req->params->{$name} ) {
365 15         1098 return uc( $c->req->params->{$name} );
366             }
367             }
368             }
369 47         2952 return uc( $c->req->method );
370             }
371              
372             =head2 edit( I<context> )
373              
374             Overrides base method to disable chaining.
375              
376             =cut
377              
378 0     0 1 0 sub edit { shift->next::method(@_) }
379              
380             =head2 view( I<context> )
381              
382             Overrides base method to disable chaining.
383              
384             =cut
385              
386 6     6 1 32 sub view { shift->next::method(@_) }
387              
388             =head2 save( I<context> )
389              
390             Overrides base method to disable chaining.
391              
392             =cut
393              
394 3     3 1 14 sub save { shift->next::method(@_) }
395              
396             =head2 rm( I<context> )
397              
398             Overrides base method to disable chaining.
399              
400             =cut
401              
402 3     3 1 11 sub rm { shift->next::method(@_) }
403              
404             =head2 remove( I<context> )
405              
406             Overrides base method to disable chaining.
407              
408             =cut
409              
410 2     2 1 20 sub remove { shift->next::method(@_) }
411              
412             =head2 add( I<context> )
413              
414             Overrides base method to disable chaining.
415              
416             =cut
417              
418 2     2 1 14 sub add { shift->next::method(@_) }
419              
420             =head2 view_related( I<context> )
421              
422             Overrides base method to disable chaining.
423              
424             =cut
425              
426 0     0 1 0 sub view_related { shift->next::method(@_) }
427              
428             =head2 list_related( I<context> )
429              
430             Overrides base method to disable chaining.
431              
432             =cut
433              
434 0     0 1 0 sub list_related { shift->next::method(@_) }
435              
436             =head2 delete( I<context> )
437              
438             Overrides base method to disable chaining.
439              
440             =cut
441              
442 2     2 1 19 sub delete { shift->next::method(@_) }
443              
444             =head2 read( I<context> )
445              
446             Overrides base method to disable chaining.
447              
448             =cut
449              
450 0     0 1 0 sub read { shift->next::method(@_) }
451              
452             =head2 update( I<context> )
453              
454             Overrides base method to disable chaining.
455              
456             =cut
457              
458 0     0 1 0 sub update { shift->next::method(@_) }
459              
460             =head2 postcommit( I<context>, I<object> )
461              
462             Overrides base method to redirect to REST-style URL.
463              
464             =cut
465              
466             sub postcommit {
467 6     6 1 79 my ( $self, $c, $o ) = @_;
468 6         37 my $id = $self->make_primary_key_string($o);
469              
470 6 50 33     21 unless ( defined $c->res->location and length $c->res->location ) {
471              
472 6 100       546 if ( $c->action->name eq 'rm' ) {
473 1         62 $c->response->redirect( $c->uri_for('') );
474             }
475             else {
476 5         283 $c->response->redirect( $c->uri_for( '', $id ) );
477             }
478              
479             }
480              
481 6         2733 $self->next::method( $c, $o );
482             }
483              
484             =head2 new
485              
486             Overrides base method just to call next::method to ensure
487             config() gets merged correctly.
488              
489             =cut
490              
491             sub new {
492 4     4 1 1276 my ( $class, $app_class, $args ) = @_;
493 4         30 return $class->next::method( $app_class, $args );
494             }
495              
496             1;
497              
498             __END__
499              
500             =head1 AUTHOR
501              
502             Peter Karman, C<< <perl at peknet.com> >>
503              
504             =head1 BUGS
505              
506             Please report any bugs or feature requests to
507             C<bug-catalystx-crud at rt.cpan.org>, or through the web interface at
508             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CatalystX-CRUD>.
509             I will be notified, and then you'll automatically be notified of progress on
510             your bug as I make changes.
511              
512             =head1 SUPPORT
513              
514             You can find documentation for this module with the perldoc command.
515              
516             perldoc CatalystX::CRUD
517              
518             You can also look for information at:
519              
520             =over 4
521              
522             =item * Mailing List
523              
524             L<https://groups.google.com/forum/#!forum/catalystxcrud>
525              
526             =item * AnnoCPAN: Annotated CPAN documentation
527              
528             L<http://annocpan.org/dist/CatalystX-CRUD>
529              
530             =item * CPAN Ratings
531              
532             L<http://cpanratings.perl.org/d/CatalystX-CRUD>
533              
534             =item * RT: CPAN's request tracker
535              
536             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CatalystX-CRUD>
537              
538             =item * Search CPAN
539              
540             L<http://search.cpan.org/dist/CatalystX-CRUD>
541              
542             =back
543              
544             =head1 COPYRIGHT & LICENSE
545              
546             Copyright 2008 Peter Karman, all rights reserved.
547              
548             This program is free software; you can redistribute it and/or modify it
549             under the same terms as Perl itself.
550              
551             =cut
552