File Coverage

blib/lib/KelpX/Sweet.pm
Criterion Covered Total %
statement 162 192 84.3
branch 31 66 46.9
condition 1 6 16.6
subroutine 22 25 88.0
pod 0 1 0.0
total 216 290 74.4


line stmt bran cond sub pod time code
1             package KelpX::Sweet;
2              
3 7     7   26480 use warnings;
  7         15  
  7         226  
4 7     7   29 use strict;
  7         9  
  7         177  
5 7     7   3086 use true;
  7         61694  
  7         80  
6 7     7   10042 use Text::ASCIITable;
  7         102219  
  7         354  
7 7     7   3417 use FindBin;
  7         5899  
  7         310  
8 7     7   2958 use Module::Find 'useall';
  7         6805  
  7         391  
9 7     7   39 use base 'Kelp';
  7         10  
  7         3692  
10              
11             our $VERSION = '0.003';
12              
13             sub import {
14 7     7   61 my ($class, %opts) = @_;
15 7         82 strict->import();
16 7         139 warnings->import();
17 7         43 true->import();
18 7         5079 my $caller = caller;
19 7         16 my $routes = [];
20 7         8 my $configs = {};
21 7         20 my $auto = 0;
22             {
23 7     7   512828 no strict 'refs';
  7         11  
  7         7544  
  7         8  
24 7         10 push @{"${caller}::ISA"}, 'Kelp';
  7         79  
25             # check args
26             # auto load routes?
27 7 50       28 if ($opts{"-auto"}) {
28 0         0 $auto = 1;
29 0         0 my $route_tb = Text::ASCIITable->new;
30 0         0 $route_tb->setCols('Routes');
31 0         0 my @mod_routes = useall "${caller}::Route";
32 0         0 for my $mod (@mod_routes) {
33 0         0 $route_tb->addRow($mod);
34 0         0 push @$routes, $mod->get_routes();
35             }
36              
37 0         0 print $route_tb . "\n";
38             }
39              
40 7     6   23 *{"${caller}::new"} = sub { return shift->SUPER::new(@_); };
  7         24  
  6         116  
41 7         21 *{"${caller}::maps"} = sub {
42 6 50   6   30 die "Please don't use -auto and maps at the same time\n"
43             if $auto;
44              
45 6         7 my ($route_names) = @_;
46 6 50       24 unless (ref $route_names eq 'ARRAY') {
47 0         0 die "routes() expects an array references";
48             }
49              
50 6         44 my $route_tb = Text::ASCIITable->new;
51 6         226 $route_tb->setCols('Routes');
52 6         584 for my $mod (@$route_names) {
53 6         17 my $route_path = "${caller}::Route::${mod}";
54 6     6   439 eval "use $route_path;";
  6         2109  
  6         12  
  6         67  
55 6 50       123 if ($@) {
56 0         0 warn "Could not load route ${route_path}: $@";
57 0         0 next;
58             }
59              
60 6         34 $route_tb->addRow($route_path);
61 6         1019 push @$routes, $route_path->get_routes();
62             }
63              
64 6         30 print $route_tb . "\n";
65 7         28 };
66              
67 7         17 *{"${caller}::model"} = sub {
68 1     1   21147 my ($self, $model) = @_;
69 1         18 return $self->{_models}->{$model};
70 7         18 };
71              
72 7     0   16 *{"${caller}::path_to"} = sub { return $FindBin::Bin; };
  7         21  
  0         0  
73              
74 7         19 *{"${caller}::cfg"} = sub {
75 0     0   0 my ($key, $hash) = @_;
76 0         0 $configs->{$key} = $hash;
77 7         15 };
78              
79 7         33 *{"${caller}::build"} = sub {
80 6     6   1124 my ($self) = @_;
81 6         27 my $config = $self->config_hash;
82             # config
83 6 50       46 if (scalar keys %$configs > 0) {
84 0         0 for my $key (keys %$configs) {
85 0         0 $config->{"+${key}"} = $configs->{$key};
86             }
87             }
88            
89             # models
90 6 50       24 if ($config->{models}) {
91 6         18 $self->{_models} = {};
92 6         54 my $model_tb = Text::ASCIITable->new;
93 6         276 $model_tb->setCols('Model', 'Alias');
94 6 50       588 unless (ref $config->{models} eq 'HASH') {
95 0         0 die "config: models expects a hash reference\n";
96             }
97              
98 6         9 for my $model (keys %{$config->{models}}) {
  6         26  
99 6         10 my $name = $model;
100 6         14 my $opts = $config->{models}->{$model};
101 6         13 my $mod = $opts->{model};
102 6     6   457 eval "use $mod;";
  6         2066  
  6         823801  
  6         128  
103 6 50       30 if ($@) {
104 0         0 die "Could not load model $mod: $@";
105             }
106              
107 6         11 my @args = @{$opts->{args}};
  6         33  
108 6 50       24 if (my $ret = $mod->build(@args)) {
109 6 50       337475 if (ref $ret) {
110 6         52 $model_tb->addRow($mod, $name);
111             # returned a standard hash reference
112 6 50       940 if (ref $ret eq 'HASH') {
113 0         0 foreach my $key (keys %$ret) {
114 0 0       0 if (ref $ret->{$key}) {
115 0         0 $self->{_models}->{"${name}::${key}"} = $ret->{$key};
116 0         0 $model_tb->addRow(ref $ret->{$key}, "${name}::${key}");
117             }
118             }
119             }
120             else {
121 6         25 $self->{_models}->{$name} = $ret;
122            
123             # is this dbix::class?
124 6         55 require mro;
125 6         16 my $dbref = ref $ret;
126 6 50       10 if (grep { $_ eq 'DBIx::Class::Schema' } @{mro::get_linear_isa($dbref)}) {
  42         62  
  6         34  
127 6 50       398 if ($dbref->can('sources')) {
128 6         29 my @sources = $dbref->sources;
129 6         319 for my $source (@sources) {
130 6         58 $self->{_models}->{"${name}::${source}"} = $ret->resultset($source);
131 6         5432 $model_tb->addRow("${dbref}::ResultSet::${source}", "${name}::${source}");
132             }
133             }
134             }
135             }
136             }
137             else {
138 0         0 die "Did not return a valid object from models build(): $name\n";
139             }
140             }
141             else {
142 0         0 die "build() failed: $mod";
143             }
144             }
145              
146 6 50       643 if (scalar keys %{$self->{_models}} > 0) {
  6         43  
147 6         30 print $model_tb . "\n";
148             }
149             }
150             # routes
151 6         5379 my $r = $self->routes;
152 6         39 for my $route (@$routes) {
153 6         27 for my $url (keys %$route) {
154 36 100       17309 if ($route->{$url}->{bridge}) {
    50          
155 6         50 $r->add([ uc($route->{$url}->{type}) => $url ], { to => $route->{$url}->{coderef}, bridge => 1 });
156             }
157             elsif ($route->{$url}->{type} eq 'any') {
158 0         0 $r->add($url, $route->{$url}->{coderef});
159             }
160             else {
161 30         160 $r->add([ uc($route->{$url}->{type}) => $url ], $route->{$url}->{coderef});
162             }
163             }
164             }
165 7         42 };
166              
167 7         24 *{"${caller}::detach"} = sub {
168 1     1   23984 my ($self) = @_;
169              
170 1         12 my @caller = caller(1);
171 1         2 my $fullpath = $caller[3];
172 1         2 my $name;
173 1 50       9 if ($fullpath =~ /.+::(.+)$/) {
174 1         2 $name = $1;
175             }
176              
177 1 50       3 if ($name) {
178 1 50       4 print "[debug] Rendering template: $name\n" if $ENV{KELPX_SWEET_DEBUG};
179 1         3 $self->template($name, $self->stash);
180             }
181 7         18 };
182              
183             # if 'has' is not available (ie: no Moose, Moo, Mouse, etc), then import our own small version
184 7 50       80 unless ($caller->can('has')) {
185 7         11 *{"${caller}::has"} = \&_has;
  7         16  
186             }
187              
188             # if 'around' is not available, import a small version of our own
189             {
190 7     7   38 no warnings 'redefine';
  7         10  
  7         1590  
  7         10  
191 7 50       49 unless ($caller->can('around')) {
192 7         733 *{"${caller}::around"} = sub {
193 6     6   3333 my ($method, $code) = @_;
194              
195 6         15 my $fullpkg = "${caller}::${method}";
196 6         16 my $old_code = \&$fullpkg;
197 6         101 *{"${fullpkg}"} = sub {
198 6     6   409892 $code->($old_code, @_);
199 6         18 };
200 7         20 };
201             }
202             }
203             }
204             }
205              
206             sub _has {
207 6     6   48 my ($acc, %attrs) = @_;
208 6         12 my $class = caller;
209 6         6 my $ro = 0;
210 6         6 my $rq = 0;
211 6         6 my $df;
212 6 50       18 if (%attrs) {
213 6 50       18 if ($attrs{is} eq 'ro') { $ro = 1; }
  6         9  
214 6 50       14 if ($attrs{required}) { $rq = 1; }
  6         9  
215 6 50       15 if ($attrs{default}) { $df = $attrs{default}; }
  6         8  
216              
217 6 50       19 if ($df) {
218 6 50       19 die "has: default expects a code reference\n"
219             unless ref $df eq 'CODE';
220             }
221             }
222            
223             {
224 7     7   90 no strict 'refs';
  7         9  
  7         1778  
  6         6  
225 6         37 *{"${class}::${acc}"} = sub {
226             #if ($attrs{default}) { $_[0]->{$name} = $attrs{default}; }
227 1 50 33 1   26093 if ($rq and not $df) {
228 0 0 0     0 if (not $_[0]->{$acc} and not $_[1]) {
229 0         0 die "You attempted to use a field that can't be left blank: ${acc}";
230             }
231             }
232              
233 1 50       4 if ($df) { $_[0]->{$acc} = $df->(); }
  1         5  
234            
235 1 50       6 if (@_ == 2) {
236 0 0       0 die "Can't modify a readonly accessor: ${acc}"
237             if $ro;
238 0         0 $_[0]->{$acc} = $_[1];
239             }
240 1         4 return $_[0]->{$acc};
241 6         18 };
242             }
243             }
244              
245             sub new {
246 0     0 0 0 bless { @_[ 1 .. $#_ ] }, $_[0];
247             }
248              
249             =head1 NAME
250              
251             KelpX::Sweet - Kelp with extra sweeteners
252              
253             =head1 DESCRIPTION
254              
255             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? KelpX::Sweet attempts to do just that.
256              
257             =head1 SIMPLE TUTORIAL
258              
259             For the most part, your original C will remain the same as Kelps.
260              
261             B
262            
263             package MyApp;
264             use KelpX::Sweet;
265              
266             maps ['Main'];
267              
268             Yep, that's the complete code for your base. You pass C an array reference of the routes you want to include.
269             It will look for them in C. So the above example will load C.
270             Next, let's create that file
271              
272             B
273              
274             package MyApp::Route::Main;
275              
276             use KelpX::Sweet::Route;
277              
278             get '/' => 'Controller::Root::hello';
279             get '/nocontroller' => sub { 'Hello, world from no controller!' };
280              
281             Simply use C, then create your route definitions here. You're welcome to put your logic inside code refs,
282             but that makes the whole idea of this module pointless ;)
283             It will load C then whatever you pass to it. So the '/' above will call C. Don't worry,
284             any of your arguments will also be sent the method inside that controller, so you don't need to do anything else!
285              
286             Finally, we can create the controller
287              
288             B
289              
290             package MyApp::Controller::Root;
291              
292             use KelpX::Sweet::Controller;
293              
294             sub hello {
295             my ($self) = @_;
296             return "Hello, world!";
297             }
298              
299             You now have a fully functional Kelp app! Remember, because this module is just a wrapper, you can do pretty much anything L
300             can, like C<$self->>param> for example.
301              
302             =head1 SUGARY SYNTAX
303              
304             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
305             routes. It uses a similar syntax to L. You'll also find one called C.
306              
307             =head2 get
308              
309             This will trigger a standard GET request.
310              
311             get '/mypage' => sub { 'It works' };
312              
313             =head2 post
314              
315             Will trigger on POST requests only
316              
317             post '/someform' => sub { 'Posted like a boss' };
318              
319             =head2 any
320              
321             Will trigger on POST B GET requests
322              
323             any '/omni' => sub { 'Hit me up on any request' };
324              
325             =head2 bridge
326              
327             Bridges are cool, so please check out the Kelp documentation for more information on what they do and how they work.
328              
329             bridge '/users/:id' => sub {
330             unless ($self->user->logged_in) {
331             return;
332             }
333              
334             return 1;
335             };
336              
337             get '/users/:id/view' => 'Controller::Users::view';
338              
339             =head2 has
340              
341             If you only want basic accessors and KelpX::Sweet detects you don't have any OOP frameworks activated with C, then it will import its
342             own little method which works similar to L's. Currently, it only supports C, C and C.
343              
344             package MyApp;
345            
346             use KelpX::Sweet;
347             has 'x' => ( is => 'rw', default => sub { "Hello, world" } );
348              
349             package MyApp::Controller::Main;
350            
351             use KelpX::Sweet::Controller;
352            
353             sub hello { shift->x; } # Hello, world
354              
355             =head2 around
356              
357             Need more power? Want to modify the default C method? No problem. Similar to C, if KelpX::Sweet detects you have no C method, it will import one.
358             This allows you to tap into build if you really want to for some reason.
359              
360             package MyApp;
361              
362             use KelpX::Sweet;
363              
364             around 'build' => sub {
365             my $method = shift;
366             my $self = shift;
367             my $routes = $self->routes;
368             $routes->add('/manual' => sub { "Manually added" });
369              
370             $self->$method(@_);
371             };
372              
373             =head1 MODELS
374              
375             You can always use an attribute to create a database connection, or separate them using models in a slightly cleaner way.
376             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
377             arguments it might have (like the dbi line, username and password).
378              
379             # config.pl
380             models => {
381             'LittleDB' => {
382             'model' => 'TestApp::Model::LittleDB',
383             'args' => ['dbi:SQLite:testapp.db'],
384             },
385             },
386              
387             Then, you create C
388              
389             package TestApp::Model::LittleDB;
390              
391             use KelpX::Sweet::Model;
392             use DBIx::Lite;
393              
394             sub build {
395             my ($self, @args) = @_;
396             return DBIx::Lite->connect(@args);
397             }
398              
399             As you can see, the C function returns the DB object you want. You can obviously use DBIx::Class or whatever you want here.
400              
401             That's all you need. Now you can pull that model instance out at any time in your controllers with C.
402              
403             package TestApp::Controller::User;
404              
405             use KelpX::Sweet::Controller;
406              
407             sub users {
408             my ($self) = @_;
409             my @users = $self->model('LittleDB')->table('users')->all;
410             return join ', ', map { $_->name } @users;
411             }
412              
413             =head2 Named ResultSets
414              
415             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
416             from the C method, like so
417              
418             package TestApp::Model::LittleDB;
419            
420             use KelpX::Sweet::Model;
421             use DBIx::Lite;
422              
423             sub build {
424             my ($self, @args) = @_;
425             my $schema = DBIx::Lite->connect(@args);
426             return {
427             'User' => $schema->table('users'),
428             'Product' => $schema->table('products'),
429             };
430             }
431              
432             Then, you can do this stuff in your controllers
433              
434             package TestApp::Controller::Assets;
435              
436             sub users {
437             my ($self) = @_;
438             my @users = $self->model('LittleDB::User')->all;
439             return join "
", map { $_->name . " (" . $_->email . ")" } @users;
440             }
441              
442             sub products {
443             my ($self) = @_;
444             my @products = $self->model('LittleDB::Product')->all;
445             return join "
", map { $_->name . " (" . sprintf("%.2f", $_->value) . ")" } @products;
446             }
447              
448             =head2 Models and DBIx::Class
449              
450             If you enjoy the way Catalyst handles DBIx::Class models, you're going to love this (I hope so, at least). KelpX::Sweet will automagically
451             create models based on the sources of your schema if it detects it's a DBIx::Class::Schema.
452             Nothing really has to change, KelpX::Sweet will figure it out on its own.
453              
454             package TestApp::Model::LittleDB;
455              
456             use KelpX::Sweet::Model;
457             use LittleDB::Schema;
458              
459             sub build {
460             my ($self, @args) = @_;
461             return LittleDB::Schema->connect(@args);
462             }
463              
464             Then just use it as you normally would in Catalyst (except we store it in C<$self>, not C<$c>).
465              
466             package TestApp::Controller::User;
467            
468             use KelpX::Sweet::Controller;
469            
470             sub users {
471             my ($self) = @_;
472             my @users = $self->model('LittleDB::User')->all;
473             return join ', ', map { $_->name } @users;
474             }
475              
476             KelpX::Sweet will loop through all your schemas sources and create models based on your alias, and the sources name. So, C.
477              
478             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!
479              
480             .----------------------------------------------------------.
481             | Model | Alias |
482             +--------------------------------------+-------------------+
483             | TestApp::Model::LittleDB | LittleDB |
484             | LittleDB::Schema::ResultSet::User | LittleDB::User |
485             | LittleDB::Schema::ResultSet::Product | LittleDB::Product |
486             '--------------------------------------+-------------------'
487              
488             =head1 VIEWS
489              
490             OK, so to try and not separate too much, I've chosen not to include views. Just use the standard Kelp modules
491             (ie: L). However, there is a convenience method mentioned below.
492              
493             =head2 detach
494              
495             This method will call C