File Coverage

blib/lib/Leyland.pm
Criterion Covered Total %
statement 49 155 31.6
branch 3 48 6.2
condition 3 47 6.3
subroutine 14 20 70.0
pod 2 2 100.0
total 71 272 26.1


line stmt bran cond sub pod time code
1             package Leyland;
2              
3             # ABSTRACT: RESTful web application framework based on Plack
4              
5 2     2   148938 use Moo;
  2         38565  
  2         13  
6 2     2   4030 use parent 'Plack::Component';
  2         375  
  2         28  
7 2     2   42698 use namespace::clean;
  2         28156  
  2         13  
8 2     2   2072 use version 0.77;
  2         5563  
  2         16  
9              
10             our $VERSION = "1.000002";
11             $VERSION = eval $VERSION;
12             our $DISPLAY_VERSION = version->parse($VERSION)->normal;
13              
14 2     2   228 use Carp;
  2         3  
  2         143  
15 2     2   8694 use Encode;
  2         32851  
  2         698  
16 2     2   1582 use Leyland::Localizer;
  2         7  
  2         96  
17 2     2   1420 use Leyland::Negotiator;
  2         5  
  2         81  
18 2     2   5063 use Module::Load;
  2         2785  
  2         18  
19 2     2   194 use Scalar::Util qw/blessed/;
  2         5  
  2         365  
20 2     2   2201 use Text::SpanningTable;
  2         4745  
  2         71  
21 2     2   2093 use Tie::IxHash;
  2         13944  
  2         165  
22 2     2   21 use Try::Tiny;
  2         6  
  2         6191  
