File Coverage

lib/Web/Components/Loader.pm
Criterion Covered Total %
statement 34 34 100.0
branch n/a
condition n/a
subroutine 14 15 100.0
pod 1 1 100.0
total 49 50 100.0


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