File Coverage

blib/lib/Prancer.pm
Criterion Covered Total %
statement 64 110 58.1
branch 0 8 0.0
condition n/a
subroutine 19 27 70.3
pod 1 5 20.0
total 84 150 56.0


line stmt bran cond sub pod time code
1             package Prancer;
2              
3 1     1   15732 use strict;
  1         1  
  1         35  
4 1     1   4 use warnings FATAL => 'all';
  1         1  
  1         35  
5              
6 1     1   408 use version;
  1         1432  
  1         4  
7             our $VERSION = '1.02';
8              
9             # using Web::Simple in this context will implicitly make Prancer a subclass of
10             # Web::Simple::Application. that will cause a number of things to be imported
11             # into the Prancer namespace. see ->import below for more details.
12 1     1   626 use Web::Simple 'Prancer';
  1         43626  
  1         6  
13              
14 1     1   3320 use Cwd ();
  1         2  
  1         11  
15 1     1   458 use Module::Load ();
  1         844  
  1         17  
16 1     1   464 use Try::Tiny;
  1         1164  
  1         46  
17 1     1   5 use Carp;
  1         0  
  1         37  
18              
19 1     1   357 use Prancer::Core;
  1         3  
  1         45  
20 1     1   451 use Prancer::Request;
  1         2  
  1         32  
21 1     1   335 use Prancer::Response;
  1         2  
  1         27  
22 1     1   385 use Prancer::Session;
  1         3  
  1         733  