23              
24             our %INFO;
25              
26             =head1 NAME
27              
28             Leyland - RESTful web application framework based on Plack
29              
30             =head1 SYNOPSIS
31              
32             # in app.psgi:
33              
34             #!/usr/bin/perl -w
35              
36             use strict;
37             use warnings;
38             use MyApp;
39              
40             my $app = MyApp->new->to_app;
41              
42             =head1 DESCRIPTION
43              
44             Leyland is a L-based application framework for building truely
45             RESTful, MVC-style web applications. It is feature rich and highly
46             extensible.
47              
48             B
49              
50             Leyland v1.0.0 brings small changes that break backwords compatibility.
51             Read the L for more information.
52              
53             =head2 FEATURES
54              
55             =over
56              
57             =item * B Leyland was designed from
58             the ground up according to the Representational State Transfer style of
59             software architecture. Leyland applications perform real HTTP negotiations,
60             (can) provide different representations of the same resource easily, respond
61             with proper HTTP status codes, throw real HTTP exceptions, etc.
62              
63             =item * B - Leyland automatically
64             serializes resources to representations in the format your client
65             wants to receive, like JSON and XML. It will also automatically deserialize
66             JSON/XML requests coming from the client to Perl data-structures.
67              
68             =item * B - Leyland applications are pure UTF-8. Anything your
69             application receives is automatically UTF-8 decoded, and anything your
70             application sends is automatically UTF-8 encoded. Leyland apps will not
71             accept, nor provide, content in a different character set. If you want to
72             use different/multiple encodings, then Leyland is not for you.
73              
74             =item * B - Pretty much every other
75             application framework only concerns itself with localizing the application
76             to the locale of the machine on which it is running. I find that this is
77             rarely useful nor interesting to the application developer. Leyland localizes for
78             the client, not the server. If the client wants to view your application
79             (which may be a simple website) in Hebrew, and your application supports
80             Hebrew, then you can easily provide him with Hebrew representations.
81             Leyland uses L for this purpose.
82              
83             =item * B<< Easy deployment and middleware support via L >> - Leyland doesn't
84             support Plack, it is dependant on it. Leyland's entire session support,
85             for example, depends on Plack's L
86             middleware. Use the full power of Plack in your Leyland application.
87              
88             =item * B - Leyland is much smaller than L or other
89             major frameworks, while still providing lots of features. While it is not
90             a "micro-framework", it is pretty small. If you're looking for an extremely
91             lightweight solution, my other framework - L - might fit your need.
92              
93             =item * B - Leyland was designed to be as flexible and
94             as extensible as possible - where flexibility matters, and strict - where
95             constistency and convention are appropriate. Leyland goes to great lengths
96             to give you the ability to do things the way you want to, and more
97             importantly - the way your end-users want to. Your applications listen to
98             your users' preferences and automatically decide on a suitable course of action.
99             Leyland is also L based, making it easy to extend and tweak its behavior
100             (and making it L compatible).
101              
102             =item * B - You don't really need a pony, do you?
103              
104             =back
105              
106             =head2 MANUAL / TUTORIAL / GUIDE / GIBBERISH
107              
108             To learn about using Leyland, please refer to the L. The
109             documentation of this distribution's classes is for reference only, the
110             manual is where you're most likely to find your answers. Or not.
111              
112             =head2 UPGRADING FROM VERSION 0.1.7 OR SMALLER
113              
114             Major changes have been made in Leyland version 1.0.0. While most should be
115             backwords compatible, some are not. Please take a look at the L
116             for a complete list of changes and a simple guide for upgrading existing applications.
117              
118             =head2 WHAT'S WITH THE NAME?
119              
120             Leyland is named after Mr. Bean's clunker of a car - the British Leyland
121             Mini 1000. I don't know why.
122              
123             =head1 EXTENDS
124              
125             L
126              
127             =head1 ATTRIBUTES
128              
129             =head2 name
130              
131             The package name of the application, for example C or C.
132             Automatically created.
133              
134             =head2 config
135              
136             A hash-ref of configuration options supplied to the app by the PSGI file.
137             These options are purely for the writer of the application and have nothing
138             to do with Leyland itself.
139              
140             =head2 context_class
141              
142             The name of the class to be used as the context class for every request.
143             Defaults to L. If provided, the class must extend
144             Leyland::Context.
145              
146             =head2 localizer
147              
148             If application config defines a path for localization files, this will hold
149             a L object, which is based on L.
150              
151             =head2 views
152              
153             An array refernce of all L classes enabled in the app's
154             configuration. If none defined, L is used by default.
155              
156             =head2 routes
157              
158             A L object holding all routes defined in the application's
159             controllers. Automatically created, not to be used directly by applications.
160              
161             =head2 cwe
162              
163             The plack environment in which the application is running. This is the
164             C environment variable. Defaults to "development" unless you've
165             provided a specific value to C (via the C<-E> switch or by
166             changing C directly).
167              
168             =cut
169              
170             has 'name' => (
171             is => 'ro',
172             isa => sub { die "name must be a scalar" if ref $_[0] },
173             writer => '_set_name'
174             );
175              
176             has 'config' => (
177             is => 'ro',
178             isa => sub { die "config must be a hash-ref" unless ref $_[0] && ref $_[0] eq 'HASH' },
179             default => sub { {} }
180             );
181              
182             has 'context_class' => (
183             is => 'ro',
184             isa => sub { die "context_class must be a scalar" if ref $_[0] },
185             writer => '_set_context_class',
186             default => sub { 'Leyland::Context' }
187             );
188              
189             has 'localizer' => (
190             is => 'ro',
191             predicate => 'has_localizer',
192             writer => '_set_localizer'
193             );
194              
195             has 'views' => (
196             is => 'ro',
197             isa => sub { die "views must be an array-ref" unless ref $_[0] && ref $_[0] eq 'ARRAY' },
198             predicate => 'has_views',
199             writer => '_set_views'
200             );
201              
202             has 'routes' => (
203             is => 'ro',
204             isa => sub { die "routes must be a Tie::IxHash object" unless ref $_[0] && ref $_[0] eq 'Tie::IxHash' },
205             predicate => 'has_routes',
206             writer => '_set_routes'
207             );
208              
209             has 'cwe' => (
210             is => 'ro',
211             isa => sub { die "cwe must be a scalar" if ref $_[0] },
212             default => sub { $ENV{PLACK_ENV} }
213             );
214              
215             =head1 CLASS METHODS
216              
217             =head2 new( [ %attrs ] )
218              
219             Creates a new instance of this class. None of the attributes are required
220             (in fact, you shouldn't pass most of them), though you can pass the
221             C and C attributes if you need.
222              
223             =head1 OBJECT METHODS
224              
225             =head2 setup()
226              
227             This method is not available by default, but is expected to be provided by
228             application classes (though it is not required). If present, it will be
229             called upon creation of the application object. The method is expected to
230             return a hash-ref of Leyland-specific options. The following options are
231             supported:
232              
233             =over
234              
235             =item * views
236              
237             A list of view classes to load. Defaults to C<["Tenjin"]>.
238              
239             =item * view_dir
240              
241             The path to the directory in which views/templates reside (defaults to C).
242              
243             =item * locales
244              
245             The path to the directory in which localization files (in L's format)
246             reside (if localization is used).
247              
248             =item * default_mime
249              
250             The default return MIME type for routes that lack a specific declaration (defaults to C).
251              
252             =back
253              
254             =head2 call( \%env )
255              
256             The request handler. Receives a standard PSGI env hash-ref, creates a new instance of the
257             application's context class (most probably L), performs HTTP negotiations
258             and finds routes matching the request. If any are found, the first one is invoked and
259             an HTTP response is generated and returned.
260              
261             You should note that requests to paths that end with a slash will automatically
262             be redirected without the trailing slash.
263              
264             =cut
265              
266             sub call {
267 0     0 1 0 my ($self, $env) = @_;
268              
269             # create the context object
270 0         0 my $c = $self->context_class->new(
271             app => $self,
272             env => $env
273             );
274              
275             # does the request path have an "unnecessary" trailing slash?
276             # if so, remove it and redirect to the resulting URI
277 0 0 0     0 if ($c->path ne '/' && $c->path =~ m!/$!) {
278 0         0 my $newpath = $`;
279 0         0 my $uri = $c->uri;
280 0         0 $uri->path($newpath);
281            
282 0         0 $c->res->redirect($uri, 301);
283 0         0 return $c->_respond;
284             }
285              
286             # is this an OPTIONS request?
287 0 0       0 if ($c->method eq 'OPTIONS') {
288             # get all available methods by using Leyland::Negotiator
289             # and return a 204 No Content response
290 0         0 $c->log->debug('Finding supported methods for requested path.');
291 0         0 return $c->_respond(204, { 'Allow' => join(', ', Leyland::Negotiator->find_options($c, $self->routes)) });
292             } else {
293             # negotiate for routes and invoke the first matching route (if any).
294             # handle route passes and return the final output after UTF-8 encoding.
295             # if at any point an expception is raised, handle it.
296             return try {
297             # get routes
298 0     0   0 $c->log->debug('Searching matching routes.');
299 0         0 $c->_set_routes(Leyland::Negotiator->negotiate($c, $self->routes));
300              
301             # invoke first route
302 0         0 $c->log->debug('Invoking first matching route.');
303 0         0 my $ret = $c->_invoke_route;
304              
305             # are we passing to the next matching route?
306             # to prevent infinite loops, limit passing to no more than 100 times
307 0   0     0 while ($c->_pass_next && $c->current_route < 100) {
308             # we need to pass to the next matching route.
309             # first, let's erase the pass flag from the context
310             # so we don't try to do this infinitely
311 0         0 $c->_set_pass_next(0);
312             # no let's invoke the route
313 0         0 $ret = $c->_invoke_route;
314             }
315              
316 0         0 $c->finalize(\$ret);
317            
318 0         0 $c->_respond(undef, undef, $ret);
319             } catch {
320 0     0   0 $self->_handle_exception($c, $_);
321 0         0 };
322             }
323             }
324              
325             =head2 has_localizer()
326              
327             Returns a true value if the application has a localizer.
328              
329             =head2 has_views()
330              
331             Returns a true value if the application has any view classes.
332              
333             =head2 has_routes()
334              
335             Returns a true value if the application has any routes defined in its
336             controllers.
337              
338             =head1 INTERNAL METHODS
339              
340             The following methods are only to be used internally.
341              
342             =head2 BUILD()
343              
344             Automatically called by L after instance creation, this method
345             runs the applicaiton's C method (if any), loads the context class,
346             localizer, controllers and views. It then find all routes in the controllers
347             and prints a nice info table to the log.
348              
349             =cut
350              
351             sub BUILD {
352 1     1 1 16 my $self = shift;
353              
354             # invoke setup method and get application settings
355 1 50       10 my $settings = $self->can('setup') ? $self->setup : {};
356 1   50     10 $settings->{views} ||= ['Tenjin'];
357 1   50     4 $settings->{view_dir} ||= 'views';
358              
359 1         12 $self->_set_name(blessed $self);
360              
361 1   50     125 $INFO{default_mime} = $settings->{default_mime} || 'text/html';
362              
363 1 50       4 $self->_set_context_class($settings->{context_class})
364             if $settings->{context_class};
365              
366             # load the context class
367 1         11 load $self->context_class;
368              
369             # init localizer, if localization path given
370 1 50       30 $self->_set_localizer(Leyland::Localizer->new(path => $self->config->{locales}))
371             if exists $self->config->{locales};
372              
373             # require Module::Pluggable and load all views and controllers
374             # with it
375 1         5 load Module::Pluggable;
376 0           Module::Pluggable->import(
377             search_path => [$self->name.'::View'],
378             sub_name => '_views',
379             instantiate => 'new'
380             );
381 0           Module::Pluggable->import(
382             search_path => [$self->name.'::Controller'],
383             sub_name => 'controllers',
384             require => 1
385             );
386              
387             # init views, if any, start with view modules in the app
388 0           my @views = $self->_views;
389             # now load views defined in the config file
390 0           VIEW: foreach (@{$settings->{views}}) {
  0            
391             # have we already loaded this view in the first step?
392 0           foreach my $v ($self->_views) {
393 0 0         next VIEW if blessed($v) eq $_;
394             }
395              
396             # attempt to load this view
397 0           my $class = "Leyland::View::$_";
398 0           load $class;
399 0           push(@views, $class->new(view_dir => $settings->{view_dir}));
400             }
401 0 0         $self->_set_views(\@views) if scalar @views;
402              
403             # if we haven't loaded any views, load Tenjin
404 0 0         unless (scalar @views) {
405 0           load Leyland::View::Tenjin;
406 0           $self->_set_views([ Leyland::View::Tenjin->new(view_dir => $settings->{view_dir}) ]);
407             }
408              
409             # get all routes
410 0           my $routes = Tie::IxHash->new;
411 0           foreach ($self->controllers) {
412 0   0       my $prefix = $_->prefix || '_root_';
413            
414             # in order to allow multiple controllers having the same
415             # prefix, let's see if we've already encountered this prefix,
416             # and if so, merge the routes
417 0 0 0       if ($routes->EXISTS($prefix)) {
    0          
418 0           foreach my $r ($_->routes->Keys) {
419 0           foreach my $m (keys %{$_->routes->FETCH($r)}) {
  0            
420 0 0         if ($routes->FETCH($prefix)->EXISTS($r)) {
421 0           $routes->FETCH($prefix)->FETCH($r)->{$m} = $_->routes->FETCH($r)->{$m};
422             } else {
423 0           $routes->FETCH($prefix)->Push($r => { $m => $_->routes->FETCH($r)->{$m} });
424             }
425             }
426             }
427             } elsif ($_->routes && $_->routes->Length) {
428 0           $routes->Push($prefix => $_->routes);
429             }
430             }
431 0           $self->_set_routes($routes);
432              
433             # print debug information
434 0           $self->_initial_debug_info;
435             }
436              
437             # _handle_exception( $c, $exp )
438             # -----------------------------
439             # Receives exceptions thrown by the application (including run-time errors)
440             # and generates an HTTP response with the error information, in a format
441             # recognizable by the client.
442              
443             sub _handle_exception {
444 0     0     my ($self, $c, $exp) = @_;
445              
446             # have we caught a Leyland::Exception object? if not, turn it into
447             # a Leyland::Exception
448 0 0 0       $exp = Leyland::Exception->new(code => 500, error => $exp)
449             unless blessed($exp) && $exp->isa('Leyland::Exception');
450              
451             # log the error thrown
452 0           $c->log->info('Exception thrown: '.$exp->code.", message: ".$exp->error);
453              
454             # is this a redirecting exception?
455 0 0 0       if ($exp->code =~ m/^3\d\d$/ && $exp->has_location) {
456 0           $c->res->redirect($exp->location);
457 0           return $c->_respond($exp->code);
458             }
459              
460             # are we on the development environment? if so, and the client
461             # accepts HTML (and the exception has no HTML MIME), we croak
462             # with a simple error message so that Plack displays a nice stack trace
463 0 0 0       croak $self->name.' croaked with HTTP status code '.$exp->code.' and error message "'.$exp->error.'"'
      0        
      0        
464             if $self->cwe eq 'development' && $c->accepts('text/html') && (!$exp->has_mimes || !$exp->has_mime('text/html'));
465              
466             # do we have templates for any of the client's requested MIME types?
467             # if so, render the first one you find.
468 0 0         if ($exp->has_mimes) {
469 0           foreach (@{$c->wanted_mimes}) {
  0            
470 0 0         return $c->_respond(
471             $exp->code,
472             { 'Content-Type' => $_->{mime}.'; charset=UTF-8' },
473             $c->template($exp->mime($_->{mime}), $exp->hash, $exp->use_layout)
474             ) if $exp->has_mime($_->{mime});
475             }
476             }
477              
478             # we haven't found any templates for the request mime types, let's
479             # attempt to serialize the error ourselves if the client accepts
480             # JSON or XML
481 0           foreach (@{$c->wanted_mimes}) {
  0            
482 0 0 0       return $c->_respond(
      0        
      0        
      0        
483             $exp->code,
484             { 'Content-Type' => $_->{mime}.'; charset=UTF-8' },
485             $c->_serialize($exp->hash, $_->{mime})
486             ) if $_->{mime} eq 'text/html' ||
487             $_->{mime} eq 'application/xhtml+xml' ||
488             $_->{mime} eq 'application/json' ||
489             $_->{mime} eq 'application/atom+xml' ||
490             $_->{mime} eq 'application/xml';
491             }
492              
493             # We do not support none of the MIME types the client wants,
494             # let's return plain text
495 0           return $c->_respond(
496             $exp->code,
497             { 'Content-Type' => 'text/plain; charset=UTF-8' },
498             $exp->error
499             );
500             }
501              
502             # _autolog( $msg )
503             # ----------------
504             # Used by C when printing the application's info
505             # table.
506              
507 0     0     sub _autolog { print STDOUT $_[0], "\n" }
508              
509             # _initial_debug_info()
510             # ---------------------
511             # Prints an info table of the application after initialization.
512              
513             sub _initial_debug_info {
514 0     0     my $self = shift;
515              
516 0           my @views;
517 0 0         foreach (sort @{$self->views || []}) {
  0            
518 0           my $view = ref $_;
519 0           $view =~ s/^Leyland::View:://;
520 0           push(@views, $view);
521             }
522              
523 0           my $t1 = Text::SpanningTable->new(96);
524 0           $t1->exec(\&_autolog);
525              
526 0           $t1->hr('top');
527 0           $t1->row($self->name.' (powered by Leyland '.$DISPLAY_VERSION.')');
528 0           $t1->dhr;
529 0           $t1->row('Current working environment: '.$self->cwe);
530 0           $t1->row('Avilable views: '.join(', ', @views));
531            
532 0           $t1->hr('bottom');
533            
534 0           my $t2 = Text::SpanningTable->new(16, 24, 13, 18, 18, 12);
535 0           $t2->exec(\&_autolog);
536 0           $t2->hr('top');
537 0           $t2->row([6, 'Available routes:']);
538 0           $t2->dhr;
539              
540 0 0 0       if ($self->has_routes && $self->routes->Length) {
541 0           $t2->row('Prefix', 'Regex', 'Method', 'Accepts', 'Returns', 'Is');
542 0           $t2->dhr;
543              
544 0 0         foreach (sort { ($b eq '_root_') <=> ($a eq '_root_') || $a cmp $b } $self->routes->Keys) {
  0            
545 0           my $c = $_;
546 0           $c =~ s!_root_!(root)!;
547 0           my $pre = $self->routes->FETCH($_);
548 0 0         if ($pre) {
549 0           foreach my $r (sort $pre->Keys) {
550 0           my $reg = $pre->FETCH($r);
551 0           foreach my $m (sort keys %$reg) {
552 0 0         my $returns = ref $reg->{$m}->{rules}->{returns} eq 'ARRAY' ? join(', ', @{$reg->{$m}->{rules}->{returns}}) : $reg->{$m}->{rules}->{returns};
  0            
553 0 0         my $accepts = ref $reg->{$m}->{rules}->{accepts} eq 'ARRAY' ? join(', ', @{$reg->{$m}->{rules}->{accepts}}) : $reg->{$m}->{rules}->{accepts};
  0            
554 0 0         my $is = ref $reg->{$m}->{rules}->{is} eq 'ARRAY' ? join(', ', @{$reg->{$m}->{rules}->{is}}) : $reg->{$m}->{rules}->{is};
  0            
555            
556 0           $t2->row($c, $r, uc($m), $accepts, $returns, $is);
557             }
558             }
559             }
560             }
561             } else {
562 0           $t2->row([6, '-- No routes available!']);
563             }
564              
565 0           $t2->hr('bottom');
566             }
567              
568             $Leyland::CODES = {
569             # success codes
570             200 => 'OK',
571             201 => 'Created',
572             202 => 'Accepted',
573             204 => 'No Content',
574              
575             # redirect codes
576             300 => 'Multiple Choices',
577             301 => 'Moved Permanently',
578             302 => 'Found',
579             303 => 'See Other',
580             304 => 'Not Modified',
581             307 => 'Temporary Redirect',
582              
583             # client error codes
584             400 => 'Bad Request',
585             401 => 'Unauthorized',
586             403 => 'Forbidden',
587             404 => 'Not Found',
588             405 => 'Method Not Allowed',
589             406 => 'Not Acceptable',
590             408 => 'Request Timeout',
591             409 => 'Conflict',
592             410 => 'Gone',
593             411 => 'Length Required',
594             412 => 'Precondition Failed',
595             413 => 'Request Entity Too Large',
596             414 => 'Request-URI Too Long',
597             415 => 'Unsupported Media Type',
598             417 => 'Expectation Failed',
599              
600             # server error codes
601             500 => 'Internal Server Error',
602             501 => 'Not Implemented',
603             502 => 'Bad Gateway',
604             503 => 'Service Unavailable',
605             504 => 'Gateway Timeout',
606             522 => 'Connection timed out',
607             };
608              
609             =head1 AUTHOR
610              
611             Ido Perlmuter, C<< >>
612              
613             =head1 ACKNOWLEDGMENTS
614              
615             I wish to thank the following people:
616              
617             =over
618              
619             =item * L for submitting bug fixes
620              
621             =item * L for some helpful ideas
622              
623             =back
624              
625             =head1 BUGS
626              
627             Please report any bugs or feature requests to C, or through
628             the web interface at L. I will be notified, and then you'll
629             automatically be notified of progress on your bug as I make changes.
630              
631             =head1 SUPPORT
632              
633             You can find documentation for this module with the perldoc command.
634              
635             perldoc Leyland
636              
637             You can also look for information at:
638              
639             =over 4
640              
641             =item * RT: CPAN's request tracker
642              
643             L
644              
645             =item * AnnoCPAN: Annotated CPAN documentation
646              
647             L
648              
649             =item * CPAN Ratings
650              
651             L
652              
653             =item * Search CPAN
654              
655             L
656              
657             =back
658              
659             =head1 LICENSE AND COPYRIGHT
660              
661             Copyright 2010-2014 Ido Perlmuter.
662              
663             This program is free software; you can redistribute it and/or modify it
664             under the terms of either: the GNU General Public License as published
665             by the Free Software Foundation; or the Artistic License.
666              
667             See http://dev.perl.org/licenses/ for more information.
668              
669             =cut
670              
671             1;