File Coverage

blib/lib/KelpX/AppBuilder.pm
Criterion Covered Total %
statement 33 86 38.3
branch 3 18 16.6
condition 4 6 66.6
subroutine 9 14 64.2
pod 0 3 0.0
total 49 127 38.5


line stmt bran cond sub pod time code
1             package KelpX::AppBuilder;
2              
3 2     2   48973 use 5.010;
  2         8  
  2         86  
4 2     2   12 use warnings;
  2         4  
  2         62  
5 2     2   9 use strict;
  2         8  
  2         72  
6 2     2   1693 use Module::Find 'useall';
  2         2770  
  2         134  
7 2     2   2088 use File::ShareDir 'module_dir';
  2         15514  
  2         435  
8              
9             our $VERSION = '0.003';
10              
11             =head1 NAME
12              
13             KelpX::AppBuilder - Create re-usable apps with Kelp
14              
15             =head1 SYNOPSIS
16              
17             KelpX::AppBuilder makes it trivial to reuse your entire route map and views in an entirely new Kelp application. You create a base app, which can still be run normally, and from there you can start a new project and reuse everything from your base app without duplicating things.
18              
19             =head1 USAGE
20              
21             First off, we'll need to modify the structure so File::ShareDir knows where to find our views/assets globally. It should look something like this
22              
23             BaseApp/lib/auto/BaseApp/views
24             BaseApp/lib/auto/BaseApp/assets
25              
26             You don't have to use the same names, just the structure.
27              
28             Then, in your base application
29              
30             package BaseApp;
31            
32             use Kelp::Base 'Kelp';
33             use KelpX::AppBuilder 'Base';
34              
35             # we don't need a build method, KelpX::AppBuilder will automatically
36             # create it based on what we define in the maps method
37             sub maps {
38             {
39             '/' => BaseApp::Controller::Root->can('index'),
40             '/login' => BaseApp::Controller::Auth->can('login'),
41             '/accounts/manage/:id' => {
42             to => BaseApp::Controller::Accounts->can('manage'),
43             bridge => 1
44             },
45             '/accounts/manage/:id/view', BaseApp::Controller::Accounts->can('view'),
46             }
47             }
48              
49             1;
50              
51             B you can include an 'auto' method into your application. The auto method gets called before every single page, so it's handy to use to ensure a user is logged in. You can do this by adding
52             'auto' => 1
53              
54             to your maps hash ref. It will handle the route line and bridging for you. Don't forget to create the auto method in BaseApp::Controller::Root if you enable this though! An example:
55              
56             package BaseApp::Controller::Root;
57              
58             sub auto {
59             my ($self) = @_;
60             my $url = $self->named->{page};
61             unless ($url eq 'login') {
62             if (my $user = $self->user) {
63             return 1;
64             }
65              
66             return;
67             }
68              
69             return 1;
70             }
71              
72             We'll call our new app 'TestApp' (original, eh?). Copy across your config from BaseApp into your TestApp conf, then tell KelpX::AppBuilder what module it should use as its base dir. Once this is done, it will generate a method called C for you to use.
73              
74             use KelpX::AppBuilder Config => 'BaseApp';
75             middleware_init => {
76             Static => {
77             path => qw{^/assets/|^/apps/},
78             root => base_path(),
79             },
80            
81             ...
82             },
83              
84             # use local views, and the one from BaseApp
85             'Template::Toolkit' => {
86             ENCODING => 'utf8',
87             INCLUDE_PATH => [
88             './views',
89             base_path() . '/views'
90             ],
91             RELATIVE => 1,
92             TAG_STYLE => 'asp',
93             },
94              
95             The final part is loading your BaseApp controllers into your TestApp. That's fairly easy.
96              
97             package TestApp;
98              
99             use Kelp::Base 'Kelp';
100             use KelpX::AppBuilder;
101              
102             sub build {
103             my ($self) = @_;
104             my $r = $self->routes;
105            
106             KelpX::AppBuilder->new('BaseApp')->add_maps($r);
107              
108             # now you can add TestApp's own routing
109             $r->add('/hello', sub { "Hello, world!" });
110             }
111              
112             1;
113              
114             Congratulations. You've just reused the controllers from your BaseApp.
115              
116             =cut
117              
118             sub import {
119 2     2   26 my ($me, @opts) = @_;
120 2         18 my $class = caller;
121 2 100 66     18 if (@opts and $opts[0] eq 'Base') {
122 1         6 my @controllers = useall "${class}::Controller";
123 1         376 for my $c (@controllers) {
124 0         0 eval "use $c";
125 0         0 say "=> Loaded controller $c";
126             }
127              
128             {
129 2     2   21 no strict 'refs';
  2         5  
  2         365  
  1         2  
130 1         7 *{"${class}::build"} = sub {
131 0     0   0 my ($self) = @_;
132 0         0 my $r = $self->routes;
133              
134 0         0 my $maps = $class->maps;
135 0         0 for my $method (keys %$maps) {
136 0         0 $r->add($method, $maps->{$method});
137             }
138 1         5 };
139             }
140             }
141              
142 2 50 66     1940 if (@opts and $opts[0] eq 'Config') {
143 0 0         if (scalar @opts > 1) {
144 0           my $mod = $opts[1];
145 0           eval "use $mod";
146 0 0         if ($@) {
147 0           die "[error] Could not load base module $mod into config: $@\n";
148             }
149              
150             {
151 2     2   14 no strict 'refs';
  2         3  
  2         738  
  0            
152 0     0     *{"${class}::base_path"} = sub { return module_dir($mod) };
  0            
  0            
153             }
154             }
155             else {
156 0           die "[error] Config import option expects a base app name\n";
157             }
158             }
159             }
160              
161             sub new {
162 0     0 0   my ($class, $base) = @_;
163 0           my $self = { name => $base };
164 0           eval "use $base";
165 0 0         if ($@) {
166 0           print STDERR "[error] Failed to open $base: $@\n";
167 0           exit 5;
168             }
169              
170 0           my @controllers = useall "${base}::Controller";
171             eval "use $_"
172 0           for @controllers;
173              
174              
175 0           return bless $self, $class;
176             }
177              
178             sub load_controllers {
179 0     0 0   my ($self, @controllers) = @_;
180 0           for my $c (@controllers) {
181 0           eval "use $self->{name}::Controller::$c";
182 0 0         if ($@) {
183 0           die "[error] Could not load controller $c: $@\n";
184             }
185             }
186              
187 0           return $self;
188             }
189              
190             sub add_maps {
191 0     0 0   my ($self, $r) = @_;
192             {
193 0           my $class = caller;
  0            
194 2     2   13 no strict 'refs';
  2         4  
  2         613  
195              
196 0           my $mod = $self->{name};
197 0 0         unless ($mod->can('maps')) {
198 0           print STDERR "Base mod $mod is missing 'maps' method\n";
199 0           exit 3;
200             }
201              
202 0           my @no_import = qw(new build import);
203 0           foreach my $method (keys %{"${mod}::"}) {
  0            
204 0           *{"${class}::${method}"} = *{"${mod}::${method}"}
  0            
  0            
205 0 0         unless grep { $_ eq $method } @no_import;
206             }
207 0           my $maps = $mod->maps;
208 0           foreach my $path (keys %$maps) {
209 0 0         if ($path eq 'auto') {
210 0           my $root = "${mod}::Controller::Root";
211 0           $r->add('/:page' => { to => $root->can('auto'), bridge => 1 });
212 0           next;
213             }
214              
215 0           $r->add($path, $maps->{$path});
216             }
217              
218             }
219             }
220              
221             =head1 PLEASE NOTE
222              
223             This module is still a work in progress, so I would advise against using KelpX::AppBuilder in a production environment. I'm still looking at ways to make KelpX::AppBuilder more user friendly, but unfortunately reusing an application is not a simple process :-)
224              
225             =head1 AUTHOR
226              
227             Brad Haywood
228              
229             =head1 LICENSE
230              
231             You may distribute this code under the same terms as Perl itself.
232              
233             =cut
234             1;
235             __END__