File Coverage

blib/lib/Prancer.pm
Criterion Covered Total %
statement 64 111 57.6
branch 0 10 0.0
condition 0 3 0.0
subroutine 19 27 70.3
pod 1 5 20.0
total 84 156 53.8


line stmt bran cond sub pod time code
1             package Prancer;
2              
3 1     1   446543 use strict;
  1         3  
  1         37  
4 1     1   6 use warnings FATAL => 'all';
  1         3  
  1         48  
5              
6 1     1   967 use version;
  1         2644  
  1         8  
7             our $VERSION = '1.05';
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   1115 use Web::Simple 'Prancer';
  1         77083  
  1         8  
13              
14 1     1   4988 use Cwd ();
  1         3  
  1         20  
15 1     1   1589298 use Module::Load ();
  1         1927  
  1         42  
16 1     1   1465 use Try::Tiny;
  1         2453  
  1         101  
17 1     1   9 use Carp;
  1         4  
  1         80  
18              
19 1     1   866 use Prancer::Core;
  1         3  
  1         49  
20 1     1   671 use Prancer::Request;
  1         4  
  1         32  
21 1     1   517 use Prancer::Response;
  1         3  
  1         29  
22 1     1   522 use Prancer::Session;
  1         3  
  1         926  
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   6 no strict 'refs';
  1         2  
  1         39  
