File Coverage

lib/Web/Components/Loader.pm
Criterion Covered Total %
statement 42 42 100.0
branch 1 2 50.0
condition n/a
subroutine 17 18 100.0
pod 1 1 100.0
total 61 63 98.4


line stmt bran cond sub pod time code
1             package Web::Components::Loader;
2              
3 1     1   201325 use strictures;
  1         3  
  1         10  
4 1     1   232 use namespace::autoclean;
  1         2  
  1         10  
5              
6 1         240 use HTTP::Status qw( HTTP_BAD_REQUEST HTTP_FOUND
7 1     1   676 HTTP_INTERNAL_SERVER_ERROR );
  1         3781  
8 1     1   15 use Try::Tiny;
  1         4  
  1         77  
9 1         22 use Unexpected::Types qw( ArrayRef CodeRef HashRef NonEmptySimpleStr
10 1     1   7 Object RequestFactory );
  1         1  
11 1         9 use Web::Components::Util qw( deref exception is_arrayref
12 1     1   1597 load_components throw );
  1         3  
13 1     1   1235 use Web::ComposableRequest;
  1         113220  
  1         51  
14 1     1   914 use Web::Simple::Role;
  1         403  
  1         9  
15              
16             requires qw( config log );
17              
18             # Attribute constructors
19             my $_build_factory_args = sub {
20 1     1   12 my $self = shift;
21              
22 1         16 my $prefix = deref $self->config, 'name';
23              
24             return sub {
25 2     2   6295 my ($self, $attr) = @_;
26              
27 2 50       9 $prefix and $attr->{domain_prefix} = $prefix;
28              
29 2         26 return $attr;
30 1         18 };
31             };
32              
33             my $_build__factory = sub {
34 1     1   28 my $self = shift;
35              
36 1         17 return Web::ComposableRequest->new
37             ( buildargs => $self->factory_args, config => $self->config );
38             };
39              
40             my $_build__routes = sub {
41 1     1   42 my $controllers = $_[ 0 ]->controllers; my @keys = keys %{ $controllers };
  1         40  
  1         4  
42              
43 1         5 return [ map { $controllers->{ $_ }->dispatch_request } sort @keys ];
  1         5  
44             };
45              
46             # Public attributes
47             has 'factory_args' => is => 'lazy', isa => CodeRef,
48             builder => $_build_factory_args;
49              
50             has 'controllers' => is => 'lazy', isa => HashRef[Object], builder => sub {
51 1     1   25 load_components 'Controller', application => $_[ 0 ] };
52              
53             has 'models' => is => 'lazy', isa => HashRef[Object], builder => sub {
54 1     1   1109 load_components 'Model', application => $_[ 0 ], views => $_[ 0 ]->views };
55              
56             has 'views' => is => 'lazy', isa => HashRef[Object], builder => sub {
57 1     1   16 load_components 'View', application => $_[ 0 ] };
58              
59             # Private attributes
60             has '_action_suffix' => is => 'lazy', isa => NonEmptySimpleStr,
61 1     1   1135 builder => sub { deref $_[ 0 ]->config, 'action_suffix', '_action' };
62              
63             has '_factory' => is => 'lazy', isa => RequestFactory,
64             builder => $_build__factory, handles => [ 'new_from_simple_request' ];
65              
66             has '_routes' => is => 'lazy', isa => ArrayRef[CodeRef],
67             builder => $_build__routes;
68              
69             has '_tunnel_method' => is => 'lazy', isa => NonEmptySimpleStr,
70 1     1   29 builder => sub { deref $_[ 0 ]->config, 'tunnel_method', 'from_request' };
71              
72             # Private functions
73             my $_header = sub {
74             return [ 'Content-Type' => 'text/plain', @{ $_[ 0 ] // [] } ];
75             };
76              
77             # Private methods
78             my $_internal_server_error = sub {
79             my ($self, $e) = @_; $self->log->error( $e );
80              
81             return [ HTTP_INTERNAL_SERVER_ERROR, $_header->(), [ $e ] ];
82             };
83              
84             my $_parse_sig = sub {
85             my ($self, $args) = @_;
86              
87             exists $self->models->{ $args->[ 0 ] } and return @{ $args };
88              
89             my ($moniker, $method) = split m{ / }mx, $args->[ 0 ], 2;
90              
91             exists $self->models->{ $moniker } and shift @{ $args }
92             and return $moniker, $method, @{ $args };
93              
94             return;
95             };
96              
97             my $_recognise_signature = sub {
98             my ($self, $args) = @_;
99              
100             is_arrayref $args and $args->[ 0 ]
101             and exists $self->models->{ $args->[ 0 ] } and return 1;
102              
103             my ($moniker, $method) = split m{ / }mx, $args->[ 0 ], 2;
104              
105             $moniker and exists $self->models->{ $moniker } and return 1;
106              
107             return 0;
108             };
109              
110             my $_redirect = sub {
111             my ($self, $req, $stash) = @_;
112              
113             my $code = $stash->{code} // HTTP_FOUND;
114             my $redirect = $stash->{redirect};
115             my $message = $redirect->{message};
116             my $location = $redirect->{location};
117              
118             if ($message and $req->can( 'session' )) {
119             $req->can( 'loc_default' )
120             and $self->log->info( $req->loc_default( @{ $message } ) );
121              
122             my $mid; $mid = $req->session->add_status_message( $message )
123             and $location->query_form( $location->query_form, 'mid' => $mid );
124             }
125              
126             return [ $code, [ 'Location', $location ], [] ];
127             };
128              
129             my $_render_view = sub {
130             my ($self, $moniker, $method, $req, $stash) = @_;
131              
132             is_arrayref $stash and return $stash; # Plack response short circuits view
133              
134             exists $stash->{redirect} and return $self->$_redirect( $req, $stash );
135              
136             $stash->{view}
137             or throw 'Model [_1] method [_2] stashed no view', [ $moniker, $method ];
138              
139             my $view = $self->views->{ $stash->{view} }
140             or throw 'Model [_1] method [_2] unknown view [_3]',
141             [ $moniker, $method, $stash->{view} ];
142             my $res = $view->serialize( $req, $stash )
143             or throw 'View [_1] returned false', [ $stash->{view} ];
144              
145             return $res
146             };
147              
148             my $_render_exception = sub {
149             my ($self, $moniker, $req, $e) = @_; my $res;
150              
151             ($e->can( 'rv' ) and $e->rv > HTTP_BAD_REQUEST)
152             or $e = exception $e, { rv => HTTP_BAD_REQUEST };
153              
154             my $attr = deref $self->config, 'loader_attr', { should_log_errors => 1 };
155              
156             if ($attr->{should_log_errors}) {
157             my $username = $req->can( 'username' ) ? $req->username : 'unknown';
158             my $msg = "${e}"; chomp $msg; $self->log->error( "${msg} (${username})" );
159             }
160              
161             try {
162             my $stash = $self->models->{ $moniker }->exception_handler( $req, $e );
163              
164             $res = $self->$_render_view( $moniker, 'exception_handler', $req, $stash);
165             }
166             catch { $res = $self->$_internal_server_error( "${e}\n${_}" ) };
167              
168             return $res;
169             };
170              
171             my $_render = sub {
172             my ($self, @args) = @_;
173              
174             $self->$_recognise_signature( $args[ 0 ] ) or return @args;
175              
176             my ($moniker, $method, undef, @request) = $self->$_parse_sig( $args[ 0 ] );
177              
178             my $opts = { domain => $moniker }; my ($req, $res);
179              
180             try { $req = $self->new_from_simple_request( $opts, @request ) }
181             catch { $res = $self->$_internal_server_error( $_ ) };
182              
183             $res and return $res;
184              
185             try {
186             $method eq $self->_tunnel_method
187             and $method = $req->tunnel_method.$self->_action_suffix;
188              
189             my $stash = $self->models->{ $moniker }->execute( $method, $req );
190              
191             $res = $self->$_render_view( $moniker, $method, $req, $stash );
192             }
193             catch { $res = $self->$_render_exception( $moniker, $req, $_ ) };
194              
195             $req->can( 'session' ) and $req->session->update;
196              
197             return $res;
198             };
199              
200             my $_filter = sub () {
201             my $self = shift; return response_filter { $self->$_render( @_ ) };
202             };
203              
204             # Construction
205       0 1   sub dispatch_request { # uncoverable subroutine
206             # Not applied if it already exists in the consuming class
207             }
208              
209             around 'dispatch_request' => sub {
210             return $_filter, @{ $_[ 1 ]->_routes };
211             };
212              
213             1;
214              
215             __END__
216              
217             =pod
218              
219             =encoding utf-8
220              
221             =head1 Name
222              
223             Web::Components::Loader - Loads and instantiates MVC components
224              
225             =head1 Synopsis
226              
227             package Component::Server;
228              
229             use Class::Usul;
230             use Plack::Builder;
231             use Web::Simple;
232             use Moo;
233              
234             has '_usul' => is => 'lazy', builder => sub {
235             Class::Usul->new( config => { appclass => __PACKAGE__ } ) },
236             handles => [ 'config', 'debug', 'l10n', 'lock', 'log' ];
237              
238             with q(Web::Components::Loader);
239              
240             =head1 Description
241              
242             Loads and instantiates MVC components. Searches the namespaces; C<Controller>,
243             C<Model>, and C<View> in the consuming classes library root. Any components
244             found are loaded and instantiated
245              
246             The component collection references are passed to the component constructors
247             so that a component can discover any dependent components. The collection
248             references are not fully populated when the component is instantiated so
249             attributes that default to component references should be marked as lazy
250              
251             =head1 Configuration and Environment
252              
253             This role requires C<config> and C<log> methods in the consuming class
254              
255             Defines the following attributes;
256              
257             =over 3
258              
259             =item C<controllers>
260              
261             An array reference of controller object reference sorted into C<moniker>
262             order
263              
264             =item C<models>
265              
266             A hash reference of model object references
267              
268             =item C<view>
269              
270             A hash reference of view object references
271              
272             =back
273              
274             =head1 Subroutines/Methods
275              
276             =head2 C<dispatch_request>
277              
278             Installs a response filter that processes and renders the responses from
279             the controller methods
280              
281             Controller responses that do not match the expected signature are allowed to
282             bubble up
283              
284             The expected controller return value signature is;
285              
286             [ 'model_moniker', 'method_name', @web_simple_request_parameters ]
287              
288             The L<Web::Simple> request parameters are used to instantiate an instance of
289             L<Web::ComposableRequest::Base>
290              
291             The specified method on the model select by the moniker is called passing the
292             request object in. A hash references, the stash, is the expected response and
293             this is passed along with the request object into the view which renders the
294             response
295              
296             Array references, a L<Plack> response, are allowed to bubble up and bypass
297             the call to the view
298              
299             If the stash contains a redirect attribute then a redirect response is
300             generated. Any message intended to be viewed by the user is stored in the
301             session and is retrieved by the next request
302              
303             =head1 Diagnostics
304              
305             None
306              
307             =head1 Dependencies
308              
309             =over 3
310              
311             =item L<HTTP::Message>
312              
313             =item L<Try::Tiny>
314              
315             =item L<Unexpected>
316              
317             =item L<Web::Simple>
318              
319             =back
320              
321             =head1 Incompatibilities
322              
323             There are no known incompatibilities in this module
324              
325             =head1 Bugs and Limitations
326              
327             There are no known bugs in this module. Please report problems to
328             http://rt.cpan.org/NoAuth/Bugs.html?Dist=Web-Components.
329             Patches are welcome
330              
331             =head1 Acknowledgements
332              
333             Larry Wall - For the Perl programming language
334              
335             =head1 Author
336              
337             Peter Flanigan, C<< <pjfl@cpan.org> >>
338              
339             =head1 License and Copyright
340              
341             Copyright (c) 2017 Peter Flanigan. All rights reserved
342              
343             This program is free software; you can redistribute it and/or modify it
344             under the same terms as Perl itself. See L<perlartistic>
345              
346             This program is distributed in the hope that it will be useful,
347             but WITHOUT WARRANTY; without even the implied warranty of
348             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE
349              
350             =cut
351              
352             # Local Variables:
353             # mode: perl
354             # tab-width: 3
355             # End:
356             # vim: expandtab shiftwidth=3: