File Coverage

blib/lib/Thunderhorse/App.pm
Criterion Covered Total %
statement 213 215 99.0
branch 17 24 70.8
condition 3 3 100.0
subroutine 40 40 100.0
pod 14 16 87.5
total 287 298 96.3


line stmt bran cond sub pod time code
1             package Thunderhorse::App;
2             $Thunderhorse::App::VERSION = '0.102';
3 21     21   14150118 use v5.40;
  21         109  
4 21     21   159 use Mooish::Base -standard;
  21         66  
  21         303  
5              
6 21     21   378518 use Thunderhorse qw(pagi_loop);
  21         106  
  21         2819  
7 21     21   12923 use Thunderhorse::Config;
  21         1783159  
  21         1468  
8 21     21   11883 use Gears qw(load_component get_component_name);
  21         67626  
  21         4375  
9 21     21   14232 use Thunderhorse::Context;
  21         53268  
  21         1713  
10 21     21   15684 use Thunderhorse::Router;
  21         47389  
  21         1448  
11 21     21   12878 use Thunderhorse::Controller;
  21         48748  
  21         1369  
12 21     21   14874 use Thunderhorse::AppController;
  21         3668  
  21         1787  
13 21     21   221 use Path::Tiny;
  21         54  
  21         2231  
14              
15 21     21   14347 use HTTP::Status qw(status_message);
  21         173663  
  21         5344  
16 21     21   19406 use IO::Async::Loop;
  21         1200882  
  21         5784  
17 21     21   264 use Future::AsyncAwait;
  21         49  
  21         267  
18 21     21   15268 use PAGI::Utils qw(handle_lifespan);
  21         72954  
  21         2104  
19 21     21   11403 use FindBin;
  21         39243  
  21         81695  
