File Coverage

blib/lib/Kelp.pm
Criterion Covered Total %
statement 143 148 96.6
branch 40 52 76.9
condition 17 22 77.2
subroutine 34 36 94.4
pod 14 16 87.5
total 248 274 90.5


line stmt bran cond sub pod time code
1             package Kelp;
2              
3 31     31   217730 use Kelp::Base;
  31         65  
  31         177  
4              
5 31     31   2306 use Carp qw/ longmess croak /;
  31         69  
  31         1885  
6 31     31   15151 use FindBin;
  31         33950  
  31         1675  
7 31     31   15084 use Encode;
  31         403777  
  31         2374  
8 31     31   243 use Try::Tiny;
  31         57  
  31         1580  
9 31     31   20801 use Data::Dumper;
  31         196842  
  31         1884  
10 31     31   15916 use Sys::Hostname;
  31         32033  
  31         2117  
11 31     31   14048 use Plack::Util;
  31         314685  
  31         963  
12 31     31   16818 use Class::Inspector;
  31         113382  
  31         1247  
13 31     31   228 use Scalar::Util qw(blessed);
  31         64  
  31         67533  
14              
15             our $VERSION = '1.05';
16              
17             # Basic attributes
18             attr -host => hostname;
19             attr mode => $ENV{KELP_ENV} // $ENV{PLACK_ENV} // 'development';
20             attr -path => $FindBin::Bin;
21             attr -name => sub { ( ref( $_[0] ) =~ /(\w+)$/ ) ? $1 : 'Noname' };
22             attr request_obj => 'Kelp::Request';
23             attr response_obj => 'Kelp::Response';
24              
25              
26             # Debug
27             attr long_error => $ENV{KELP_LONG_ERROR} // 0;
28              
29             # The charset is UTF-8 unless otherwise instructed
30             attr -charset => sub {
31             $_[0]->config("charset") // 'UTF-8';
32             };
33              
34             # Name the config module
35             attr config_module => 'Config';
36              
37             # Undocumented.
38             # Used to unlock the undocumented features of the Config module.
39             attr __config => undef;
40              
41             attr -loaded_modules => sub { {} };
42              
43             # Each route's request an response objects will
44             # be put here:
45             attr req => undef;
46             attr res => undef;
47              
48             # Initialization
49             sub new {
50 41     41 1 4449 my $self = shift->SUPER::new(@_);
51              
52             # Always load these modules, but allow client to override
53 41         203 $self->_load_config();
54 40         274 $self->_load_routes();
55              
56             # Load the modules from the config
57 40 50       168 if ( defined( my $modules = $self->config('modules') ) ) {
58 40         187 $self->load_module($_) for (@$modules);
59             }
60              
61 40         201 $self->build();
62 40         150 return $self;
63             }
64              
65             my $last_anon = 0;
66             sub new_anon {
67 6     6 1 427 my $class = shift;
68              
69             # make sure we don't eval something dodgy
70 6 100 66     99 die "invalid class for new_anon"
      100        
      66        
71             if ref $class # not a string
72             || !$class # not an empty string, undef or 0
73             || !Class::Inspector->loaded($class) # not a loaded class
74             || !$class->isa(__PACKAGE__) # not a correct class
75             ;
76              
77 4         231 my $anon_class = "Kelp::Anonymous::$class" . ++$last_anon;
78 4         9 my $err = do {
79 4         6 local $@;
80 4     2   425 my $eval_status = eval qq[
  2     1   16  
  2     1   3  
  2         16  
  1         9  
  1         3  
  1         9  
  1         8  
  1         2  
  1         8  
81             {
82             package $anon_class;
83             use parent -norequire, '$class';
84             }
85             1;
86             ];
87 4 50       35 $@ || !$eval_status;
88             };
89              
90 4 50       15 if ($err) {
91 0 0       0 die "Couldn't create anonymous Kelp instance: " .
92             (length $err > 1 ? $err : 'unknown error');
93             }
94              
95 4         27 return $anon_class->new(@_);
96             }
97              
98             sub _load_config {
99 41     41   91 my $self = shift;
100 41         202 $self->load_module( $self->config_module, extra => $self->__config );
101             }
102              
103             sub _load_routes {
104 40     40   90 my $self = shift;
105 40         120 $self->load_module('Routes');
106             }
107              
108             # Create a shallow copy of the app, optionally blessed into a
109             # different subclass.
110             sub _clone {
111 5     5   7 my $self = shift;
112 5   33     13 my $subclass = shift || ref($self);
113              
114 5 50       14 ref $self or croak '_clone requires instance';
115 5         67 return bless { %$self }, $subclass;
116             }
117              
118             sub load_module {
119 162     162 1 3054 my ( $self, $name, %args ) = @_;
120              
121             # A module name with a leading + indicates it's already fully
122             # qualified (i.e., it does not need the Kelp::Module:: prefix).
123 162 100       561 my $prefix = $name =~ s/^\+// ? undef : 'Kelp::Module';
124              
125             # Make sure the module was not already loaded
126 162 50       482 return if $self->loaded_modules->{$name};
127              
128 162         597 my $class = Plack::Util::load_class( $name, $prefix );
129 161         2647 my $module = $self->loaded_modules->{$name} = $class->new( app => $self );
130              
131             # When loading the Config module itself, we don't have
132             # access to $self->config yet. This is why we check if
133             # config is available, and if it is, then we pull the
134             # initialization hash.
135 161         325 my $args_from_config = {};
136 161 100       771 if ( $self->can('config') ) {
137 128   100     522 $args_from_config = $self->config("modules_init.$name") // {};
138             }
139              
140 161         950 $module->build( %$args_from_config, %args );
141 160         588 return $module;
142             }
143              
144             # Override this one to add custom initializations
145       34 1   sub build {
146             }
147              
148             # Override to use a custom request object
149             sub build_request {
150 202     202 1 368 my ( $self, $env ) = @_;
151 202         673 my $package = $self->request_obj;
152 202         12882 eval qq{require $package};
153 202         1567 return $package->new( app => $self, env => $env);
154             }
155              
156             # Override to use a custom response object
157             sub build_response {
158 196     196 1 322 my $self = shift;
159 196         510 my $package = $self->response_obj;
160 196         11831 eval qq{require $package};
161 196         1334 return $package->new( app => $self );
162             }
163              
164             # Override to manipulate the end response
165             sub before_finalize {
166 192     192 1 283 my $self = shift;
167 192         391 $self->res->header('X-Framework' => 'Perl Kelp');
168             }
169              
170             # Override this to wrap more middleware around the app
171             sub run {
172 201     201 1 421 my $self = shift;
173 201     201   1104 my $app = sub { $self->psgi(@_) };
  201         161223  
174              
175             # Add middleware
176 201 50       855 if ( defined( my $middleware = $self->config('middleware') ) ) {
177 201         448 for my $class (@$middleware) {
178              
179             # Make sure the middleware was not already loaded
180             # This does not apply for testing, in which case we want
181             # the middleware to wrap every single time
182 7 50 66     101 next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING};
183              
184 7         29 my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
185 7   100     13258 my $args = $self->config("middleware_init.$class") // {};
186 7         73 $app = $mw->wrap( $app, %$args );
187             }
188             }
189              
190 201         59119 return $app;
191             }
192              
193             sub psgi {
194 201     201 0 428 my ( $self, $env ) = @_;
195              
196             # Create the request and response objects
197 201         531 my $req = $self->req( $self->build_request($env) );
198 201         595 my $res = $self->res( $self->build_response );
199              
200             # Get route matches
201 201         715 my $match = $self->routes->match( $req->path, $req->method );
202              
203             # None found? Show 404 ...
204 201 100       573 if ( !@$match ) {
205 7         35 $res->render_404;
206 7         22 return $self->finalize;
207             }
208              
209             try {
210              
211             # Go over the entire route chain
212 194     194   8915 for my $route (@$match) {
213              
214             # Dispatch
215 198         566 $self->req->named( $route->named );
216 198         501 $self->req->route_name( $route->name );
217 198         600 my $data = $self->routes->dispatch( $self, $route );
218              
219             # Log info about the route
220 166 100       9184 if ( $self->can('logger') ) {
221 1         4 $self->info(
222             sprintf( "%s - %s %s - %s",
223             $req->address, $req->method,
224             $req->path, $route->to )
225             );
226             }
227              
228             # Is it a bridge? Bridges must return a true value
229             # to allow the rest of the routes to run.
230 166 100       560 if ( $route->bridge ) {
231 10 100       22 if ( !$data ) {
232 4 100       12 $res->render_403 unless $res->rendered;
233 4         10 last;
234             }
235 6         14 next;
236             }
237              
238             # If the route returned something, then analyze it and render it
239 156 100       361 if ( defined $data ) {
240              
241             # Handle delayed response if CODE
242 154 100       425 return $data if ref($data) eq 'CODE';
243 153 100       379 $res->render($data) unless $res->rendered;
244             }
245             }
246              
247             # If nothing got rendered
248 159 100       387 if ( !$self->res->rendered ) {
249             # render 404 if only briges matched
250 3 100       9 if ( $match->[-1]->bridge ) {
251 1         4 $res->render_404;
252             }
253             # or die with error
254             else {
255 2         5 die $match->[-1]->to
256             . " did not render for method "
257             . $req->method;
258             }
259             }
260              
261 157         414 $self->finalize;
262             }
263             catch {
264 36     36   1946 my $exception = $_;
265 36         92 my $res = $self->res;
266              
267 36 100 100     237 if (blessed $exception && $exception->isa('Kelp::Exception')) {
268             # No logging here, since it is a message for the user with a code
269             # rather than a real exceptional case
270             # (Nothing really broke, user code invoked this)
271              
272 12         39 $res->render_exception($exception);
273             }
274             else {
275 24 50       90 my $message = $self->long_error ? longmess($exception) : $exception;
276              
277             # Log error
278 24 50       105 $self->logger( 'critical', $message ) if $self->can('logger');
279              
280             # Render 500
281 24         74 $res->render_500($_);
282             }
283 33         188 $self->finalize;
284 194         1809 };
285             }
286              
287             sub finalize {
288 197     197 0 297 my $self = shift;
289 197         544 $self->before_finalize;
290 197         8740 $self->res->finalize;
291             }
292              
293              
294             #----------------------------------------------------------------
295             # Request and Response shortcuts
296             #----------------------------------------------------------------
297             sub param {
298 68     68 1 3741 my $self = shift;
299 68         151 unshift @_, $self->req;
300              
301             # goto will allow carp show the correct caller
302 68         350 goto $_[0]->can('param');
303             }
304              
305 0     0 1 0 sub session { shift->req->session(@_) }
306              
307             sub stash {
308 2     2 1 3 my $self = shift;
309 2 100       10 @_ ? $self->req->stash->{$_[0]} : $self->req->stash;
310             }
311              
312             sub named {
313 21     21 1 72 my $self = shift;
314 21 50       46 @_ ? $self->req->named->{$_[0]} : $self->req->named;
315             }
316              
317             #----------------------------------------------------------------
318             # Utility
319             #----------------------------------------------------------------
320              
321             sub url_for {
322 7     7 1 21 my ( $self, $name, @args ) = @_;
323 7         13 my $result = $name;
324 7     7   46 try { $result = $self->routes->url( $name, @args ) };
  7         163  
325 7         100 return $result;
326             }
327              
328             sub abs_url {
329 0     0 1 0 my ( $self, $name, @args ) = @_;
330 0         0 my $url = $self->url_for( $name, @args );
331 0         0 return URI->new_abs( $url, $self->config('app_url') )->as_string;
332             }
333              
334             1;
335              
336             __END__
337              
338             =pod
339              
340             =head1 NAME
341              
342             Kelp - A web framework light, yet rich in nutrients.
343              
344             =head1 SYNOPSIS
345              
346             package MyApp;
347             use parent 'Kelp';
348              
349             # bootstrap your application
350             sub build {
351             my ($self) = @_;
352              
353             my $r = $self->routes;
354              
355             $r->add('/simple/route', 'route_handler');
356             $r->add('/route/:name', {
357             to => 'namespace::controller::action',
358             ... # other options, see Kelp::Routes
359             });
360             }
361              
362             # example route handler
363             sub route_handler {
364             my ($kelp_instance, @route_parameters) = @_;
365              
366             return 'text to be rendered';
367             }
368              
369             1;
370              
371             =head1 DESCRIPTION
372              
373             Kelp is a light, modular web framework built on top of Plack.
374              
375             This document lists all the methods and attributes available in the main
376             instance of a Kelp application, passed as a first argument to route handling
377             routines.
378              
379             See L<Kelp::Manual> for a complete reference.
380              
381             See L<Kelp::Manual::Cookbook> for solutions to common problems.
382              
383             =head1 ATTRIBUTES
384              
385             =head2 hostname
386              
387             Gets the current hostname.
388              
389             sub some_route {
390             my $self = shift;
391             if ( $self->hostname eq 'prod-host' ) {
392             ...
393             }
394             }
395              
396             =head2 mode
397              
398             Sets or gets the current mode. The mode is important for the app to know what
399             configuration file to merge into the main configuration. See
400             L<Kelp::Module::Config> for more information.
401              
402             my $app = MyApp->new( mode => 'development' );
403             # conf/config.pl and conf/development.pl are merged with priority
404             # given to the second one.
405              
406             =head2 request_obj
407              
408             Provide a custom package name to define the global ::Request object. Defaults to
409             L<Kelp::Request>.
410              
411             =head2 response_obj
412              
413             Provide a custom package name to define the global ::Response object. Defaults to
414             L<Kelp::Response>.
415              
416             =head2 config_module
417              
418             Sets of gets the class of the configuration module to be loaded on startup. The
419             default value is C<Config>, which will cause the C<Kelp::Module::Config> to get
420             loaded. See the documentation for L<Kelp::Module::Config> for more information
421             and for an example of how to create and use other config modules.
422              
423             =head2 loaded_modules
424              
425             A hashref containing the names and instances of all loaded modules. For example,
426             if you have these two modules loaded: Template and JSON, then a dump of
427             the C<loaded_modules> hash will look like this:
428              
429             {
430             Template => Kelp::Module::Template=HASH(0x208f6e8),
431             JSON => Kelp::Module::JSON=HASH(0x209d454)
432             }
433              
434             This can come in handy if your module does more than just registering a new method
435             into the application. Then, you can use its object instance to access that
436             additional functionality.
437              
438              
439             =head2 path
440              
441             Gets the current path of the application. That would be the path to C<app.psgi>
442              
443             =head2 name
444              
445             Gets or sets the name of the application. If not set, the name of the main
446             class will be used.
447              
448             my $app = MyApp->new( name => 'Twittar' );
449              
450             =head2 charset
451              
452             Sets of gets the encoding charset of the app. It will be C<UTF-8>, if not set to
453             anything else. The charset could also be changed in the config files.
454              
455             =head2 long_error
456              
457             When a route dies, Kelp will by default display a short error message. Set this
458             attribute to a true value if you need to see a full stack trace of the error.
459             The C<KELP_LONG_ERROR> environment variable can also set this attribute.
460              
461             =head2 req
462              
463             This attribute only makes sense if called within a route definition. It will
464             contain a reference to the current L<Kelp::Request> instance.
465              
466             sub some_route {
467             my $self = shift;
468             if ( $self->req->is_json ) {
469             ...
470             }
471             }
472              
473             =head2 res
474              
475             This attribute only makes sense if called within a route definition. It will
476             contain a reference to the current L<Kelp::Response> instance.
477              
478             sub some_route {
479             my $self = shift;
480             $self->res->json->render( { success => 1 } );
481             }
482              
483             =head1 METHODS
484              
485             =head2 new
486              
487             my $the_only_kelp = KelpApp->new;
488              
489             A standard constructor. B<Cannot> be called multiple times: see L</new_anon>.
490              
491             =head2 new_anon
492              
493             my $kelp1 = KelpApp->new_anon(config => 'conf1');
494             my $kelp2 = KelpApp->new_anon(config => 'conf2');
495              
496             A constructor that can be called repeatedly. Cannot be mixed with L</new>.
497              
498             It works by creating a new anonymous class extending the class of your
499             application and running I<new> on it. C<ref $kelp> will return I<something
500             else> than the name of your Kelp class, but C<< $kelp->isa('KelpApp') >> will
501             be true. This will likely be useful during testing or when running multiple
502             instances of the same application with different configurations.
503              
504             =head2 build
505              
506             On its own, the C<build> method doesn't do anything. It is called by the
507             constructor, so it can be overridden to add route destinations and
508             initializations.
509              
510             package MyApp;
511              
512             sub build {
513             my $self = shift;
514             my $r = $self->routes;
515              
516             # Load some modules
517             $self->load_module("MongoDB");
518             $self->load_module("Validate");
519              
520             # Add all route destinations
521             $r->add("/one", "one");
522             ...
523              
524             }
525              
526             =head2 load_module
527              
528             C<load_module($name, %options)>
529              
530             Used to load a module. All modules must be under the C<Kelp::Module::>
531             namespace.
532              
533             $self->load_module("Redis", server => '127.0.0.1');
534             # Will look for and load Kelp::Module::Redis
535              
536             Options for the module may be specified after its name, or in the
537             C<modules_init> hash in the config. Precedence is given to the
538             inline options.
539             See L<Kelp::Module> for more information on making and using modules.
540              
541             =head2 build_request
542              
543             This method is used to create the request object for each HTTP request. It
544             returns an instance of the class defined in the request_obj attribute (defaults to
545             L<Kelp::Request>), initialized with the current request's environment. You can
546             override this method to use a custom request module if you need to do something
547             interesting. Though there is a provided attribute that can be used to overide
548             the class of the object used.
549              
550             package MyApp;
551             use MyApp::Request;
552              
553             sub build_request {
554             my ( $self, $env ) = @_;
555             return MyApp::Request->new( app => $app, env => $env );
556             }
557              
558             # Now each request will be handled by MyApp::Request
559              
560             =head2 before_finalize
561              
562             Override this method to modify the response object just before it gets
563             finalized.
564              
565             package MyApp;
566              
567             sub before_finalize {
568             my $self = shift;
569             $self->res->set_header("X-App-Name", "MyApp");
570             }
571              
572             ...
573              
574             The above is an example of how to insert a custom header into the response of
575             every route.
576              
577             =head2 build_response
578              
579             This method creates the response object, e.g. what an HTTP request will return.
580             By default the object created is L<Kelp::Response> though this can be
581             overwritten via the respone_obj attribute. Much like L</build_request>, the
582             response can also be overridden to use a custom response object if you need
583             something completely custom.
584              
585             =head2 run
586              
587             This method builds and returns the PSGI app. You can override it in order to
588             include middleware. See L<Kelp::Manual/Adding middleware> for an example.
589              
590             =head2 param
591              
592             A shortcut to C<$self-E<gt>req-E<gt>param>:
593              
594             sub some_route {
595             my $self = shift;
596             if ( $self->param('age') > 18 ) {
597             $self->can_watch_south_path(1);
598             }
599             }
600              
601             This function can be tricky to use because of context sensivity. See
602             L<Kelp::Request/param> for more information and examples.
603              
604             =head2 session
605              
606             A shortcut to C<$self-E<gt>req-E<gt>session>. Take a look at L<Kelp::Request/session>
607             for more information and examples.
608              
609             =head2 stash
610              
611             Provides safe access to C<$self-E<gt>req-E<gt>stash>. When called without
612             arguments, it will return the stash hash. If called with a single argument, it
613             will return the value of the corresponding key in the stash.
614             See L<Kelp::Request/stash> for more information and examples.
615              
616             =head2 named
617              
618             Provides safe access to C<$self-E<gt>req-E<gt>named>. When called without
619             arguments, it will return the named hash. If called with a single argument, it
620             will return the value of the corresponding key in the named hash.
621             See L<Kelp::Request/named> for more information and examples.
622              
623             =head2 url_for
624              
625             A safe shortcut to C<$self-E<gt>routes-E<gt>url>. Builds a URL from path and
626             arguments.
627              
628             sub build {
629             my $self = shift;
630             $self->routes->add("/:name/:id", { name => 'name', to => sub {
631             ...
632             }});
633             }
634              
635             sub check {
636             my $self = shift;
637             my $url_for_name = $self->url_for('name', name => 'jake', id => 1003);
638             $self->res->redirect_to( $url_for_name );
639             }
640              
641             =head2 abs_url
642              
643             Same as L</url_for>, but returns the full absolute URI for the current
644             application (based on configuration).
645              
646             =head1 AUTHOR
647              
648             Stefan Geneshky - minimal <at> cpan.org
649              
650             =head1 LICENSE
651              
652             This module and all the modules in this package are governed by the same license
653             as Perl itself.
654              
655             =cut