23              
24             # even though this *should* work automatically, it was not
25             our @CARP_NOT = qw(Prancer Try::Tiny);
26              
27             # the list of methods that will be created on the fly, linked to private
28             # methods of the same name, and exported to the caller. this makes things like
29             # the bareword call to "config" work. this list is populated in ->import
30             our @TO_EXPORT = ();
31              
32             # a super private method
33             my $enable_static = sub {
34             my ($self, $app) = @_;
35             return $app unless defined($self->{'_core'}->config());
36              
37             my $config = $self->{'_core'}->config->get('static');
38             return $app unless defined($config);
39              
40             try {
41             # this intercepts requests for documents under the configured URL
42             # and checks to see if the requested file exists in the configured
43             # file system path. if it does exist then it is served up. if it
44             # doesn't exist then the request will pass through to the handler.
45             die "no directory is configured for the static file loader\n" unless defined($config->{'dir'});
46             my $dir = Cwd::realpath($config->{'dir'});
47             die "${\$config->{'dir'}} does not exist\n" unless defined($dir);
48             die "${\$config->{'dir'}} is not readable\n" unless (-r $dir);
49              
50             # this is the url under which static files will be stored
51             my $path = $config->{'path'} || '/static';
52              
53             require Plack::Middleware::Static;
54             $app = Plack::Middleware::Static->wrap($app,
55             'path' => sub { s/^$path//x },
56             'root' => $dir,
57             'pass_through' => 1,
58             );
59             } catch {
60             my $error = (defined($_) ? $_ : "unknown");
61             carp "initialization warning generated while trying to load the static file loader: ${error}";
62             };
63              
64             return $app;
65             };
66              
67             # a super private method
68             my $enable_sessions = sub {
69             my ($self, $app) = @_;
70             return $app unless defined($self->{'_core'}->config());
71              
72             my $config = $self->{'_core'}->config->get('session');
73             return $app unless defined($config);
74              
75             try {
76             # load the session state package first
77             # this will probably be a cookie
78             my $state_package = undef;
79             my $state_options = undef;
80             if (ref($config->{'state'}) && ref($config->{'state'}) eq "HASH") {
81             $state_package = $config->{'state'}->{'driver'};
82             $state_options = $config->{'state'}->{'options'};
83             }
84              
85             # make sure state options are legit
86             if (defined($state_options) && (!ref($state_options) || ref($state_options) ne "HASH")) {
87             die "session state configuration options are invalid -- expected a HASH\n";
88             }
89              
90             # set defaults and then load the state package
91             $state_package ||= "Prancer::Session::State::Cookie";
92             $state_options ||= {};
93             Module::Load::load($state_package);
94              
95             # set the default for the cookie name because the plack default is dumb
96             $state_options->{'session_key'} ||= (delete($state_options->{'key'}) || "PSESSION");
97              
98             # now load the store package
99             my $store_package = undef;
100             my $store_options = undef;
101             if (ref($config->{'store'}) && ref($config->{'store'}) eq "HASH") {
102             $store_package = $config->{'store'}->{'driver'};
103             $store_options = $config->{'store'}->{'options'};
104             }
105              
106             # make sure store options are legit
107             if (defined($store_options) && (!ref($store_options) || ref($store_options) ne "HASH")) {
108             die "session store configuration options are invalid -- expected a HASH\n";
109             }
110              
111             # set defaults and then load the store package
112             $store_package ||= "Prancer::Session::Store::Memory";
113             $store_options ||= {};
114             Module::Load::load($store_package);
115              
116             require Plack::Middleware::Session;
117             $app = Plack::Middleware::Session->wrap($app,
118             'state' => $state_package->new(%{$state_options}),
119             'store' => $store_package->new(%{$store_options}),
120             );
121             } catch {
122             my $error = (defined($_) ? $_ : "unknown");
123             carp "initialization warning generated while trying to load the session handler: ${error}";
124             };
125              
126             return $app;
127             };
128              
129             sub new {
130 0     0 0 0 my ($class, $configuration_file) = @_;
131 0         0 my $self = bless({}, $class);
132              
133             # the core is where our methods *really* live
134             # we mostly just proxy through to that
135 0         0 $self->{'_core'} = Prancer::Core->new($configuration_file);
136              
137             # @TO_EXPORT is an array of arrayrefs representing methods that we want to
138             # make available in our caller's namespace. each arrayref has two values:
139             #
140             # 0 = namespace into which we'll import the method
141             # 1 = the method that will be imported (must be implemented in Prancer::Core)
142             #
143             # this makes "namespace::method" resolve to "$self->{'_core'}->method()".
144 0         0 for my $method (@TO_EXPORT) {
145             # don't import things that can't be resolved
146 0 0       0 croak "Prancer::Core does not implement ${\$method->[1]}" unless $self->{'_core'}->can($method->[1]);
  0         0  
147              
148 1     1   5 no strict 'refs';
  1         2  
  1         29  
149 1     1   4 no warnings 'redefine';
  1         0  
  1         133  
150 0         0 *{"${\$method->[0]}::${\$method->[1]}"} = sub {
  0         0  
  0         0  
151 0     0   0 my $internal = "${\$method->[1]}";
  0         0  
152 0         0 return $self->{'_core'}->$internal(@_);
153 0         0 };
154             }
155              
156             # here are things that will always be exported into the Prancer namespace.
157             # this DOES NOT export things things into our children's namespace, only
158             # into the Prancer namespace. this makes things like "$app->config()" work.
159 0         0 for my $method (qw(config)) {
160             # don't export things that can't be resolved
161 0 0       0 croak "Prancer::Core does not implement ${\$method->[1]}" unless $self->{'_core'}->can($method);
  0         0  
162              
163 1     1   4 no strict 'refs';
  1         2  
  1         24  
164 1     1   3 no warnings 'redefine';
  1         1  
  1         153  
165 0         0 *{"${\__PACKAGE__}::${method}"} = sub {
  0         0  
166 0     0   0 return $self->{'_core'}->$method(@_);
167 0         0 };
168             }
169              
170 0         0 $self->initialize();
171 0         0 return $self;
172             }
173              
174             sub import {
175 1     1   9 my ($class, @options) = @_;
176              
177             # store what namespace are importing things to
178 1         1 my $namespace = caller(0);
179              
180             {
181             # this block makes our caller a child class of this class
182 1     1   7 no strict 'refs';
  1         2  
  1         138  
  1         2  
183 1         1 unshift(@{"${namespace}::ISA"}, __PACKAGE__);
  1         15  
184             }
185              
186             # this is used by Web::Simple to not complain about keywords in prototypes
187             # like HEAD and GET. but we need to extend it to classes that implement us
188             # so it is being adding it here, too.
189 1         7 warnings::illegalproto->unimport();
190              
191             # keep track of what has been loaded so someone doesn't put the same thing
192             # into the import list in twice.
193 1         17 my $loaded = {};
194              
195 1         2 my @actions = ();
196 1         1 for my $option (@options) {
197 0 0       0 next if exists($loaded->{$option});
198 0         0 $loaded->{$option} = 1;
199              
200             # these options will be exported as proxies to real methods
201 0 0       0 if ($option =~ /^(config)$/x) {
202 1     1   4 no strict 'refs';
  1         2  
  1         308  
203              
204             # need to predefine the exported method so that barewords work
205 0     0   0 *{"${\__PACKAGE__}::${1}"} = *{"${namespace}::${1}"} = sub { return; };
  0         0  
  0         0  
  0         0  
  0         0  
206              
207             # this will tell ->new() to create the actual method
208 0         0 push(@TO_EXPORT, [ $namespace, $1 ]);
209              
210 0         0 next;
211             }
212              
213 0         0 croak "${option} is not exported by the ${\__PACKAGE__} package";
  0         0  
214             }
215              
216 1         12 return;
217             }
218              
219             sub to_psgi_app {
220 0     0 1   my $self = shift;
221              
222             # get the PSGI app from Web::Simple and wrap middleware around it
223 0           my $app = $self->SUPER::to_psgi_app();
224              
225             # enable static document loading
226 0           $app = $enable_static->($self, $app);
227              
228             # enable sessions
229 0           $app = $enable_sessions->($self, $app);
230              
231 0           return $app;
232             }
233              
234             # NOTE: your program can definitely implement ->dispatch_request instead of
235             # ->handler but ->handler will give you easier access to request and response
236             # data using Prancer::Request and Prancer::Response.
237             sub dispatch_request {
238 0     0 0   my ($self, $env) = @_;
239              
240 0           my $request = Prancer::Request->new($env);
241 0           my $response = Prancer::Response->new($env);
242 0           my $session = Prancer::Session->new($env);
243              
244 0           return $self->handler($env, $request, $response, $session);
245             }
246              
247             sub handler {
248 0     0 0   croak "->handler must be implemented in child class";
249             }
250              
251             sub initialize {
252 0     0 0   return;
253             }
254              
255             1;
256              
257             =head1 NAME
258              
259             Prancer
260              
261             =head1 SYNOPSIS
262              
263             When using as part of a web application:
264              
265             ===> foobar.yml
266              
267             session:
268             state:
269             driver: Prancer::Session::State::Cookie
270             options:
271             session_key: PSESSION
272             store:
273             driver: Prancer::Session::Store::Storable
274             options:
275             dir: /tmp/prancer/sessions
276              
277             static:
278             path: /static
279             dir: /srv/www/resources
280              
281             ===> myapp.psgi
282              
283             #!/usr/bin/env perl
284              
285             use strict;
286             use warnings;
287             use Plack::Runner;
288              
289             # this just returns a PSGI application. $x can be wrapped with additional
290             # middleware before sending it along to Plack::Runner.
291             my $x = MyApp->new("/path/to/foobar.yml")->to_psgi_app();
292              
293             # run the psgi app through Plack and send it everything from @ARGV. this
294             # way Plack::Runner will get options like what listening port to use and
295             # application server to use -- Starman, Twiggy, etc.
296             my $runner = Plack::Runner->new();
297             $runner->parse_options(@ARGV);
298             $runner->run($x);
299              
300             ===> MyApp.pm
301              
302             package MyApp;
303              
304             use strict;
305             use warnings;
306              
307             use Prancer qw(config);
308              
309             sub initialize {
310             my $self = shift;
311              
312             # in here we can initialize things like plugins
313             # but this method is not required to be implemented
314              
315             return;
316             }
317              
318             sub handler {
319             my ($self, $env, $request, $response, $session) = @_;
320              
321             sub (GET + /) {
322             $response->header("Content-Type" => "text/plain");
323             $response->body("Hello, world!");
324             return $response->finalize(200);
325             }, sub (GET + /foo) {
326             $response->header("Content-Type" => "text/plain");
327             $response->body(sub {
328             my $writer = shift;
329             $writer->write("Hello, world!");
330             $writer->close();
331             return;
332             });
333             }
334             }
335              
336             1;
337              
338             If you save the above snippet as C and run it like this:
339              
340             plackup myapp.psgi
341              
342             You will get "Hello, world!" in your browser. Or you can use Prancer as part of
343             a standalone command line application:
344              
345             #!/usr/bin/env perl
346              
347             use strict;
348             use warnings;
349              
350             use Prancer::Core qw(config);
351              
352             # the advantage to using Prancer in a standalone application is the ability
353             # to use a standard configuration and to load plugins for things like
354             # loggers and database connectors and template engines.
355             my $x = Prancer::Core->new("/path/to/foobar.yml");
356             print "Hello, world!;
357              
358             =head1 DESCRIPTION
359              
360             Prancer is yet another PSGI framework that provides routing and session
361             management as well as plugins for logging, database access, and template
362             engines. It does this by wrapping L to handle routing and by
363             wrapping other libraries to bring easy access to things that need to be done in
364             web applications.
365              
366             There are two parts to using Prancer for a web application: a package to
367             contain your application and a script to call your application. Both are
368             necessary.
369              
370             The package containing your application should contain a line like this:
371              
372             use Prancer;
373              
374             This modifies your application package such that it inherits from Prancer. It
375             also means that your package must implement the C method and
376             optionally implement the C method. As Prancer inherits from
377             Web::Simple it will also automatically enable the C and C
378             pragmas.
379              
380             As mentioned, putting C at the top of your package will require
381             you to implement the C method, like this:
382              
383             sub handler {
384             my ($self, $env, $request, $response, $session) = @_;
385              
386             # routing goes in here.
387             # see Web::Simple for documentation on writing routing rules.
388             sub (GET + /) {
389             $response->header("Content-Type" => "text/plain");
390             $response->body("Hello, world!");
391             return $response->finalize(200);
392             }
393             }
394              
395             The C<$request> variable is a L object. The C<$response>
396             variable is a L object. The C<$session> variable is a
397             L object. If there is no configuration for sessions in any of
398             your configuration files then C<$session> will be C.
399              
400             You may implement your own C method in your application but you B
401             call C<$class-ESUPER::new(@_);> to get the configuration file loaded and
402             any methods exported. As an alternative to implemeting C and remembering
403             to call C, Prancer will make a call to C<-Einitialize> at the
404             end of its own implementation of C so things that you might put in C
405             can instead be put into C, like this:
406              
407             sub initialize {
408             my $self = shift;
409              
410             # this is where you can initialize things when your package is created
411              
412             return;
413             }
414              
415             By default, Prancer does not export anything into your package's namespace.
416             However, that doesn't mean that there is not anything that it I export
417             were one to ask:
418              
419             use Prancer qw(config);
420              
421             Importing C will make the keyword C available which gives
422             access to any configuration options loaded by Prancer.
423              
424             The second part of the Prancer equation is the script that creates and calls
425             your package. This can be a pretty small and standard little script, like this:
426              
427             my $myapp = MyApp->new("/path/to/foobar.yml")
428             my $psgi = $myapp->to_psgi_app();
429              
430             C<$myapp> is just an instance of your package. You can pass to C either
431             one specific configuration file or a directory containing lots of configuration
432             files. The functionality is documented in C.
433              
434             C<$psgi> is just a PSGI app that you can send to L or whatever
435             you use to run PSGI apps. You can also wrap middleware around C<$app>.
436              
437             my $psgi = $myapp->to_psgi_app();
438             $psgi = Plack::Middleware::Runtime->wrap($psgi);
439              
440             =head1 CONFIGURATION
441              
442             Prancer needs a configuration file. Ok, it doesn't I a configuration
443             file. By default, Prancer does not require any configuration. But it is less
444             useful without one. You I always create your application like this:
445              
446             my $app = MyApp->new->to_psgi_app();
447              
448             How Prancer loads configuration files is documented in L.
449             Anything you put into your configuration file is available to your application.
450              
451             There are two special configuration keys reserved by Prancer. The key
452             C will configure Prancer's session as documented in
453             L. The key C will configure static file loading
454             through L.
455              
456             To configure static file loading you can add this to your configuration file:
457              
458             static:
459             path: /static
460             dir: /path/to/my/resources
461              
462             The C option is required to indicate the root directory for your static
463             resources. The C option indicates the web path to link to your static
464             resources. If no path is not provided then static files can be accessed under
465             C by default.
466              
467             =head1 CREDITS
468              
469             This module could have been written except on the shoulders of the following
470             giants:
471              
472             =over
473              
474             =item
475              
476             The name "Prancer" is a riff on the popular PSGI framework L and
477             L. L is derived directly from
478             L. Thank you to the Dancer/Dancer2 teams.
479              
480             =item
481              
482             L is derived from L. Thank you to
483             David Precious.
484              
485             =item
486              
487             L, L, L,
488             L and the session packages are but thin wrappers with minor
489             modifications to L, L,
490             L, and L. Thank you to Tatsuhiko
491             Miyagawa.
492              
493             =item
494              
495             The entire routing functionality of this module is offloaded to L.
496             Thank you to Matt Trout for some great code that I am able to easily leverage.
497              
498             =back
499              
500             =head1 COPYRIGHT
501              
502             Copyright 2013, 2014 Paul Lockaby. All rights reserved.
503              
504             This library is free software; you can redistribute it and/or modify it under
505             the same terms as Perl itself.
506              
507             =head1 SEE ALSO
508              
509             =over
510              
511             =item L
512             =item L
513              
514             =back
515              
516             =cut