File Coverage

blib/lib/KelpX/Sweet.pm
Criterion Covered Total %
statement 69 125 55.2
branch 6 30 20.0
condition n/a
subroutine 12 17 70.5
pod 0 1 0.0
total 87 173 50.2


line stmt bran cond sub pod time code
1             package KelpX::Sweet;
2              
3 2     2   16884 use warnings;
  2         3  
  2         60  
4 2     2   6 use strict;
  2         3  
  2         87  
5 2     2   808 use true;
  2         16839  
  2         11  
6 2     2   2890 use Text::ASCIITable;
  2         29483  
  2         122  
7 2     2   1204 use FindBin;
  2         1673  
  2         79  
8 2     2   11 use base 'Kelp';
  2         2  
  2         1076  
9              
10             our $VERSION = '0.001';
11              
12             sub import {
13 2     2   20 my ($class, %args) = @_;
14 2         26 strict->import();
15 2         23 warnings->import();
16 2         12 true->import();
17 2         1352 my $caller = caller;
18 2         3 my $routes = [];
19 2         3 my $configs = {};
20             {
21 2     2   145706 no strict 'refs';
  2         5  
  2         1903  
  2         2  
22 2         2 push @{"${caller}::ISA"}, 'Kelp';
  2         19  
23 2     1   6 *{"${caller}::new"} = sub { return shift->SUPER::new(@_); };
  2         6  
  1         24  
24 2         6 *{"${caller}::maps"} = sub {
25 1     1   4 my ($route_names) = @_;
26 1 50       5 unless (ref $route_names eq 'ARRAY') {
27 0         0 die "routes() expects an array references";
28             }
29              
30 1         6 my $route_tb = Text::ASCIITable->new;
31 1         35 $route_tb->setCols('Routes');
32 1         89 for my $mod (@$route_names) {
33 1         2 my $route_path = "${caller}::Route::${mod}";
34 1     1   58 eval "use $route_path;";
  1         321  
  1         1  
  1         20  
35 1 50       15 if ($@) {
36 0         0 warn "Could not load route ${route_path}: $@";
37 0         0 next;
38             }
39              
40 1         6 $route_tb->addRow($route_path);
41 1         134 push @$routes, $route_path->get_routes();
42             }
43              
44 1         5 print $route_tb . "\n";
45 2         7 };
46              
47 2         4 *{"${caller}::model"} = sub {
48 0     0   0 my ($self, $model) = @_;
49 0         0 return $self->{_models}->{$model};
50 2         4 };
51              
52 2     0   18 *{"${caller}::path_to"} = sub { return $FindBin::Bin; };
  2         9  
  0         0  
53              
54 2         8 *{"${caller}::cfg"} = sub {
55 0     0   0 my ($key, $hash) = @_;
56 0         0 $configs->{$key} = $hash;
57 2         7 };
58              
59 2         8 *{"${caller}::build"} = sub {
60 1     1   17183 my ($self) = @_;
61 1         4 my $config = $self->config_hash;
62             # config
63 1 50       15 if (scalar keys %$configs > 0) {
64 0         0 for my $key (keys %$configs) {
65 0         0 $config->{"+${key}"} = $configs->{$key};
66             }
67             }
68            
69             # models
70 1 50       4 if ($config->{models}) {
71 0         0 $self->{_models} = {};
72 0         0 my $model_tb = Text::ASCIITable->new;
73 0         0 $model_tb->setCols('Model', 'Alias');
74 0 0       0 unless (ref $config->{models} eq 'HASH') {
75 0         0 die "config: models expects a hash reference\n";
76             }
77              
78 0         0 for my $model (keys %{$config->{models}}) {
  0         0  
79 0         0 my $name = $model;
80 0         0 my $opts = $config->{models}->{$model};
81 0         0 my $mod = $opts->{model};
82 0         0 eval "use $mod;";
83 0 0       0 if ($@) {
84 0         0 die "Could not load model $mod: $@";
85             }
86              
87 0         0 my @args = @{$opts->{args}};
  0         0  
88 0 0       0 if (my $ret = $mod->build(@args)) {
89 0 0       0 if (ref $ret) {
90 0         0 $model_tb->addRow($mod, $name);
91 0         0 $self->{_models}->{$name} = $ret;
92            
93             # is this dbix::class?
94 0         0 require mro;
95 0         0 my $dbref = ref $ret;
96 0 0       0 if (grep { $_ eq 'DBIx::Class::Schema' } @{mro::get_linear_isa($dbref)}) {
  0         0  
  0         0  
97 0 0       0 if ($dbref->can('sources')) {
98 0         0 my @sources = $dbref->sources;
99 0         0 for my $source (@sources) {
100 0         0 $self->{_models}->{"${name}::${source}"} = $ret->resultset($source);
101 0         0 $model_tb->addRow("${dbref}::ResultSet::${source}", "${name}::${source}");
102             }
103             }
104             }
105             }
106             else {
107 0         0 die "Did not return a valid object from models build(): $name\n";
108             }
109             }
110             else {
111 0         0 die "build() failed: $mod";
112             }
113             }
114              
115 0 0       0 if (scalar keys %{$self->{_models}} > 0) {
  0         0  
116 0         0 print $model_tb . "\n";
117             }
118             }
119             # routes
120 1         4 my $r = $self->routes;
121 1         4 for my $route (@$routes) {
122 1         4 for my $url (keys %$route) {
123 1 50       7 if ($route->{$url}->{bridge}) {
    50          
124 0         0 $r->add([ uc($route->{$url}->{type}) => $url ], { to => $route->{$url}->{coderef}, bridge => 1 });
125             }
126             elsif ($route->{$url}->{type} eq 'any') {
127 0         0 $r->add($url, $route->{$url}->{coderef});
128             }
129             else {
130 1         9 $r->add([ uc($route->{$url}->{type}) => $url ], $route->{$url}->{coderef});
131             }
132             }
133             }
134 2         16 };
135              
136 2         60 *{"${caller}::detach"} = sub {
137 0     0   0 my ($self) = @_;
138              
139 0         0 my @caller = caller(1);
140 0         0 my $fullpath = $caller[3];
141 0         0 my $name;
142 0 0       0 if ($fullpath =~ /.+::(.+)$/) {
143 0         0 $name = $1;
144             }
145              
146 0 0       0 if ($name) {
147 0         0 print "[debug] Rendering template: $name\n";
148 0         0 $self->template($name, $self->stash);
149             }
150 2         7 };
151             }
152             }
153              
154             sub new {
155 0     0 0 0 bless { @_[ 1 .. $#_ ] }, $_[0];
156             }
157              
158             =head1 NAME
159              
160             KelpX::Sweet - Kelp with extra sweeteners
161              
162             =head1 DESCRIPTION
163              
164             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.
165              
166             =head1 SIMPLE TUTORIAL
167              
168             For the most part, your original C will remain the same as Kelps.
169              
170             B
171            
172             package MyApp;
173             use KelpX::Sweet;
174              
175             maps ['Main'];
176              
177             Yep, that's the complete code for your base. You pass C an array reference of the routes you want to include.
178             It will look for them in C. So the above example will load C.
179             Next, let's create that file
180              
181             B
182              
183             package MyApp::Route::Main;
184              
185             use KelpX::Sweet::Route;
186              
187             get '/' => 'Controller::Root::hello';
188             get '/nocontroller' => sub { 'Hello, world from no controller!' };
189              
190             Simply use C, then create your route definitions here. You're welcome to put your logic inside code refs,
191             but that makes the whole idea of this module pointless ;)
192             It will load C then whatever you pass to it. So the '/' above will call C. Don't worry,
193             any of your arguments will also be sent the method inside that controller, so you don't need to do anything else!
194              
195             Finally, we can create the controller
196              
197             B
198              
199             package MyApp::Controller::Root;
200              
201             use KelpX::Sweet::Controller;
202              
203             sub hello {
204             my ($self) = @_;
205             return "Hello, world!";
206             }
207              
208             You now have a fully functional Kelp app! Remember, because this module is just a wrapper, you can do pretty much anything L
209             can, like C<$self->>param> for example.
210              
211             =head1 SUGARY SYNTAX
212              
213             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
214             routes. It uses a similar syntax to L. You'll also find one called C.
215              
216             =head2 get
217              
218             This will trigger a standard GET request.
219              
220             get '/mypage' => sub { 'It works' };
221              
222             =head2 post
223              
224             Will trigger on POST requests only
225              
226             post '/someform' => sub { 'Posted like a boss' };
227              
228             =head2 any
229              
230             Will trigger on POST B GET requests
231              
232             any '/omni' => sub { 'Hit me up on any request' };
233              
234             =head2 bridge
235              
236             Bridges are cool, so please check out the Kelp documentation for more information on what they do and how they work.
237              
238             bridge '/users/:id' => sub {
239             unless ($self->user->logged_in) {
240             return;
241             }
242              
243             return 1;
244             };
245              
246             get '/users/:id/view' => 'Controller::Users::view';
247              
248             =head1 MODELS
249              
250             You can always use an attribute to create a database connection, or separate them using models in a slightly cleaner way.
251             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
252             arguments it might have (like the dbi line, username and password).
253              
254             # config.pl
255             models => {
256             'LittleDB' => {
257             'model' => 'TestApp::Model::LittleDB',
258             'args' => ['dbi:SQLite:testapp.db'],
259             },
260             },
261              
262             Then, you create C
263              
264             package TestApp::Model::LittleDB;
265              
266             use KelpX::Sweet::Model;
267             use DBIx::Lite;
268              
269             sub build {
270             my ($self, @args) = @_;
271             return DBIx::Lite->connect(@args);
272             }
273              
274             As you can see, the C function returns the DB object you want. You can obviously use DBIx::Class or whatever you want here.
275              
276             That's all you need. Now you can pull that model instance out at any time in your controllers with C.
277              
278             package TestApp::Controller::User;
279              
280             use KelpX::Sweet::Controller;
281              
282             sub users {
283             my ($self) = @_;
284             my @users = $self->model('LittleDB')->table('users')->all;
285             return join ', ', map { $_->name } @users;
286             }
287              
288             =head2 Models and DBIx::Class
289              
290             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
291             create models based on the sources of your schema if it detects it's a DBIx::Class::Schema.
292             Nothing really has to change, KelpX::Sweet will figure it out on its own.
293              
294             package TestApp::Model::LittleDB;
295              
296             use KelpX::Sweet::Model;
297             use LittleDB::Schema;
298              
299             sub build {
300             my ($self, @args) = @_;
301             return LittleDB::Schema->connect(@args);
302             }
303              
304             Then just use it as you normally would in Catalyst (except we store it in C<$self>, not C<$c>).
305              
306             package TestApp::Controller::User;
307            
308             use KelpX::Sweet::Controller;
309            
310             sub users {
311             my ($self) = @_;
312             my @users = $self->model('LittleDB::User')->all;
313             return join ', ', map { $_->name } @users;
314             }
315              
316             KelpX::Sweet will loop through all your schemas sources and create models based on your alias, and the sources name. So, C.
317              
318             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!
319              
320             .----------------------------------------------------------.
321             | Model | Alias |
322             +--------------------------------------+-------------------+
323             | TestApp::Model::LittleDB | LittleDB |
324             | LittleDB::Schema::ResultSet::User | LittleDB::User |
325             | LittleDB::Schema::ResultSet::Product | LittleDB::Product |
326             '--------------------------------------+-------------------'
327              
328             =head1 VIEWS
329              
330             OK, so to try and not separate too much, I've chosen not to include views. Just use the standard Kelp modules
331             (ie: L). However, there is a convenience method mentioned below.
332              
333             =head2 detach
334              
335             This method will call C