149 1     1   5 no warnings 'redefine';
  1         1  
  1         158  
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         35  
164 1     1   5 no warnings 'redefine';
  1         1  
  1         162  
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   11 my ($class, @options) = @_;
176              
177             # store what namespace are importing things to
178 1         2 my $namespace = caller(0);
179              
180             {
181             # this block makes our caller a child class of this class
182 1     1   5 no strict 'refs';
  1         2  
  1         119  
  1         2  
183 1         2 unshift(@{"${namespace}::ISA"}, __PACKAGE__);
  1         18  
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         9 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         18 my $loaded = {};
194              
195 1         3 my @actions = ();
196 1         3 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   5 no strict 'refs';
  1         3  
  1         420  
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         14 return;
217             }
218              
219             sub to_psgi_app {
220 0     0 1   my $self = shift;
221 0 0 0       croak "cannot call ->to_psgi_app before calling ->new" unless (ref($self) && $self->isa(__PACKAGE__));
222              
223             # get the PSGI app from Web::Simple and wrap middleware around it
224 0           my $app = $self->SUPER::to_psgi_app();
225              
226             # enable static document loading
227 0           $app = $enable_static->($self, $app);
228              
229             # enable sessions
230 0           $app = $enable_sessions->($self, $app);
231              
232 0           return $app;
233             }
234              
235             # NOTE: your program can definitely implement ->dispatch_request instead of
236             # ->handler but ->handler will give you easier access to request and response
237             # data using Prancer::Request and Prancer::Response.
238             sub dispatch_request {
239 0     0 0   my ($self, $env) = @_;
240              
241 0           my $request = Prancer::Request->new($env);
242 0           my $response = Prancer::Response->new();
243 0           my $session = Prancer::Session->new($env);
244              
245 0           return $self->handler($env, $request, $response, $session);
246             }
247              
248             sub handler {
249 0     0 0   croak "->handler must be implemented in child class";
250             }
251              
252             sub initialize {
253 0     0 0   return;
254             }
255              
256             1;
257              
258             =head1 NAME
259              
260             Prancer
261              
262             =head1 SYNOPSIS
263              
264             When using as part of a web application:
265              
266             ===> foobar.yml
267              
268             session:
269             state:
270             driver: Prancer::Session::State::Cookie
271             options:
272             session_key: PSESSION
273             store:
274             driver: Prancer::Session::Store::Storable
275             options:
276             dir: /tmp/prancer/sessions
277              
278             static:
279             path: /static
280             dir: /srv/www/resources
281              
282             ===> myapp.psgi
283              
284             #!/usr/bin/env perl
285              
286             use strict;
287             use warnings;
288             use Plack::Runner;
289              
290             # this just returns a PSGI application. $x can be wrapped with additional
291             # middleware before sending it along to Plack::Runner.
292             my $x = MyApp->new("/path/to/foobar.yml")->to_psgi_app();
293              
294             # run the psgi app through Plack and send it everything from @ARGV. this
295             # way Plack::Runner will get options like what listening port to use and
296             # application server to use -- Starman, Twiggy, etc.
297             my $runner = Plack::Runner->new();
298             $runner->parse_options(@ARGV);
299             $runner->run($x);
300              
301             ===> MyApp.pm
302              
303             package MyApp;
304              
305             use strict;
306             use warnings;
307              
308             use Prancer qw(config);
309              
310             sub initialize {
311             my $self = shift;
312              
313             # in here we can initialize things like plugins
314             # but this method is not required to be implemented
315              
316             return;
317             }
318              
319             sub handler {
320             my ($self, $env, $request, $response, $session) = @_;
321              
322             sub (GET + /) {
323             $response->header("Content-Type" => "text/plain");
324             $response->body("Hello, world!");
325             return $response->finalize(200);
326             }, sub (GET + /foo) {
327             $response->header("Content-Type" => "text/plain");
328             $response->body(sub {
329             my $writer = shift;
330             $writer->write("Hello, world!");
331             $writer->close();
332             return;
333             });
334             }
335             }
336              
337             1;
338              
339             If you save the above snippet as C and run it like this:
340              
341             plackup myapp.psgi
342              
343             You will get "Hello, world!" in your browser. Or you can use Prancer as part of
344             a standalone command line application:
345              
346             #!/usr/bin/env perl
347              
348             use strict;
349             use warnings;
350              
351             use Prancer::Core qw(config);
352              
353             # the advantage to using Prancer in a standalone application is the ability
354             # to use a standard configuration and to load plugins for things like
355             # loggers and database connectors and template engines.
356             my $x = Prancer::Core->new("/path/to/foobar.yml");
357             print "Hello, world!;
358              
359             =head1 DESCRIPTION
360              
361             Prancer is yet another PSGI framework that provides routing and session
362             management as well as plugins for logging, database access, and template
363             engines. It does this by wrapping L to handle routing and by
364             wrapping other libraries to bring easy access to things that need to be done in
365             web applications.
366              
367             There are two parts to using Prancer for a web application: a package to
368             contain your application and a script to call your application. Both are
369             necessary.
370              
371             The package containing your application should contain a line like this:
372              
373             use Prancer;
374              
375             This modifies your application package such that it inherits from Prancer. It
376             also means that your package must implement the C method and
377             optionally implement the C method. As Prancer inherits from
378             Web::Simple it will also automatically enable the C and C
379             pragmas.
380              
381             As mentioned, putting C at the top of your package will require
382             you to implement the C method, like this:
383              
384             sub handler {
385             my ($self, $env, $request, $response, $session) = @_;
386              
387             # routing goes in here.
388             # see Web::Simple for documentation on writing routing rules.
389             sub (GET + /) {
390             $response->header("Content-Type" => "text/plain");
391             $response->body("Hello, world!");
392             return $response->finalize(200);
393             }
394             }
395              
396             The C<$request> variable is a L object. The C<$response>
397             variable is a L object. The C<$session> variable is a
398             L object. If there is no configuration for sessions in any of
399             your configuration files then C<$session> will be C.
400              
401             You may implement your own C method in your application but you B
402             call C<$class-ESUPER::new(@_);> to get the configuration file loaded and
403             any methods exported. As an alternative to implemeting C and remembering
404             to call C, Prancer will make a call to C<-Einitialize> at the
405             end of its own implementation of C so things that you might put in C
406             can instead be put into C, like this:
407              
408             sub initialize {
409             my $self = shift;
410              
411             # this is where you can initialize things when your package is created
412              
413             return;
414             }
415              
416             By default, Prancer does not export anything into your package's namespace.
417             However, that doesn't mean that there is not anything that it I export
418             were one to ask:
419              
420             use Prancer qw(config);
421              
422             Importing C will make the keyword C available which gives
423             access to any configuration options loaded by Prancer.
424              
425             The second part of the Prancer equation is the script that creates and calls
426             your package. This can be a pretty small and standard little script, like this:
427              
428             my $myapp = MyApp->new("/path/to/foobar.yml")
429             my $psgi = $myapp->to_psgi_app();
430              
431             C<$myapp> is just an instance of your package. You can pass to C either
432             one specific configuration file or a directory containing lots of configuration
433             files. The functionality is documented in C.
434              
435             C<$psgi> is just a PSGI app that you can send to L or whatever
436             you use to run PSGI apps. You can also wrap middleware around C<$app>.
437              
438             my $psgi = $myapp->to_psgi_app();
439             $psgi = Plack::Middleware::Runtime->wrap($psgi);
440              
441             =head1 CONFIGURATION
442              
443             Prancer needs a configuration file. Ok, it doesn't I a configuration
444             file. By default, Prancer does not require any configuration. But it is less
445             useful without one. You I always create your application like this:
446              
447             my $app = MyApp->new->to_psgi_app();
448              
449             How Prancer loads configuration files is documented in L.
450             Anything you put into your configuration file is available to your application.
451              
452             There are two special configuration keys reserved by Prancer. The key
453             C will configure Prancer's session as documented in
454             L. The key C will configure static file loading
455             through L.
456              
457             To configure static file loading you can add this to your configuration file:
458              
459             static:
460             path: /static
461             dir: /path/to/my/resources
462              
463             The C option is required to indicate the root directory for your static
464             resources. The C option indicates the web path to link to your static
465             resources. If no path is not provided then static files can be accessed under
466             C by default.
467              
468             =head1 CREDITS
469              
470             This module could have been written except on the shoulders of the following
471             giants:
472              
473             =over
474              
475             =item
476              
477             The name "Prancer" is a riff on the popular PSGI framework L and
478             L. L is derived directly from
479             L. Thank you to the Dancer/Dancer2 teams.
480              
481             =item
482              
483             L is derived from L. Thank you to
484             David Precious.
485              
486             =item
487              
488             L, L, L,
489             L and the session packages are but thin wrappers with minor
490             modifications to L, L,
491             L, and L. Thank you to Tatsuhiko
492             Miyagawa.
493              
494             =item
495              
496             The entire routing functionality of this module is offloaded to L.
497             Thank you to Matt Trout for some great code that I am able to easily leverage.
498              
499             =back
500              
501             =head1 COPYRIGHT
502              
503             Copyright 2013, 2014 Paul Lockaby. All rights reserved.
504              
505             This library is free software; you can redistribute it and/or modify it under
506             the same terms as Perl itself.
507              
508             =head1 SEE ALSO
509              
510             =over
511              
512             =item
513              
514             L
515              
516             =item
517              
518             L
519              
520             =back
521              
522             =cut