package CatalystX::CRUD::REST; use strict; use warnings; use base qw( CatalystX::CRUD::Controller ); use Carp; use MRO::Compat; use mro 'c3'; use Data::Dump qw( dump ); __PACKAGE__->mk_accessors(qw( enable_rpc_compat )); __PACKAGE__->config( enable_rpc_compat => 0 ); our $VERSION = '0.45'; #warn "REST VERSION = $VERSION"; =head1 NAME CatalystX::CRUD::REST - RESTful CRUD controller =head1 SYNOPSIS # create a controller package MyApp::Controller::Foo; use strict; use base qw( CatalystX::CRUD::REST ); use MyForm::Foo; __PACKAGE__->config( form_class => 'MyForm::Foo', init_form => 'init_with_foo', init_object => 'foo_from_form', default_template => 'path/to/foo/edit.tt', model_name => 'Foo', primary_key => 'id', view_on_single_result => 0, page_size => 50, enable_rpc_compat => 0, ); 1; # now you can manage Foo objects using your MyForm::Foo form class # with URIs at: # foo/ # and use the HTTP method name to indicate the appropriate action. # POST /foo -> create new record # GET /foo -> list all records # PUT /foo/ -> update record # DELETE /foo/ -> delete record # GET /foo/ -> view record # GET /foo//edit_form -> edit record form # GET /foo/create_form -> create record form =head1 DESCRIPTION CatalystX::CRUD::REST is a subclass of CatalystX::CRUD::Controller. Instead of calling RPC-style URIs, the REST API uses the HTTP method name to indicate the action to be taken. See CatalystX::CRUD::Controller for more details on configuration. The REST API is designed with identical configuration options as the RPC-style Controller API, so that you can simply change your @ISA chain and enable REST features for your application. B If you are using a CatalystX::CRUD::REST subclass in your application, it is important to add the following to your main MyApp.pm file, just after the setup() call: __PACKAGE__->setup(); # add these 2 lines use MRO::Compat; use mro 'c3'; Class::C3::initialize(); This is required for Class::C3 to resolve the inheritance chain correctly, especially in the case where your app is subclassing more than one CatalystX::CRUD::Controller::* class. =cut =head1 METHODS =head2 edit_form Acts just like edit() in base Controller class, but with a RESTful name. =head2 create_form Acts just like create() in base Controller class, but with a RESTful name. =cut sub create_form : Path('create_form') { my ( $self, $c ) = @_; $self->create($c); } sub edit_form : PathPart Chained('fetch') Args(0) { my ( $self, $c ) = @_; return $self->edit($c); } =head2 create Redirects to create_form(). =cut # no-op to undo the superclass Local attr sub create { shift->next::method(@_); } sub _rest_create : Path('create') { my ( $self, $c ) = @_; $c->res->redirect( $c->uri_for( $self->action_for('create_form'), $c->req->params ) ); } =head2 rest Attribute: Path Args Calls the appropriate method based on the HTTP method name. =cut my %http_method_map = ( 'POST' => 'save', 'PUT' => 'save', 'DELETE' => 'rm', 'GET' => 'view' ); my %rpc_methods = map { $_ => 1 } qw( create read update delete edit save rm view ); my %related_methods = map { $_ => 1 } qw( add remove ); sub rest : Path { my ( $self, $c, @arg ) = @_; my $method = $self->req_method($c); if ( !exists $http_method_map{$method} ) { $c->res->status(400); $c->res->body("Bad HTTP request for method $method"); return; } $c->log->debug( "rpc compat mode = " . $self->enable_rpc_compat ) if $c->debug; $c->log->debug( "rest args : " . dump \@arg ) if $c->debug; my $n = scalar @arg; if ( $n <= 2 ) { $self->_rest( $c, @arg ); } elsif ( $n <= 4 ) { $self->_rest_related( $c, @arg ); } else { $self->_set_status_404($c); return; } } =head2 default Attribute: Private Returns 404 status. In theory, this action is never reached, and if it is, will log an error. It exists only for debugging purposes. =cut sub default : Private { my ( $self, $c, @arg ) = @_; $c->log->error("default method reached"); $self->_set_status_404($c); } sub _set_status_404 { my ( $self, $c ) = @_; $c->res->status(404); $c->res->body('Resource not found'); } sub _rest_related { my ( $self, $c, @arg ) = @_; my ( $oid, $rel_name, $fval, $rpc ) = @arg; $c->log->debug("rest_related OID: $oid") if $c->debug; if ($rpc) { if ( !$self->enable_rpc_compat or !exists $related_methods{$rpc} ) { $self->_set_status_404($c); return; } } my $http_method = $self->req_method($c); $self->related( $c, $rel_name, $fval ); my $rpc_method; if ($rpc) { $rpc_method = $rpc; } elsif ( $http_method eq 'POST' or $http_method eq 'PUT' ) { $rpc_method = 'add'; } elsif ( $http_method eq 'DELETE' ) { $rpc_method = 'remove'; } else { # related() will screen for GET based on config # but we do not allow that for REST $c->res->status(400); $c->res->body("Bad HTTP request for method $http_method"); return; } $self->_call_rpc_method_as_action( $c, $rpc_method, $oid ); } sub _rest { my ( $self, $c, @arg ) = @_; # default oid to emptry string and not 0 # so we can test for length and # still have a false value for fetch() my $oid = shift @arg || ''; my $rpc = shift @arg; $c->log->debug("rest OID: $oid") if $c->debug; if ($rpc) { if ( !$self->enable_rpc_compat or !exists $rpc_methods{$rpc} ) { $self->_set_status_404($c); return; } } my $method = $self->req_method($c); if ( !length $oid && $method eq 'GET' ) { $c->log->debug("GET request with no OID") if $c->debug; $c->action->name('list'); $c->action->reverse( join( '/', $c->action->namespace, 'list' ) ); return $self->list($c); } # what RPC-style method to call my $rpc_method = defined($rpc) ? $rpc : $http_method_map{$method}; # backwards compat naming for RPC style if ( $rpc_method =~ m/^(create|edit)$/ ) { $rpc_method .= '_form'; } $self->_call_rpc_method_as_action( $c, $rpc_method, $oid ); } sub _call_rpc_method_as_action { my ( $self, $c, $rpc_method, $oid ) = @_; $self->fetch( $c, $oid ); my $http_method = $self->req_method($c); $c->log->debug("$http_method -> $rpc_method") if $c->debug; # so View::TT (others?) auto-template-deduction works just like RPC style $c->action->name($rpc_method); $c->action->reverse( join( '/', $c->action->namespace, $rpc_method ) ); return $self->$rpc_method($c); } =head2 req_method( I ) Internal method. Returns the HTTP method name, allowing POST to serve as a tunnel when the C<_http_method> or C param is present. Since most browsers do not support PUT or DELETE HTTP methods, you can use the special param to tunnel the desired HTTP method and then POST instead. =cut my @tunnel_param_names = qw( x-tunneled-method _http_method ); sub req_method { my ( $self, $c ) = @_; if ( uc( $c->req->method ) eq 'POST' ) { for my $name (@tunnel_param_names) { if ( exists $c->req->params->{$name} ) { return uc( $c->req->params->{$name} ); } } } return uc( $c->req->method ); } =head2 edit( I ) Overrides base method to disable chaining. =cut sub edit { shift->next::method(@_) } =head2 view( I ) Overrides base method to disable chaining. =cut sub view { shift->next::method(@_) } =head2 save( I ) Overrides base method to disable chaining. =cut sub save { shift->next::method(@_) } =head2 rm( I ) Overrides base method to disable chaining. =cut sub rm { shift->next::method(@_) } =head2 remove( I ) Overrides base method to disable chaining. =cut sub remove { shift->next::method(@_) } =head2 add( I ) Overrides base method to disable chaining. =cut sub add { shift->next::method(@_) } =head2 delete( I ) Overrides base method to disable chaining. =cut sub delete { shift->next::method(@_) } =head2 read( I ) Overrides base method to disable chaining. =cut sub read { shift->next::method(@_) } =head2 update( I ) Overrides base method to disable chaining. =cut sub update { shift->next::method(@_) } =head2 postcommit( I, I ) Overrides base method to redirect to REST-style URL. =cut sub postcommit { my ( $self, $c, $o ) = @_; my $id = $self->make_primary_key_string($o); unless ( defined $c->res->location and length $c->res->location ) { if ( $c->action->name eq 'rm' ) { $c->response->redirect( $c->uri_for('') ); } else { $c->response->redirect( $c->uri_for( '', $id ) ); } } $self->next::method( $c, $o ); } =head2 new Overrides base method just to call next::method to ensure config() gets merged correctly. =cut sub new { my ( $class, $app_class, $args ) = @_; return $class->next::method( $app_class, $args ); } 1; __END__ =head1 AUTHOR Peter Karman, C<< >> =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc CatalystX::CRUD You can also look for information at: =over 4 =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * RT: CPAN's request tracker L =item * Search CPAN L =back =head1 COPYRIGHT & LICENSE Copyright 2008 Peter Karman, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut