File Coverage

lib/Web/Components/Loader.pm
Criterion Covered Total %
statement 36 36 100.0
branch n/a
condition n/a
subroutine 15 16 100.0
pod 1 1 100.0
total 52 53 100.0


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