| 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 |
||||||
| 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 |
||||||
| 331 | It will look for them in 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 |
||||||
| 344 | but that makes the whole idea of this module pointless ;) | ||||||
| 345 | It will load C |
||||||
| 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 |
||||||
| 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 |
||||||
| 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 |
||||||
| 404 | own little method which works similar to L |
||||||
| 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 |
||||||
| 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 |
||||||
| 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 |
||||||
| 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 |
||||||
| 611 | |||||||
| 612 | =head2 detach | ||||||
| 613 | |||||||
| 614 | This method will call C for you with the added benefit of automatically filling out the filename and including whatever | ||||||
| 615 | is in the stash for you. | ||||||
| 616 | |||||||
| 617 | package MyApp::Controller::Awesome; | ||||||
| 618 | |||||||
| 619 | use Pulp::Controller; | ||||||
| 620 | |||||||
| 621 | sub hello { | ||||||
| 622 | my ($self) = @_; | ||||||
| 623 | $self->stash->{name} = 'World'; | ||||||
| 624 | $self->detach; | ||||||
| 625 | } | ||||||
| 626 | |||||||
| 627 | Then, you just create C |
||||||
| 628 | |||||||
| 629 | Hello, [% name %] |
||||||
| 630 | |||||||
| 631 | While not really required, it does save a bit of typing and can come in quite useful. | ||||||
| 632 | |||||||
| 633 | =head1 IMPORT OPTIONS | ||||||
| 634 | |||||||
| 635 | =head2 -auto | ||||||
| 636 | |||||||
| 637 | Importing -auto will automatically include any route modules within your C |
||||||
| 638 | For example, we have two controllers, C |
||||||
| 639 | |||||||
| 640 | package MyApp::Route::Main; | ||||||
| 641 | |||||||
| 642 | use Pulp::Route; | ||||||
| 643 | |||||||
| 644 | get '/' => sub { "Hi" }; | ||||||
| 645 | |||||||
| 646 | package MyApp::Route::New; | ||||||
| 647 | |||||||
| 648 | use Pulp::Route; | ||||||
| 649 | |||||||
| 650 | get '/new/url' => sub { "New one" }; | ||||||
| 651 | |||||||
| 652 | Then to kick off our app, all we need is | ||||||
| 653 | |||||||
| 654 | package MyApp; | ||||||
| 655 | use Pulp -auto => 1; | ||||||
| 656 | |||||||
| 657 | That's it. Pulp will complain if you attempt to use C |
||||||
| 658 | |||||||
| 659 | =head1 REALLY COOL THINGS TO NOTE | ||||||
| 660 | |||||||
| 661 | =head2 Default imports | ||||||
| 662 | |||||||
| 663 | You should be aware that Pulp will import warnings, strict and true for you. Because of this, there is no requirement to | ||||||
| 664 | add a true value to the end of your file. I chose this because it just makes things look a little cleaner. | ||||||
| 665 | |||||||
| 666 | =head2 Pulp starter | ||||||
| 667 | |||||||
| 668 | On installation of Pulp, you'll receive a file called C |
||||||
| 669 | and it will create a working test app with minimal boilerplate so you can get started straight away. Just run it as: | ||||||
| 670 | |||||||
| 671 | $ pulp MyApp | ||||||
| 672 | $ pulp Something::With::A::Larger::Namespace | ||||||
| 673 | |||||||
| 674 | =head1 SEE ALSO | ||||||
| 675 | |||||||
| 676 | L |
||||||
| 677 | |||||||
| 678 | |||||||
| 679 | =head1 AUTHOR | ||||||
| 680 | |||||||
| 681 | Brad Haywood |
||||||
| 682 | |||||||
| 683 | =head1 LICENSE | ||||||
| 684 | |||||||
| 685 | You may distribute this code under the same terms as Perl itself. | ||||||
| 686 | |||||||
| 687 | =cut | ||||||
| 688 | |||||||
| 689 | 1; | ||||||
| 690 | __END__ |