File Coverage

blib/lib/Pulp.pm
Criterion Covered Total %
statement 66 226 29.2
branch 5 74 6.7
condition 1 9 11.1
subroutine 11 26 42.3
pod 0 1 0.0
total 83 336 24.7


line stmt bran cond sub pod time code
1             package Pulp;
2              
3 1     1   24982 use warnings;
  1         3  
  1         45  
4 1     1   8 use strict;
  1         2  
  1         57  
5 1     1   642 use true;
  1         26494  
  1         5  
6 1     1   1894 use Text::ASCIITable;
  1         17418  
  1         66  
7 1     1   580 use FindBin;
  1         950  
  1         81  
8 1     1   450 use Module::Find 'useall';
  1         1239  
  1         73  
9 1     1   6 use base 'Kelp';
  1         1  
  1         775  
10              
11             our $VERSION = '0.001';
12              
13             sub import {
14 1     1   10 my ($class, %opts) = @_;
15 1         11 strict->import();
16 1         11 warnings->import();
17 1         6 true->import();
18 1         741 my $caller = caller;
19 1         2 my $routes = [];
20 1         1 my $configs = {};
21 1         11 my $auto = 0;
22             {
23 1     1   92019 no strict 'refs';
  1         3  
  1         1532  
  1         1  
24 1         1 push @{"${caller}::ISA"}, 'Kelp';
  1         10  
25             # check args
26             # v2
27 1 50       4 if ($opts{"-v2"}) {
28 0         0 my $con_tb = Text::ASCIITable->new({ headingText => 'Controllers' });
29 0         0 $con_tb->setCols('Action', 'Path');
30 0         0 my @conts = useall "${caller}::Controller";
31 0         0 for my $mod (@conts) {
32 0         0 my $actions = $mod->_actions;
33 0         0 foreach my $action (keys %$actions) {
34 0         0 $con_tb->addRow($action, $actions->{$action});
35             }
36             }
37              
38 0         0 print $con_tb . "\n";
39             }
40              
41             # auto load routes?
42 1         2 my @in_mods;
43 1 50       2 if ($opts{"extends"}) {
44 0         0 @in_mods = useall $opts{"extends"} . "::Route";
45             }
46              
47 1 50 33     5 if ($opts{"-auto"} || $opts{"extends"}) {
48 0         0 $auto = 1;
49 0         0 my $route_tb = Text::ASCIITable->new;
50 0         0 $route_tb->setCols('Routes');
51 0         0 my @mod_routes = useall "${caller}::Route";
52 0         0 for my $mod (@mod_routes) {
53 0         0 $route_tb->addRow($mod);
54 0         0 push @$routes, $mod->get_routes();
55             }
56              
57 0 0       0 if (@in_mods) {
58 0         0 for my $mod (@in_mods) {
59 0         0 $route_tb->addRow($mod);
60 0         0 push @$routes, $mod->get_routes();
61             }
62             }
63              
64 0         0 print $route_tb . "\n";
65             }
66              
67 1     0   3 *{"${caller}::new"} = sub { return shift->SUPER::new(@_); };
  1         9  
  0         0  
68 1         4 *{"${caller}::maps"} = sub {
69 0 0   0   0 die "Please don't use -auto and maps at the same time\n"
70             if $auto;
71              
72 0         0 my ($route_names) = @_;
73 0 0       0 unless (ref $route_names eq 'ARRAY') {
74 0         0 die "routes() expects an array references";
75             }
76              
77 0         0 my $route_tb = Text::ASCIITable->new;
78 0         0 $route_tb->setCols('Routes');
79 0         0 for my $mod (@$route_names) {
80 0         0 my $route_path = "${caller}::Route::${mod}";
81 0         0 eval "use $route_path;";
82 0 0       0 if ($@) {
83 0         0 warn "Could not load route ${route_path}: $@";
84 0         0 next;
85             }
86              
87 0         0 $route_tb->addRow($route_path);
88 0         0 push @$routes, $route_path->get_routes();
89             }
90              
91 0         0 print $route_tb . "\n";
92 1         2 };
93              
94 1         3 *{"${caller}::model"} = sub {
95 0     0   0 my ($self, $model) = @_;
96 0         0 return $self->{_models}->{$model};
97 1         1 };
98              
99 1     0   2 *{"${caller}::path_to"} = sub { return $FindBin::Bin; };
  1         3  
  0         0  
100              
101 1         3 *{"${caller}::cfg"} = sub {
102 0     0   0 my ($key, $hash) = @_;
103 0         0 $configs->{$key} = $hash;
104 1         1 };
105              
106 1         2 *{"${caller}::build"} = sub {
107 0     0   0 my ($self) = @_;
108 0         0 my $config = $self->config_hash;
109             # config
110 0 0       0 if (scalar keys %$configs > 0) {
111 0         0 for my $key (keys %$configs) {
112 0         0 $config->{"+${key}"} = $configs->{$key};
113             }
114             }
115            
116             # models
117 0 0       0 if ($config->{models}) {
118 0         0 $self->{_models} = {};
119 0         0 my $model_tb = Text::ASCIITable->new;
120 0         0 $model_tb->setCols('Model', 'Alias');
121 0 0       0 unless (ref $config->{models} eq 'HASH') {
122 0         0 die "config: models expects a hash reference\n";
123             }
124              
125 0         0 for my $model (keys %{$config->{models}}) {
  0         0  
126 0         0 my $name = $model;
127 0         0 my $opts = $config->{models}->{$model};
128 0         0 my $mod = $opts->{model};
129 0         0 eval "use $mod;";
130 0 0       0 if ($@) {
131 0         0 die "Could not load model $mod: $@";
132             }
133              
134 0         0 my @args = @{$opts->{args}};
  0         0  
135 0 0       0 if (my $ret = $mod->build(@args)) {
136 0 0       0 if (ref $ret) {
137 0         0 $model_tb->addRow($mod, $name);
138             # returned a standard hash reference
139 0 0       0 if (ref $ret eq 'HASH') {
140 0         0 foreach my $key (keys %$ret) {
141 0 0       0 if (ref $ret->{$key}) {
142 0         0 $self->{_models}->{"${name}::${key}"} = $ret->{$key};
143 0         0 $model_tb->addRow(ref $ret->{$key}, "${name}::${key}");
144             }
145             }
146             }
147             else {
148 0         0 $self->{_models}->{"${name}"} = $ret;
149              
150             # is this dbix::class?
151 0         0 require mro;
152 0         0 my $dbref = ref $ret;
153 0 0       0 if (grep { $_ eq 'DBIx::Class::Schema' } @{mro::get_linear_isa($dbref)}) {
  0         0  
  0         0  
154 0 0       0 if ($dbref->can('sources')) {
155 0         0 my $use_api = $mod->_use_api;
156 0         0 my @sources = $dbref->sources;
157 0         0 for my $source (@sources) {
158 0         0 $self->{_models}->{"${name}::${source}"} = $ret->resultset($source);
159 0         0 $model_tb->addRow("${dbref}::ResultSet::${source}", "${name}::${source}");
160              
161 0 0       0 if ($use_api) {
162 0         0 my $lc_source = lc $source;
163             $self->routes->add(['GET' => "/api/${lc_source}/list" ], { to => sub {
164 0     0   0 my ($aself) = @_;
165 0         0 my @data;
166 0         0 my @users = $aself->model("${name}::${source}")->all;
167 0         0 for my $user (@users) {
168 0         0 my %usr_data = $user->get_columns;
169 0         0 push @data, \%usr_data;
170             }
171              
172 0         0 return \@data;
173 0         0 }});
174              
175             $self->routes->add(['GET' => "/api/${lc_source}/find/:id"], { to => sub {
176 0     0   0 my ($aself, $id) = @_;
177 0         0 my %data = $aself->model("${name}::${source}")->find($id)->get_columns;
178 0         0 return \%data;
179 0         0 }});
180              
181             $self->routes->add(['GET' => "/api/${lc_source}/search"], { to => sub {
182 0     0   0 my ($aself) = @_;
183 0         0 my @users = $aself->model("${name}::${source}")
184 0         0 ->search({ map { $_ => $aself->param($_) } $aself->param });
185 0         0 my @data;
186 0         0 for my $user (@users) {
187 0         0 my %usr_data = $user->get_columns;
188 0         0 push @data, \%usr_data;
189             }
190              
191 0         0 return \@data;
192 0         0 }});
193             }
194             }
195             }
196             }
197             }
198             }
199             else {
200 0         0 die "Did not return a valid object from models build(): $name\n";
201             }
202             }
203             else {
204 0         0 die "build() failed: $mod";
205             }
206             }
207              
208 0 0       0 if (scalar keys %{$self->{_models}} > 0) {
  0         0  
209 0         0 print $model_tb . "\n";
210             }
211             }
212             # routes
213 0         0 my $r = $self->routes;
214 0         0 for my $route (@$routes) {
215 0         0 for my $url (keys %$route) {
216 0 0       0 if ($route->{$url}->{bridge}) {
    0          
217 0         0 $r->add([ uc($route->{$url}->{type}) => $url ], { to => $route->{$url}->{coderef}, bridge => 1 });
218             }
219             elsif ($route->{$url}->{type} eq 'any') {
220 0         0 $r->add($url, $route->{$url}->{coderef});
221             }
222             else {
223 0         0 $r->add([ uc($route->{$url}->{type}) => $url ], $route->{$url}->{coderef});
224             }
225             }
226             }
227 1         4 };
228              
229 1         3 *{"${caller}::detach"} = sub {
230 0     0   0 my ($self) = @_;
231              
232 0         0 my @caller = caller(1);
233 0         0 my $fullpath = $caller[3];
234 0         0 my $name;
235 0 0       0 if ($fullpath =~ /.+::(.+)$/) {
236 0         0 $name = $1;
237             }
238              
239 0 0       0 if ($name) {
240 0 0       0 print "[debug] Rendering template: $name\n" if $ENV{KELPX_SWEET_DEBUG};
241 0         0 $self->template($name, $self->stash);
242             }
243 1         6 };
244              
245             # if 'has' is not available (ie: no Moose, Moo, Mouse, etc), then import our own small version
246 1 50       10 unless ($caller->can('has')) {
247 1         1 *{"${caller}::has"} = \&_has;
  1         2  
248             }
249              
250             # if 'around' is not available, import a small version of our own
251             {
252 1     1   6 no warnings 'redefine';
  1         2  
  1         254  
  1         1  
253 1 50       5 unless ($caller->can('around')) {
254 1         15 *{"${caller}::around"} = sub {
255 0     0     my ($method, $code) = @_;
256              
257 0           my $fullpkg = "${caller}::${method}";
258 0           my $old_code = \&$fullpkg;
259 0           *{"${fullpkg}"} = sub {
260 0     0     $code->($old_code, @_);
261 0           };
262 1         3 };
263             }
264             }
265             }
266             }
267              
268             sub _has {
269 0     0     my ($acc, %attrs) = @_;
270 0           my $class = caller;
271 0           my $ro = 0;
272 0           my $rq = 0;
273 0           my $df;
274 0 0         if (%attrs) {
275 0 0         if ($attrs{is} eq 'ro') { $ro = 1; }
  0            
276 0 0         if ($attrs{required}) { $rq = 1; }
  0            
277 0 0         if ($attrs{default}) { $df = $attrs{default}; }
  0            
278              
279 0 0         if ($df) {
280 0 0         die "has: default expects a code reference\n"
281             unless ref $df eq 'CODE';
282             }
283             }
284            
285             {
286 1     1   6 no strict 'refs';
  1         1  
  1         226  
  0            
287 0           *{"${class}::${acc}"} = sub {
288             #if ($attrs{default}) { $_[0]->{$name} = $attrs{default}; }
289 0 0 0 0     if ($rq and not $df) {
290 0 0 0       if (not $_[0]->{$acc} and not $_[1]) {
291 0           die "You attempted to use a field that can't be left blank: ${acc}";
292             }
293             }
294              
295 0 0         if ($df) { $_[0]->{$acc} = $df->(); }
  0            
296            
297 0 0         if (@_ == 2) {
298 0 0         die "Can't modify a readonly accessor: ${acc}"
299             if $ro;
300 0           $_[0]->{$acc} = $_[1];
301             }
302 0           return $_[0]->{$acc};
303 0           };
304             }
305             }
306              
307             sub new {
308 0     0 0   bless { @_[ 1 .. $#_ ] }, $_[0];
309             }
310              
311             =head1 NAME
312              
313             Pulp - Give your Kelp applications more juice
314              
315             =head1 DESCRIPTION
316              
317             Kelp is good. Kelp is great. But what if you could give it more syntactic sugar and separate your routes from the logic in a cleaner way? Pulp attempts to do just that.
318              
319             =head1 SIMPLE TUTORIAL
320              
321             For the most part, your original C will remain the same as Kelps.
322              
323             B
324            
325             package MyApp;
326             use Pulp;
327              
328             maps ['Main'];
329              
330             Yep, that's the complete code for your base. You pass C an array reference of the routes you want to include.
331             It will look for them in C. So the above example will load C.
332             Next, let's create that file
333              
334             B
335              
336             package MyApp::Route::Main;
337              
338             use Pulp::Route;
339              
340             get '/' => 'Controller::Root::hello';
341             get '/nocontroller' => sub { 'Hello, world from no controller!' };
342              
343             Simply use C, then create your route definitions here. You're welcome to put your logic inside code refs,
344             but that makes the whole idea of this module pointless ;)
345             It will load C then whatever you pass to it. So the '/' above will call C. Don't worry,
346             any of your arguments will also be sent the method inside that controller, so you don't need to do anything else!
347              
348             Finally, we can create the controller
349              
350             B
351              
352             package MyApp::Controller::Root;
353              
354             use Pulp::Controller;
355              
356             sub hello {
357             my ($self) = @_;
358             return "Hello, world!";
359             }
360              
361             You now have a fully functional Kelp app! Remember, because this module is just a wrapper, you can do pretty much anything L
362             can, like C<$self->>param> for example.
363              
364             =head1 SUGARY SYNTAX
365              
366             By sugar, we mean human readable and easy to use. You no longer need a build method, then to call ->add on an object for your
367             routes. It uses a similar syntax to L. You'll also find one called C.
368              
369             =head2 get
370              
371             This will trigger a standard GET request.
372              
373             get '/mypage' => sub { 'It works' };
374              
375             =head2 post
376              
377             Will trigger on POST requests only
378              
379             post '/someform' => sub { 'Posted like a boss' };
380              
381             =head2 any
382              
383             Will trigger on POST B GET requests
384              
385             any '/omni' => sub { 'Hit me up on any request' };
386              
387             =head2 bridge
388              
389             Bridges are cool, so please check out the Kelp documentation for more information on what they do and how they work.
390              
391             bridge '/users/:id' => sub {
392             unless ($self->user->logged_in) {
393             return;
394             }
395              
396             return 1;
397             };
398              
399             get '/users/:id/view' => 'Controller::Users::view';
400              
401             =head2 has
402              
403             If you only want basic accessors and Pulp detects you don't have any OOP frameworks activated with C, then it will import its
404             own little method which works similar to L's. Currently, it only supports C, C and C.
405              
406             package MyApp;
407            
408             use Pulp;
409             has 'x' => ( is => 'rw', default => sub { "Hello, world" } );
410              
411             package MyApp::Controller::Main;
412            
413             use Pulp::Controller;
414            
415             sub hello { shift->x; } # Hello, world
416              
417             =head2 around
418              
419             Need more power? Want to modify the default C method? No problem. Similar to C, if Pulp detects you have no C method, it will import one.
420             This allows you to tap into build if you really want to for some reason.
421              
422             package MyApp;
423              
424             use Pulp;
425              
426             around 'build' => sub {
427             my $method = shift;
428             my $self = shift;
429             my $routes = $self->routes;
430             $routes->add('/manual' => sub { "Manually added" });
431              
432             $self->$method(@_);
433             };
434              
435             =head1 MODELS
436              
437             You can always use an attribute to create a database connection, or separate them using models in a slightly cleaner way.
438             In your config you supply a hash reference with the models alias (what you will reference it as in code), the full path, and finally any
439             arguments it might have (like the dbi line, username and password).
440              
441             # config.pl
442             models => {
443             'LittleDB' => {
444             'model' => 'TestApp::Model::LittleDB',
445             'args' => ['dbi:SQLite:testapp.db'],
446             },
447             },
448              
449             Then, you create C
450              
451             package TestApp::Model::LittleDB;
452              
453             use Pulp::Model;
454             use DBIx::Lite;
455              
456             sub build {
457             my ($self, @args) = @_;
458             return DBIx::Lite->connect(@args);
459             }
460              
461             As you can see, the C function returns the DB object you want. You can obviously use DBIx::Class or whatever you want here.
462              
463             That's all you need. Now you can pull that model instance out at any time in your controllers with C.
464              
465             package TestApp::Controller::User;
466              
467             use Pulp::Controller;
468              
469             sub users {
470             my ($self) = @_;
471             my @users = $self->model('LittleDB')->table('users')->all;
472             return join ', ', map { $_->name } @users;
473             }
474              
475             =head2 Named ResultSets
476              
477             If you're not using DBIx::Class, you can still have similar styled resultsets. Simply return a standard hash reference instead of a blessed object
478             from the C method, like so
479              
480             package TestApp::Model::LittleDB;
481              
482             use Pulp::Model;
483             use DBIx::Lite;
484              
485             sub build {
486             my ($self, @args) = @_;
487             my $schema = DBIx::Lite->connect(@args);
488             return {
489             'User' => $schema->table('users'),
490             'Product' => $schema->table('products'),
491             };
492             }
493              
494             Then, you can do this stuff in your controllers
495              
496             package TestApp::Controller::Assets;
497              
498             sub users {
499             my ($self) = @_;
500             my @users = $self->model('LittleDB::User')->all;
501             return join "
", map { $_->name . " (" . $_->email . ")" } @users;
502             }
503              
504             sub products {
505             my ($self) = @_;
506             my @products = $self->model('LittleDB::Product')->all;
507             return join "
", map { $_->name . " (" . sprintf("%.2f", $_->value) . ")" } @products;
508             }
509              
510              
511             =head2 Models and DBIx::Class
512              
513             If you enjoy the way Catalyst handles DBIx::Class models, you're going to love this (I hope so, at least). Pulp will automagically
514             create models based on the sources of your schema if it detects it's a DBIx::Class::Schema.
515             Nothing really has to change, Pulp will figure it out on its own.
516              
517             package TestApp::Model::LittleDB;
518              
519             use Pulp::Model;
520             use LittleDB::Schema;
521              
522             sub build {
523             my ($self, @args) = @_;
524             return LittleDB::Schema->connect(@args);
525             }
526              
527             Then just use it as you normally would in Catalyst (except we store it in C<$self>, not C<$c>).
528              
529             package TestApp::Controller::User;
530            
531             use Pulp::Controller;
532            
533             sub users {
534             my ($self) = @_;
535             my @users = $self->model('LittleDB::User')->all;
536             return join ', ', map { $_->name } @users;
537             }
538              
539             Pulp will loop through all your schemas sources and create models based on your alias, and the sources name. So, C.
540              
541             When we start our app, even though we've only added LittleDB, you'll see we have the new ones based on our Schema. Neat!
542              
543             .----------------------------------------------------------.
544             | Model | Alias |
545             +--------------------------------------+-------------------+
546             | TestApp::Model::LittleDB | LittleDB |
547             | LittleDB::Schema::ResultSet::User | LittleDB::User |
548             | LittleDB::Schema::ResultSet::Product | LittleDB::Product |
549             '--------------------------------------+-------------------'
550              
551             =head2 Automated API generation
552              
553             Did you know Pulp can automatically create an API for your DBIx::Class schema? Currently this feature is still in beta, and only works with
554             searching. Simply pass C<-api> as an import option like so.
555              
556             package TestApp::Model::LittleDB;
557            
558             use Pulp::Model -api => 1;
559             ...
560              
561             This will tell Pulp to do all the work for you, and generates a basic JSON API.
562             Some of the commands are below:
563              
564             =head3 list
565              
566             Lists all rows found for a particular resultset
567              
568             # curl http://localhost:5000/api/user/list
569             [
570             {
571             "email" : "admin@company.ltd",
572             "name" : "Admin User",
573             "id" : 1
574             },
575             {
576             "email" : "user@company.ltd",
577             "name" : "Normal User",
578             "id" : 2
579             }
580             ]
581              
582             =head3 find
583              
584             Obtain a single row based on an id.
585              
586             # curl http://localhost:5000/api/user/find/2
587             {
588             "email" : "user@company.ltd",
589             "name" : "Normal User",
590             "id" : 2
591             }
592              
593             =head3 search
594              
595             You can also perform a search, passing query parameters as your search arguments. If no parameters are passed, you'll
596             get all results back.
597              
598             # curl http://localhost:5000/api/user/search?email=admin@company.ltd&id=1
599             [
600             {
601             "email" : "admin@company.ltd",
602             "name" : "Admin User",
603             "id" : 1
604             }
605             ]
606              
607             =head1 VIEWS
608              
609             OK, so to try and not separate too much, I've chosen not to include views. Just use the standard Kelp modules
610             (ie: L). However, there is a convenience method mentioned below.
611              
612             =head2 detach
613              
614             This method will call C