You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
9.9 KiB
389 lines
9.9 KiB
package Lemonldap::NG::Common::PSGI::Router;
|
|
|
|
use Mouse;
|
|
use Lemonldap::NG::Common::PSGI;
|
|
use Lemonldap::NG::Common::PSGI::Constants;
|
|
|
|
our $VERSION = '2.0.3';
|
|
|
|
extends 'Lemonldap::NG::Common::PSGI';
|
|
|
|
# Properties
|
|
has 'routes' => (
|
|
is => 'rw',
|
|
isa => 'HashRef',
|
|
default => sub { { GET => {}, POST => {}, PUT => {}, DELETE => {} } }
|
|
);
|
|
has 'defaultRoute' => ( is => 'rw', default => 'index.html' );
|
|
|
|
# Routes initialization
|
|
|
|
sub addRoute {
|
|
my ( $self, $word, $dest, $methods, $transform ) = (@_);
|
|
$methods ||= [qw(GET POST PUT DELETE)];
|
|
foreach my $method (@$methods) {
|
|
$self->logger->debug("Add $method route:");
|
|
$self->genRoute( $self->routes->{$method}, $word, $dest, $transform );
|
|
}
|
|
return $self;
|
|
}
|
|
|
|
sub genRoute {
|
|
my ( $self, $routes, $word, $dest, $transform ) = @_;
|
|
unless ( ref $routes eq 'HASH' ) {
|
|
$self->logger->error(
|
|
'Conflict detected between 2 extensions, aborting 1 route');
|
|
return;
|
|
}
|
|
if ( ref $word eq 'ARRAY' ) {
|
|
foreach my $w (@$word) {
|
|
$self->genRoute( $routes, $w, $dest, $transform );
|
|
}
|
|
}
|
|
else {
|
|
$dest //= $word;
|
|
if ( $word =~ /^:(.*)$/ ) {
|
|
$routes->{'#'} = $1;
|
|
die "Target required for $word" unless ($dest);
|
|
$word = ':';
|
|
}
|
|
elsif ( $word =~ m#/# ) {
|
|
$word =~ s#^(.*?)/##;
|
|
return $self->genRoute( $routes->{$1}, $word, $dest, $transform );
|
|
}
|
|
else {
|
|
$dest ||= $word;
|
|
}
|
|
if ( $dest =~ /^(.+)\.html$/ ) {
|
|
my $tpl = $1 or die;
|
|
$self->logger->debug("route $dest will use $tpl");
|
|
$routes->{$word} = sub { $self->sendHtml( $_[1], $tpl ) };
|
|
return;
|
|
}
|
|
if ( $transform and ( not ref($dest) or ref($dest) eq 'CODE' ) ) {
|
|
$dest = $transform->($dest);
|
|
}
|
|
if ( my $t = ref $dest ) {
|
|
if ( $t eq 'HASH' ) {
|
|
$routes->{$word} ||= {};
|
|
foreach my $w ( keys %$dest ) {
|
|
$self->genRoute( $routes->{$word}, $w, $dest->{$w},
|
|
$transform );
|
|
}
|
|
return;
|
|
}
|
|
elsif ( $t eq 'ARRAY' ) {
|
|
$routes->{$word} ||= {};
|
|
foreach my $w ( @{$dest} ) {
|
|
$self->genRoute( $routes->{$word}, $w, $transform );
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if ( $routes->{$word} ) {
|
|
eval { $self->logger->warn(qq'Route "$word" redefined'); };
|
|
}
|
|
if ( my $t = ref $dest ) {
|
|
if ( $t eq 'CODE' ) {
|
|
$routes->{$word} = $dest;
|
|
}
|
|
else {
|
|
die "Type $t unauthorized in routes";
|
|
}
|
|
}
|
|
elsif ( $self->can($dest) ) {
|
|
$routes->{$word} = sub { shift; $self->$dest(@_) };
|
|
}
|
|
else {
|
|
die "$dest() isn't a method";
|
|
}
|
|
$self->logger->debug("route $word added");
|
|
}
|
|
}
|
|
|
|
sub handlerAbort {
|
|
my ( $self, $path, $msg ) = @_;
|
|
delete $self->routes->{$path};
|
|
$self->addRoute(
|
|
$path => sub {
|
|
my ( $self, $req ) = @_;
|
|
return $self->sendError( $req, $msg, 500 );
|
|
}
|
|
);
|
|
}
|
|
|
|
# Methods that dispatch requests
|
|
|
|
sub handler {
|
|
my ( $self, $req ) = @_;
|
|
|
|
# Reinitialize configuration message
|
|
$Lemonldap::NG::Common::Conf::msg = '';
|
|
|
|
# Launch reqInit() if exists
|
|
if ( $self->can('reqInit') ) {
|
|
$self->reqInit($req);
|
|
}
|
|
|
|
# Only words are taken in path
|
|
my $last = 0;
|
|
my @path = grep {
|
|
$last = 1 if ( $_ =~ /[^\.\w]/ );
|
|
( $last or /^$/ ? 0 : 1 );
|
|
} split /\//, $req->path();
|
|
$self->logger->debug( "Start routing " . ( $path[0] // 'default route' ) );
|
|
|
|
if ( !@path and $self->defaultRoute ) {
|
|
@path = ( $self->defaultRoute );
|
|
}
|
|
my $res =
|
|
$self->followPath( $req, $self->routes->{ $req->method }, \@path );
|
|
return $res ? $res : $self->sendError( $req, 'Bad request', 400 );
|
|
}
|
|
|
|
sub followPath {
|
|
my ( $self, $req, $routes, $path ) = @_;
|
|
if ( $path->[0] and defined $routes->{ $path->[0] } ) {
|
|
my $w = shift @$path;
|
|
if ( ref( $routes->{$w} ) eq 'CODE' ) {
|
|
return $routes->{$w}->( $self, $req, @$path );
|
|
}
|
|
my $res = $self->followPath( $req, $routes->{$w}, $path );
|
|
return $res if ($res);
|
|
unshift @$path, $w;
|
|
}
|
|
if ( $routes->{':'} ) {
|
|
my $v = shift @$path;
|
|
$req->set_param( $routes->{'#'}, $v );
|
|
if ( ref( $routes->{':'} ) eq 'CODE' ) {
|
|
return $routes->{':'}->( $self, $req, @$path );
|
|
}
|
|
return $self->followPath( $req, $routes->{':'}, $path );
|
|
}
|
|
elsif ( my $sub = $routes->{'*'} ) {
|
|
return $self->$sub( $req, @$path );
|
|
}
|
|
else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
=encoding utf8
|
|
|
|
Lemonldap::NG::Common::PSGI::Router - Base library for REST APIs of Lemonldap::NG.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
package My::PSGI;
|
|
|
|
use base Lemonldap::NG::Common::PSGI::Router;
|
|
|
|
sub init {
|
|
my ($self,$args) = @_;
|
|
# Will be called 1 time during startup
|
|
|
|
# Declare REST routes (could be HTML templates or methods)
|
|
$self->addRoute ( 'index.html', undef, ['GET'] )
|
|
->addRoute ( books => { ':book' => 'booksMethod' }, ['GET', 'POST'] )
|
|
->addRoute ( properties => { '*' => 'propertiesMethod' }, ['GET', 'POST', 'PUT', 'DELETE']);
|
|
|
|
# Default route (ie: PATH_INFO == '/')
|
|
$self->defaultRoute('index.html');
|
|
|
|
# See Lemonldap::NG::Common::PSGI for other options
|
|
|
|
# Return a boolean. If false, then error message has to be stored in
|
|
# $self->error
|
|
return 1;
|
|
}
|
|
|
|
sub booksMethod {
|
|
my ( $self, $req, @otherPathInfo ) = @_;
|
|
my $book = $req->params('book');
|
|
my $method = $req->method;
|
|
...
|
|
$self->sendJSONresponse(...);
|
|
}
|
|
|
|
sub propertiesMethod {
|
|
my ( $self, $property, @otherPathInfo ) = @_;
|
|
my $method = $req->method;
|
|
...
|
|
$self->sendJSONresponse(...);
|
|
}
|
|
|
|
This package could then be called as a CGI, using FastCGI,...
|
|
|
|
#!/usr/bin/env perl
|
|
|
|
use My::PSGI;
|
|
use Plack::Handler::FCGI; # or Plack::Handler::CGI
|
|
|
|
Plack::Handler::FCGI->new->run( My::PSGI->run() );
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This package provides base class for Lemonldap::NG REST API but could be
|
|
used regardless.
|
|
|
|
=head1 METHODS
|
|
|
|
See L<Lemonldap::NG::Common::PSGI> for logging methods, content sending,...
|
|
|
|
=head2 Initialization methods
|
|
|
|
=head3 addRoute ( $word, $dest, $methods )
|
|
|
|
Declare a REST route. Arguments:
|
|
|
|
=over
|
|
|
|
=item $word:
|
|
|
|
the first word of /path/info.
|
|
|
|
=item $dest:
|
|
|
|
string, sub ref or hash ref (see "Route types" below)
|
|
|
|
=item $methods:
|
|
|
|
array ref containing the methods concerned by this route.
|
|
|
|
=back
|
|
|
|
=head4 Route types
|
|
|
|
As seen in "SYNOPSIS", you can declare routes with variable component. $dest
|
|
can be:
|
|
|
|
=over
|
|
|
|
=item a word:
|
|
|
|
the name of the method to call
|
|
|
|
=item undef:
|
|
|
|
$word is used as $dest
|
|
|
|
=item a ref to code:
|
|
|
|
an anonymous subroutin to call
|
|
|
|
=item a hash ref:
|
|
|
|
it's a recursive call to `{ $word => $dest }`
|
|
|
|
=item an array ref:
|
|
|
|
in this case each element of the array will be considered as
|
|
`{ $element => $element }`. So each element must be a word that makes a
|
|
correspondence between a path_info word and a subroutine
|
|
|
|
=back
|
|
|
|
Some special $word:
|
|
|
|
=over
|
|
|
|
=item ':name':
|
|
|
|
the word in path_info will be stored in GET parameters
|
|
|
|
=item '*':
|
|
|
|
the subroutine will be called with the word of path_info as second argument
|
|
(after $req)
|
|
|
|
=item 'something.html':
|
|
|
|
if $word finishes with '.html', then sendHtml() will be called with
|
|
'something.tpl' as template name. In this case, $dest is not used.
|
|
|
|
=back
|
|
|
|
Examples:
|
|
|
|
=over
|
|
|
|
=item to manage http://.../books/127 with book() where 127 is the book number, use:
|
|
|
|
$self->addRoute( books => { ':bookId' => 'book' }, ['GET'] );
|
|
|
|
bookId parameter will be stored in $req->params('bookId');
|
|
|
|
=item to manage http://.../books/127/pages/5 with page(), use:
|
|
|
|
$self->addRoute( books => { ':bookId' => { pages => { ':pageId' => 'page' } } }, ['GET'] );
|
|
|
|
=item to manage simultaneously the 2 previous examples
|
|
|
|
$self->addRoute( books => { ':bookId' => { pages => { ':pageId' => 'page' } } }, ['GET'] )
|
|
->addRoute( books => { ':bookId' => { '*' => 'book' } }, ['GET'] );
|
|
|
|
Note that book() will be called for any path_info containing /books/<$bookid>/<$other>
|
|
except if $other == 'pages'.
|
|
|
|
=item to manage /properties/p1, /properties/p2 with p1() and p2(), use:
|
|
|
|
$self->addRoute( properties => [ 'p1', 'p2' ] );
|
|
|
|
=back
|
|
|
|
=head3 defaultRoute($path)
|
|
|
|
This method defined which path_info to use if path_info is '/' or empty.
|
|
|
|
=head2 Accessors
|
|
|
|
See L<Lemonldap::NG::Common::PSGI> for inherited accessors (error, languages,
|
|
logLevel, staticPrefix, templateDir, links, syslog).
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<http://lemonldap-ng.org/>, L<Lemonldap::NG::Portal>, L<Lemonldap::NG::Handler>,
|
|
L<Plack>, L<PSGI>, L<Lemonldap::NG::Common::PSGI>,
|
|
L<Lemonldap::NG::Common::PSGI::Request>, L<HTML::Template>,
|
|
|
|
=head1 AUTHORS
|
|
|
|
=over
|
|
|
|
=item LemonLDAP::NG team L<http://lemonldap-ng.org/team>
|
|
|
|
=back
|
|
|
|
=head1 BUG REPORT
|
|
|
|
Use OW2 system to report bug or ask for features:
|
|
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>
|
|
|
|
=head1 DOWNLOAD
|
|
|
|
Lemonldap::NG is available at
|
|
L<http://forge.objectweb.org/project/showfiles.php?group_id=274>
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
See COPYING file for details.
|
|
|
|
This library is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2, or (at your option)
|
|
any later version.
|
|
|
|
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. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see L<http://www.gnu.org/licenses/>.
|
|
|
|
=cut
|
|
|