File Coverage

blib/lib/Minima/App.pm
Criterion Covered Total %
statement 31 31 100.0
branch 4 4 100.0
condition n/a
subroutine 10 10 100.0
pod n/a
total 45 45 100.0


line stmt bran cond sub pod time code
1 10     10   1031195 use v5.40;
  10         48  
2 10     10   4025 use experimental 'class';
  10         28538  
  10         54  
3              
4             class Minima::App;
5              
6 10     10   2044 use Carp;
  10         14  
  10         606  
7 10     10   4760 use Minima::Router;
  10         23  
  10         339  
8 10     10   60 use Path::Tiny ();
  10         18  
  10         200  
9 10     10   4304 use Plack::Response;
  10         125730  
  10         419  
10 10     10   5168 use Plack::Util;
  10         114802  
  10         363  
11 10     10   4822 use FindBin;
  10         13172  
  10         586  
12              
13 10     10   76 use constant DEFAULT_VERSION => 'prototype';
  10         14  
  10         20261  
14              
15             field $env :param(environment) :reader = undef;
16             field $config :param(configuration) :reader = {};
17              
18             field $router = Minima::Router->new;
19              
20             ADJUST {
21             $self->_read_config;
22             }
23              
24             method set_env ($e) { $env = $e }
25             method set_config ($c) { $config = $c; $self->_read_config }
26              
27             method development
28             {
29             return 1 if not defined $ENV{PLACK_ENV};
30              
31             $ENV{PLACK_ENV} eq 'development'
32             }
33              
34             method path ($p)
35             {
36             Path::Tiny::path($p)->absolute($config->{base_dir})->stringify;
37             }
38              
39             method run
40             {
41             croak "Can't run without an environment.\n" unless defined $env;
42              
43             my $match = $router->match($env);
44             my $response;
45             my $controller;
46             my $action;
47              
48             return $self->_not_found unless $match;
49             return $self->_redirect($match) if $match->{redirect};
50              
51             try {
52             ($controller, $action) = $self->_setup_controller($match);
53              
54             # before_action
55             if ($controller->can('before_action')) {
56             my $res = $controller->before_action($action);
57             return $res if $res; # halt
58             }
59             # actual action
60             $response = $controller->$action;
61             # after action
62             if ($controller->can('after_action')) {
63             $controller->after_action($response);
64             }
65             } catch ($e) {
66             my $err = $router->error_route;
67             # Something failed. If we're in production
68             # and there is a server_error route, try it.
69             if (!$self->development && $err) {
70             ($controller, $action) = $self->_setup_controller($err);
71             $response = $controller->$action($e);
72             } else {
73             # Nothing can be done, re-throw
74             die $e;
75             }
76             }
77              
78             # Delete body on HEAD requests
79             my $auto_head = $config->{automatic_head} // 1;
80             if ( $auto_head
81             && length $env->{REQUEST_METHOD}
82             && $env->{REQUEST_METHOD} eq 'HEAD'
83             ) {
84             return Plack::Util::response_cb($response, sub {
85 2     2   42 my $res = shift;
86 2 100       37 if ($res->[2]) {
87 1         4 $res->[2] = [];
88             } else {
89 1 100       4 return sub { defined $_[0] ? '' : undef };
  2         38  
90             }
91             });
92             }
93              
94             return $response;
95             }
96              
97             method _setup_controller ($match)
98             {
99             my $class = $match->{controller};
100             my $method = $match->{action};
101              
102             die "Route dispatch failed: "
103             . "no controller defined for `$env->{PATH_INFO}`.\n"
104             unless defined $class;
105              
106             die "Route dispatch failed: no action configured for `$class`.\n"
107             unless defined $method && length $method;
108              
109             $self->_load_class($class);
110              
111             my $controller = $class->new(
112             app => $self,
113             route => $match,
114             );
115              
116             die "Route dispatch failed: `$class` does not implement action `$method`.\n"
117             unless $controller->can($method);
118              
119             ( $controller, $method );
120             }
121              
122             method _not_found
123             {
124             [
125             404,
126             [ 'Content-Type' => 'text/plain' ],
127             [ "not found\n" ]
128             ]
129             }
130              
131             method _load_class ($class)
132             {
133             try {
134             my $file = $class;
135             $file =~ s|::|/|g;
136             require "$file.pm";
137             } catch ($e) {
138             croak "Could not load `$class`: $e\n";
139             }
140             }
141              
142             method _load_routes
143             {
144             $router->clear_routes;
145              
146             my $file = $config->{routes};
147             unless (defined $file) {
148             # No file passed. Attempt the default route.
149             $file = $self->path('etc/routes.map');
150             # If it does not exist, setup a basic route
151             # for the default controller only.
152             unless (-e $file) {
153             $router->_connect(
154             '/',
155             {
156             controller => 'Minima::Controller',
157             action => 'hello',
158             },
159             );
160             return;
161             }
162             }
163              
164             # Controller prefix
165             my $prefix = $config->{controller_prefix};
166             $router->set_prefix($prefix) if defined $prefix;
167              
168             # Read routes
169             $file = $self->path($file);
170             $router->read_file($file);
171             }
172              
173             method _redirect ($m)
174             {
175             my $res = Plack::Response->new;
176              
177             $res->redirect($m->{redirect}, $m->{redirect_status});
178             $res->finalize;
179             }
180              
181             method _read_config
182             {
183             # Ensure base_dir is set and absolute
184             my $base = $config->{base_dir} // '.';
185             $config->{base_dir} = Path::Tiny::path($base)->absolute;
186              
187             $self->_load_routes;
188             $self->_set_version;
189             }
190              
191             method _set_version
192             {
193             return if defined $config->{VERSION};
194              
195             if (defined $config->{version_from}) {
196             my $class = $config->{version_from};
197             try {
198             $self->_load_class($class);
199             } catch ($e) {
200             croak "Failed to load version from class.\n$e\n";
201             }
202             $config->{VERSION} = $class->VERSION // DEFAULT_VERSION;
203             } else {
204             $config->{VERSION} = DEFAULT_VERSION;
205             }
206             }
207              
208             __END__