File Coverage

blib/lib/PlackX/Framework/Handler.pm
Criterion Covered Total %
statement 132 163 80.9
branch 49 76 64.4
condition 37 70 52.8
subroutine 17 20 85.0
pod 0 13 0.0
total 235 342 68.7


line stmt bran cond sub pod time code
1 8     8   348439 use v5.36;
  8         31  
2             package PlackX::Framework::Handler {
3 8     8   515 use PXF::Util ();
  8         17  
  8         214  
4 8     8   38 use Scalar::Util qw(blessed);
  8         15  
  8         521  
5 8     8   4445 use HTTP::Status qw(status_message);
  8         47703  
  8         17220  
6              
7             my %globals;
8             our $psgix_streaming; # memoized, but in an "our" var so tests can change it
9       41 0   sub use_global_request_response { } # Override in subclass to turn on
10 0     0 0 0 sub global_request ($class) { $globals{$class->app_namespace}->[0] }
  0         0  
  0         0  
  0         0  
11 0     0 0 0 sub global_response ($class) { $globals{$class->app_namespace}->[1] }
  0         0  
  0         0  
  0         0  
12 10     10 0 19 sub error_response ($class, $code) { [$code, [], [status_message($code)." ($code)"]] } # Override for nicer message
  10         38  
  10         19  
  10         14  
  10         39  
13              
14             #
15             # App assembly section
16             #
17 11     11 0 1268 sub build_app ($class, %options) {
  11         22  
  11         19  
  11         14  
18             # Freeze the router
19 11         43 my $rt_engine = ($class->app_namespace . '::Router::Engine')->instance;
20 11         72 $rt_engine->freeze;
21              
22             # Honestly, it is probably better for the user to use Plack::Builder
23             # or URLMap or Cascade instead of doing this, but we do it here for
24             # convenience in development environments, at least for now. Think about
25             # removing this feature at a later date.
26 11         26 my $serve_static_files = delete $options{'serve_static_files'};
27 11         22 my $static_docroot = delete $options{'static_docroot'};
28 11 50       32 die "Unknown options: " . join(', ', keys %options) if %options;
29              
30 11     35   90 my $main_app = sub ($env) { psgi_response($class->handle_request($env, undef, $rt_engine)) };
  35         266  
  35         104582  
  35         89  
  35         51  
31 11   33     36 my $file_app = ($serve_static_files and do {
32             require Plack::App::File;
33             Plack::App::File->new(root => $static_docroot)->to_app;
34             });
35              
36             # if app_base is specified, use URLMap
37 11 50       61 if (my $app_base = $class->app_base) {
38 0         0 require Plack::App::URLMap;
39 0         0 my $mapper = Plack::App::URLMap->new;
40 0         0 $mapper->map($app_base => $main_app);
41 0 0       0 $mapper->map('/' => $file_app) if $file_app;
42 0         0 return $mapper->to_app;
43             }
44              
45             # Static file app with no app_base, so try one, try the other if it's 404
46             # (basically our own cascade whereas we could use Plack::App::Cascade).
47             # We prefer to serve the app's 404 page if the file app also returns 404
48             # because it is easier to customize the 404 page with PXF.
49             # Add a later date we might add a feature to intercept all 4xx and 5xx
50             # error codes at the last possible moment and render a user-defined page.
51 0     0   0 return sub ($env) {
  0         0  
  0         0  
52 0         0 my $main_resp = $main_app->($env);
53 0 0 0     0 return $main_resp if ref $main_resp and $main_resp->[0] != 404;
54 0         0 my $file_resp = $file_app->($env);
55 0 0 0     0 return $file_resp if ref $file_resp and $file_resp->[0] != 404;
56 0         0 return $main_resp;
57 11 50       33 } if $file_app;
58              
59             # no app_base, no static file app, just return the main app
60 11         106 return $main_app;
61             }
62              
63 11     11 0 23 sub app_base ($class) {
  11         19  
  11         21  
64 11   50     23 my $base = eval { $class->app_namespace->app_base } || eval { $class->app_namespace->uri_prefix } || '';
65 11 0 33     42 $base = '/'.$base if $base and length $base and substr($base,0,1) ne '/';
      33        
66 11         36 return $base;
67             }
68              
69             #
70             # Request handling section
71             #
72 42     42 0 1355 sub handle_request ($class, $env_or_req, $maybe_resp = undef, $maybe_rt_engine = undef) {
  41         81  
  41         77  
  41         80  
  41         99  
  41         103  
73 41         245 my $app_namespace = $class->app_namespace;
74              
75             # Get or create default request and response objects
76 41         161 my $env = $class->env_or_req_to_env($env_or_req);
77 41         178 my $request = $class->env_or_req_to_req($env_or_req);
78 41   33     689 my $response = $maybe_resp || ($app_namespace . '::Response')->new->set_defaults;
79              
80             # Memoize server info and maybe set request/response globals
81 41 50       145 $psgix_streaming = $env->{'psgi.streaming'} ? !!1 : !!0
    100          
82             if !defined $psgix_streaming;
83 41 50       157 $globals{$app_namespace} = [$request, $response]
84             if $class->use_global_request_response;
85              
86             # Set up stash
87 41   100     166 my $stash = ($request->stash or $response->stash or {});
88 41         575 $request->stash($stash);
89 41         264 $response->stash($stash);
90              
91             # Maybe set up Templating, if loaded
92 41 50       460 if (PXF::Util::is_module_loaded($app_namespace . '::Template')) {
93             eval {
94 0         0 my $template = ($app_namespace . '::Template')->new($response);
95 0         0 $template->set(STASH => $stash, REQUEST => $request, RESPONSE => $response);
96 0         0 $response->template($template);
97 0 0       0 } or do {
98 0         0 warn "$app_namespace\::Template module loaded, but unable to set up template: $@"
99             . " (Hint: Did you use/import from it or set up templating manually?)\n";
100             };
101             }
102              
103             # Clear flash if set, set response defaults, and route request
104 41 100       228 $response->flash(undef) if $request->flash;
105 41         174 return $class->route_request($request, $response, $maybe_rt_engine);
106             }
107              
108 41     41 0 71 sub route_request ($class, $request, $response, $rt_engine = undef) {
  41         72  
  41         67  
  41         63  
  41         71  
  41         70  
109 41   66     134 $rt_engine //= ($class->app_namespace . '::Router::Engine')->instance;
110 41 100       192 if (my $match = $rt_engine->match($request)) {
111 31 50       163 $request->route_base($match->{base}) if defined $match->{base};
112 31         169 $request->route_parameters($match->{route_parameters});
113              
114             # Execute global and route-specific prefilters
115 31 100       261 if (my $filterset = $match->{prefilters}) {
116 11         37 my $ret = execute_filters($filterset, $request, $response);
117 11 50 33     38 return $ret if $ret and is_valid_response($ret);
118             }
119              
120             # Execute main action
121 31         303 my $result = $match->{action}->($request, $response);
122 31 50 33     172 unless ($result and ref $result) {
123 0         0 warn "PlackX::Framework - Invalid result '$result'\n";
124 0         0 return $class->error_response(500);
125             }
126              
127             # Check if the result is actually another request object
128 31 100       261 return $class->handle_request($result) if $result->isa('Plack::Request');
129 25 50       92 return $class->error_response unless $result->isa('Plack::Response');
130 25         51 $response = $result;
131              
132             # Execute postfilters
133 25 100       89 if (my $filterset = $match->{postfilters}) {
134 11         30 my $ret = execute_filters($filterset, $request, $response);
135 11 50 33     40 return $ret if $ret and is_valid_response($ret);
136             }
137              
138             # Clean up (does server support cleanup handlers? Add to list or else execute now)
139 25 100 66     98 if ($response->cleanup_callbacks and scalar $response->cleanup_callbacks->@* > 0) {
140 3 50       77 if ($request->env->{'psgix.cleanup'}) {
141 0         0 push $request->env->{'psgix.cleanup.handlers'}->@*, $response->cleanup_callbacks->@*;
142             } else {
143 3         46 $_->($request->env) for $response->cleanup_callbacks->@*;
144             }
145             }
146              
147 25 50       293 return $response if is_valid_response($response);
148             }
149              
150 10         357 return $class->error_response(404);
151             }
152              
153             #
154             # Helper function and method section
155             #
156 22     22 0 37 sub execute_filters ($filters, $request, $response) {
  22         37  
  22         37  
  22         31  
  22         36  
157 22 50 33     132 return unless $filters and ref $filters eq 'ARRAY';
158 22         52 foreach my $filter (@$filters) {
159 37 100       183 $filter = { action => $filter, params => [] } if ref $filter eq 'CODE';
160 37         70 my $response = $filter->{action}->($request, $response, @{$filter->{params}});
  37         155  
161 37 50 33     135 return $response if $response and is_valid_response($response);
162             }
163 22         52 return;
164             }
165              
166             sub is_valid_response {
167 35     35 0 86648 my $response = pop;
168 35 100 100     214 return !!0 unless defined $response and ref $response;
169 33 100 100     141 return !!1 if ref $response eq 'ARRAY' and (@$response == 3 or @$response == 2);
      100        
170 31 100 100     626 return !!1 if blessed $response and $response->can('finalize');
171 5         29 return !!0;
172             }
173              
174 37     37 0 1142 sub psgi_response ($resp) {
  37         168  
  37         65  
175 37 100       163 return $resp
176             if !blessed $resp;
177              
178 26 100 100     213 return $resp->finalize
179             if not $resp->can('stream') or not $resp->stream;
180              
181 1     1   1075 return sub ($PSGI_responder) {
  1         3  
  1         3  
182 1         11 my $PSGI_writer = $PSGI_responder->($resp->finalize_sb);
183 1         3454 $resp->stream_writer($PSGI_writer);
184 1         11 $resp->stream->();
185 1         17 $PSGI_writer->close;
186 2 100       25 } if $psgix_streaming;
187              
188             # Simulate streaming, use "do" to make it look consistent with the above
189 1         2 return do {
190 1         3 $resp->stream->(); # execute coderef
191 1         8 $resp->stream(undef); # unset stream property
192 1         19 $resp->finalize; # finalize
193             };
194             }
195              
196 41     41 0 72 sub env_or_req_to_req ($class, $env_or_req) {
  41         100  
  41         69  
  41         76  
197 41 100 66     209 if (ref $env_or_req and ref $env_or_req eq 'HASH') {
    50 33        
198 35         102 return ($class->app_namespace . '::Request')->new($env_or_req);
199             } elsif (blessed $env_or_req and $env_or_req->isa('PlackX::Framework::Request')) {
200 6         14 return $env_or_req;
201             }
202 0         0 die 'Neither a PSGI-type HASH reference nor a PlackX::Framework::Request object.';
203             }
204              
205 41     41 0 89 sub env_or_req_to_env ($class, $env_or_req) {
  41         74  
  41         76  
  41         66  
206 41 100 66     279 if (ref $env_or_req and ref $env_or_req eq 'HASH') {
    50 33        
207 35         95 return $env_or_req;
208             } elsif (blessed $env_or_req and $env_or_req->isa('PlackX::Framework::Request')) {
209 6         31 return $env_or_req->env;
210             }
211 0           die 'Neither a PSGI-type HASH reference nor a PlackX::Framework::Request object.';
212             }
213             }
214              
215             1;