20              
21             extends 'Gears::App';
22              
23             has param 'path' => (
24             coerce => (InstanceOf ['Path::Tiny'])
25             ->plus_coercions(Str, q{ Path::Tiny::path($_) }),
26             default => sub { $FindBin::Bin },
27             );
28              
29             has param 'env' => (
30             isa => Enum ['production', 'development', 'test'],
31             default => sub { $ENV{PAGI_ENV} // 'production' },
32             );
33              
34             has param 'initial_config' => (
35             isa => Str | HashRef,
36             default => sub { {} },
37             );
38              
39             has field 'loop' => (
40             isa => InstanceOf ['IO::Async::Loop'],
41             default => sub { IO::Async::Loop->new },
42             );
43              
44             # the base app controller
45             has field 'controller' => (
46             isa => InstanceOf ['Thunderhorse::Controller'],
47             lazy => '_build_app_controller',
48             );
49              
50             has extended 'router' => (
51             reader => '_router',
52             isa => InstanceOf ['Thunderhorse::Router'],
53             default => sub { Thunderhorse::Router->new },
54             );
55              
56             has extended 'config' => (
57             builder => 1,
58             );
59              
60             has field 'modules' => (
61             isa => ArrayRef,
62             default => sub { [] },
63             );
64              
65             has field 'extra_methods' => (
66             isa => HashRef,
67             default => sub {
68             {
69             controller => {},
70             }
71             },
72             );
73              
74             has field 'extra_middleware' => (
75             isa => ArrayRef,
76             default => sub { [] },
77             );
78              
79             has field 'extra_hooks' => (
80             isa => HashRef,
81             default => sub {
82             {
83             startup => [],
84             shutdown => [],
85             error => [],
86             }
87             },
88             );
89              
90             #############################
91             ### BOOTSTRAPPING SECTION ###
92             #############################
93              
94 39         89 sub BUILD ($self, $)
95 39     39 0 611 {
  39         118  
96 39         341 $self->late_configure;
97             }
98              
99             sub _build_config ($self)
100 39     39   716986 {
  39         111  
  39         293  
101 39         1134 my $conf = Thunderhorse::Config->new;
102 39         192628 foreach my $reader ($conf->readers->@*) {
103 39 50       319 next unless $reader isa 'Gears::Config::Reader::PerlScript';
104              
105             # make app available from .pl config files
106 39         380 $reader->declared_vars->%* = (
107             $reader->declared_vars->%*,
108             app => $self,
109             );
110             }
111              
112 39         886 return $conf;
113             }
114              
115             sub _build_app_controller ($self)
116 37     37   581 {
  37         80  
  37         62  
117 37         361 return $self->_build_controller('Thunderhorse::AppController');
118             }
119              
120 101         235 sub _build_context ($self, @pagi)
121 101     101   195 {
  101         237  
  101         155  
122 101         3153 return Thunderhorse::Context->new(
123             app => $self,
124             pagi => \@pagi,
125             );
126             }
127              
128             sub is_production ($self)
129 8     8 1 153 {
  8         18  
  8         12  
130 8         93 return $self->env eq 'production';
131             }
132              
133             sub router ($self)
134 121     121 1 5160 {
  121         257  
  121         179  
135 121         468 my $router = $self->_router;
136 121         3684 $router->set_controller($self->controller);
137 121         47706 return $router;
138             }
139              
140             sub configure ($self)
141 39     39 1 7152 {
  39         225  
  39         111  
142 39         342 my $config = $self->config;
143              
144 39         208 my $preconf = $self->initial_config;
145 39 100       218 if (!ref $preconf) {
146 2         35 $config->load_from_files($self->path->child($preconf), $self->env);
147             }
148             else {
149 37         365 $config->add(var => $preconf);
150             }
151              
152             # TODO: module dependencies and ordering
153 39         6757 my %modules = $config->get('modules', {})->%*;
154 39         1121 foreach my $module_name (sort keys %modules) {
155 3         15 $self->load_module($module_name, $modules{$module_name});
156             }
157             }
158              
159             sub late_configure ($self)
160 39     39 0 85 {
  39         84  
  39         222  
161 39         372 foreach my $controller ($self->config->get('controllers', [])->@*) {
162 3         57 $self->load_controller($controller);
163             }
164             }
165              
166 10         16 sub load_module ($self, $module_class, $args = {})
  10         20  
167 10     10 1 241 {
  10         20  
  10         17  
168 10         83 my $module = load_component(get_component_name($module_class, 'Thunderhorse::Module'))
169             ->new(app => $self, config => $args);
170              
171 10         127 push $self->modules->@*, $module;
172 10         41 return $self;
173             }
174              
175             ###################################
176             ### PAGI IMPLEMENTATION SECTION ###
177             ###################################
178              
179 103     103 1 184 async sub pagi ($self, $scope, $receive, $send)
  103         168  
  103         165  
  103         195  
  103         157  
  103         157  
180 103         196 {
181 103         264 my $scope_type = $scope->{type};
182              
183             return await handle_lifespan(
184             $scope, $receive, $send,
185 2     2   562 startup => sub { $self->_on_startup(@_) },
186 2     2   544 shutdown => sub { $self->_on_shutdown(@_) },
187 103 100       376 ) if $scope_type eq 'lifespan';
188              
189             # TODO: is this needed?
190 101 50       777 die 'Unsupported scope type'
191             unless $scope_type =~ m/^(http|sse|websocket)$/;
192              
193             # copy scope since we are modifying it
194 101         855 $scope = {$scope->%*};
195 101         638 my $ctx = $scope->{thunderhorse} = $self->_build_context($scope, $receive, $send);
196              
197             # query router for matches
198 101         11493 my $req = $ctx->req;
199             my $matches = $self->_router->match(
200             $req->path,
201 101         933 (lc join '.', grep { defined } $scope_type, $req->method),
  202         2023  
202             );
203              
204             # run location handlers' nested structure
205 101         643 await pagi_loop($ctx, $matches->@*);
206              
207             # 404 is possible even if we had matches, as long as no handler consumed
208             # the context
209 99 100       4477 if (!$ctx->is_consumed) {
210 4         113 await $self->render_error(undef, $ctx, 404);
211             }
212              
213 99         2790 return;
214             }
215              
216             sub run ($self)
217 103     103 1 186 {
  103         175  
  103         162  
218             # do not turn into PAGI application if run by a thunderhorse script
219 103 50       429 return $self if $ENV{THUNDERHORSE_SCRIPT};
220              
221 103     103   21620 my $pagi = sub (@args) {
  103         344  
  103         174  
222 103         481 return $self->pagi(@args);
223 103         663 };
224              
225 103         695 foreach my $mw ($self->extra_middleware->@*) {
226 11 50       143 if (ref $mw eq 'CODE') {
    50          
227 0         0 $pagi = $mw->($pagi);
228             }
229             elsif ($mw isa 'PAGI::Middleware') {
230 11         56 $pagi = $mw->wrap($pagi);
231             }
232             else {
233 0         0 Gears::X::Thunderhorse->raise('bad middleware, not CODE or PAGI::Middleware');
234             }
235             }
236              
237 103         455 return $pagi;
238             }
239              
240 14     14 1 29 async sub render_error ($self, $controller, $ctx, $code, $message = undef)
  14         23  
  14         24  
  14         22  
  14         39  
  14         33  
  14         47  
241 14         33 {
242 14 100 100     135 $message = defined $message && !$self->is_production ? $message : status_message($code);
243 14         203 await $ctx->res->status($code)->text($message);
244             }
245              
246 56     56 1 94 async sub render_response ($self, $controller, $ctx, $result)
  56         80  
  56         72  
  56         84  
  56         219  
  56         162  
247 56         98 {
248 56         286 await $ctx->res
249             ->status_try(200)
250             ->content_type_try('text/html')
251             ->send($result);
252             }
253              
254             #########################
255             ### EXTENSION METHODS ###
256             #########################
257              
258 6         14 sub add_method ($self, $for, $name, $code)
  6         14  
  6         43  
259 6     6 1 14688 {
  6         14  
  6         11  
260 6         48 my $area = $self->extra_methods->{$for};
261 6 50       128 Gears::X::Thunderhorse->raise("bad area '$for' for symbol '$name'")
262             unless defined $area;
263              
264             Gears::X::Thunderhorse->raise("symbol '$name' already exists in area '$for'")
265 6 50       28 if exists $area->{$name};
266              
267 6         37 $area->{$name} = $code;
268 6         35 return $self;
269             }
270              
271 11         16 sub add_middleware ($self, $mw)
272 11     11 1 239 {
  11         14  
  11         10  
273 11         52 push $self->extra_middleware->@*, $mw;
274 11         45 return $self;
275             }
276              
277 7         12 sub add_hook ($self, $hook, $handler)
  7         16  
278 7     7 1 140 {
  7         15  
  7         10  
279 7         35 push $self->extra_hooks->{$hook}->@*, $handler;
280 7         23 return $self;
281             }
282              
283             #####################
284             ### HOOKS SECTION ###
285             #####################
286              
287 18         38 sub _fire_hooks ($self, $hook, @args)
  18         37  
288 18     18   40 {
  18         43  
  18         32  
289 18         145 foreach my $handler ($self->extra_hooks->{$hook}->@*) {
290 4         21 $handler->(@args);
291             }
292             }
293              
294 2         7 sub _on_startup ($self, @args)
295 2     2   5 {
  2         34  
  2         8  
296 2         17 $self->_fire_hooks(startup => @args);
297 2         78 return $self->on_startup(@args);
298             }
299              
300 1         3 async sub on_startup ($self, $state)
  1         3  
  1         2  
  1         9  
301 1     1 1 2 {
302             }
303              
304 2         4 sub _on_shutdown ($self, @args)
305 2     2   6 {
  2         7  
  2         3  
306 2         9 $self->_fire_hooks(shutdown => @args);
307 2         64 return $self->on_shutdown(@args);
308             }
309              
310 1         3 async sub on_shutdown ($self, $state)
  1         3  
  1         2  
  1         8  
311 1     1 1 2 {
312             }
313              
314 11     11 1 201 async sub on_error ($self, $controller, $ctx, $error)
  11         17  
  11         18  
  11         20  
  11         42  
  11         29  
315 11         29 {
316 11 100       84 my $code = $error isa 'Gears::X::HTTP' ? $error->code : 500;
317 11         195 await +($controller // $self->controller)->render_error($ctx, $code, $error);
318             }
319              
320             __END__