package CGI::Application::Plugin::REST;
use warnings;
use strict;
use Carp;
use base 'Exporter';
our @EXPORT = qw/ REST_error REST_route REST_media_type /;
# remember to keep version number in sync with the POD below
our $VERSION = '0.8';
# plug in to CGI::Application and setup our callbacks.
sub import{
my $caller = scalar(caller);
$caller->add_callback('init',
'CGI::Application::Plugin::REST::REST_init');
$caller->add_callback('prerun',
'CGI::Application::Plugin::REST::REST_dispatch');
goto &Exporter::import;
}
# REST_init
# Set up our variables
#
sub REST_init {
my ($self) = @_;
$self->{REST_dispatch_table} = {};
$self->{REST_my_media_type} = undef;
}
# REST_dispatch
# A cgiapp_prerun hook that maps requests to the right functions
#
sub REST_dispatch {
my ($self, $run_mode) = @_;
my $q = $self->query;
# Is this a REST run_mode? Yes then wrap the whole thing up in an eval
if (exists($self->{REST_dispatch_table}->{$run_mode})) { eval {
my $rest_run_mode = $self->{REST_dispatch_table}->{$run_mode};
# If so, create a dummy real run_mode for it (or supress an existing
# one.) This is becuse we run fro cgiapp_prerun() which wants to
# return to a real run_mode.
$self->run_modes($run_mode => sub {});
# Is the request method (GET, POST) valid for our REST run_mode?
my $request_method = $q->request_method;
if (defined($request_method)
&& exists($rest_run_mode->{$request_method})) {
my $dispatch = $rest_run_mode->{$request_method};
# Get the preferred MIME media type. Other HTTP verbs than the
# ones below (and DELETE) are not covered. Should they be?
my $media_type = undef;
if ($request_method eq 'GET' || $request_method eq 'HEAD') {
my $quality = 0.000;
foreach my $type (keys %$dispatch) {
my $temp_quality = $q->Accept($type);
if ($temp_quality > $quality) {
$quality = $temp_quality;
$media_type = $type;
}
}
}
elsif ($request_method eq 'POST' || $request_method eq 'PUT') {
$media_type = $q->content_type;
}
$self->{REST_my_media_type} = $media_type;
# Is the MIME media type valid for our REST run_mode? DELETE
# doesn't care about the media type so skip check in that case.
if ((defined($media_type) && exists($dispatch->{$media_type})) ||
$request_method eq 'DELETE') {
# Get the function to call. The rest of the array is the
# arguments we want to give to that function...
my @args = @{$dispatch->{$media_type}};
my $function = shift @args;
# ...which we get from the CGI parameters.
my $params;
foreach my $arg (@args) {
$params->{$arg} = $q->param($arg) || '';
}
# Try and run the method passing it a hashref of the arguments.
if (my $sub = $self->can($function)) {
no strict 'refs';
$self->run_modes(
$run_mode => sub { return $sub->($self, $params) } );
}
# We couldn't find or run the specified method.
else {
$self->REST_error('403', "Function doesn't exist");
}
}
# We didn't get an acceptable MIME media type.
else {
$self->REST_error('415', 'Unsupported media type');
}
}
# We didn't get an acceptable request method.
else {
$self->REST_error('405', 'Method not allowed');
}
}};
# trap any errors and pass them on to the error mode.
if ($@) {
REST_error('500', 'Application error');
my $error = $@;
$self->call_hook('error', $error);
if (my $em = $self->error_mode) {
$self->$em( $error );
} else {
croak("Error executing REST run mode '$run_mode': $error");
}
}
}
# REST_error
# prepare an error message
#
sub REST_error {
my ($self, $code, $msg) = @_;
$self->header_add(-status => "$code $msg");
die "$code $msg\n";
}
# REST_media_type
# Return the prefered MIME media type
#
sub REST_media_type {
my ($self) = @_;
return $self->{REST_my_media_type};
}
# REST_route
# Add an entry to the dispatch table
#
sub REST_route {
my $self = shift;
my %params = (
RUN_MODE => $self->start_mode,
REQUEST_METHOD => 'GET',
MEDIA_TYPES => ['*/*'],
FUNCTION => [$self->start_mode()],
@_,
);
foreach my $type (@{$params{MEDIA_TYPES}}) {
$self->{REST_dispatch_table}->{$params{RUN_MODE}}->
{$params{REQUEST_METHOD}}->{$type} = $params{FUNCTION};
}
}
1;
####
=head1 NAME
CGI::Application::Plugin::REST - Helps implement RESTful architecture in CGI applications
=head1 VERSION
This documentation refers to CGI::Application::Plugin::REST version 0.8
=head1 SYNOPSIS
in your CGI::Application derived module:
use CGI::Application::Plugin::REST;
sub setup { # or cgiapp_init
$self->REST_route(
RUN_MODE => 'widgets',
REQUEST_METHOD => 'GET',
MEDIA_TYPES => ['application/xhtml+xml',
'text/html',
'text/plain',
],
FUNCTION => ['get_widget',
'product_number',
],
);
$self->REST_route(
RUN_MODE => 'widgets',
REQUEST_METHOD => 'POST',
MEDIA_TYPES => ['x-application/widget-descriptions'],
FUNCTION => ['add_widget'],
);
$self->REST_route(
RUN_MODE => 'widgets',
REQUEST_METHOD => 'PUT',
MEDIA_TYPES => ['x-application/widget-descriptions',],
FUNCTION => ['update_widget',
'product_number',
],
);
$self->REST_route(
RUN_MODE => 'widgets',
REQUEST_METHOD => 'DELETE',
FUNCTION => ['remove_widget',
'product_number',
],
);
}
sub get_widget {
my ($self, $params) = @_;
unless (my_validation_function($params->{product_number})) {
$self->REST_error('404', 'Invalid product_number');
}
my $widget = $widgets[$params->{product_number}];
if ($self->REST_media_type eq 'text/plain') {
return $self->plain_output($widget);
}
return $self->fancy_output($widget);
}
etc.
A typical URI might look like:
http://www.example.com/index.cgi/widgets/product_number=12455
=head1 DESCRIPTION
REST stands for REpresentational State Transfer. It is an architecture for
web applications that tries to leverage the existing infrastructure of the
World Wide Web such as URIs. MIME media types and HTTP instead of building up
protocols and functions on top of them.
If you use L, this plugin will help you create a RESTful
(that's the term for "using REST") architecture by abstracting out a lot of
the busy work needed to make it happen.
=head1 METHODS
=over 4
=item B
This is the main function imported by the plugin. You give it a hashref of
options which will be used to create a dispatch table which will match URIs to
functions in your L derived module via a hook in
I. The options are:
=over 4
=item * I
Like a L run mode this is a part of a URL (if you are using
the path_info variant of C or a CGI parameter
(if you are not) which will be mapped to one or more functions. It should
not be a 'real' run mode as specified in C because
this package will create a stub function to handle it and then call the
FUNCTION specified below.
Defaults to your start run mode as specified by C.
=item * I
An HTTP verb. 'DELETE', 'GET', 'HEAD', 'POST', and 'PUT' are the only ones
which are treated specially but these are typically, all you need for a
RESTful web service.
Defaults to 'GET'.
=item * I
An arrayref of MIME media types which we want to accept as input or output for
a particular function. This plugin will take care of determining which is
the most suitable type based on the C HTTP header (for POST and PUT)
or the C HTTP header (for GET and HEAD.) DELETE doesn't care
about MIME media types.
Defaults to '*/*' which means any type.
=item * I
An arrayref. The first element is a function to be called. It should be a
method in your L derived module. The other elements are
keys in a hashref whose values are the equivalently named CGI parameters sent
to the script and passed to that function. It is your job to return output
from the function with the proper MIME media type and HTTP status code.
If the function dies at any point, it will be trapped and your applications
error run mode as defined by C will be called.
Defaults to the function handled by your start run mode as specified by
C with no additional arguments.
=back
=item B
This is a helper function which by default takes two arguments, an HTTP status
code and an error message. It adds an C HTTP header to the output and
then Cs with the code and message. This in turn will be trapped and
your applications error run mode as defined by C
will be called.
You can override this method in your application if you want different behavior,
=item B
This is a helper function that just returns the preferred MIME media type for
input or output or C if it hasn't been specified.
=back
=head1 DIAGNOSTICS
As well as the calls you make, C will be called by the package
itself in certain circumstances. Here is a list along with status codes and
messages.
=over 4
=item * 403 Function doesn't exist
The I that you wanted to call from C for this run_mode
doesn't exist in your application.
=item * 405 Method Not Allowed
The I being used to invoke this run_mode isn't defined by
C.
=item * 415 Unsupported media type
The requested MIME media type is not one of the I defined
for this run_mode by C.
=item * 500 Application error
The I that has been called for this run_mode C'd somewhere.
=head1 BUGS
This package has not been tested with modperl, fastCGI or indeed anything other
than a standard CGI environment.
You ought to be able to use URIs like this:
http://www.example.com/index.cgi/widgets/12455
You ought to be able to override the preferred MIME type with i.e. a CGI
parameter.
Maybe I should have just built upon L adding any
missing bits rather than creating a brand new module.
=head1 SEE ALSO
=over 4
=item * L:
The application framework this module plugs into.
=item * L:
A L subclass that also does URI based function dispatch and a
lot more. (Though it is currently doesn't handle MIME media types.) If you
find you are running into limitations with this module, you should look at
L.
=item * L:
Roy Fieldings' doctoral thesis in which the term REST was first defined.
=item * L
"The Restful Web" columns by Joe Gregorio have been very useful to me in
understanding the ins and outs of REST.
=back
=head1 AUTHOR
Jaldhar H. Vyas Ejaldhar@braincells.comE
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2006, Consolidated Braincells Inc. All rights reserved,
This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.