File Coverage

blib/lib/Dancer2/Plugin/DoFile.pm
Criterion Covered Total %
statement 61 75 81.3
branch 18 30 60.0
condition 19 28 67.8
subroutine 7 9 77.7
pod 0 4 0.0
total 105 146 71.9


line stmt bran cond sub pod time code
1             $Dancer2::Plugin::DoFile::VERSION = '0.12';
2             # ABSTRACT: File-based MVC plugin for Dancer2
3              
4             use strict;
5 1     1   729248 use warnings;
  1         2  
  1         22  
6 1     1   4  
  1         1  
  1         19  
7             use Dancer2::Plugin;
8 1     1   422  
  1         9616  
  1         7  
9             use JSON;
10 1     1   2918 use Hash::Merge;
  1         5909  
  1         4  
11 1     1   437  
  1         2909  
  1         634  
12             has page_loc => (
13             is => 'rw',
14             default => sub {'dofiles/pages'},
15             );
16              
17             has default_file => (
18             is => 'rw',
19             default => sub {'index'},
20             );
21              
22             has extension_list => (
23             is => 'rw',
24             default => sub { ['.do', '.view']}
25             );
26              
27             plugin_keywords 'dofile';
28              
29             my %dofiles;
30              
31             my $self = shift;
32             my $settings = $self->config;
33 1     1 0 57  
34 1         17 $settings->{$_} and $self->$_( $settings->{$_} )
35             for qw/ page_loc default_file extension_list /;
36             }
37 1   100     58  
38             my $plugin = shift;
39             my $arg = shift;
40             my %opts = @_;
41 7     7 0 133894  
42 7         11 my $app = $plugin->app;
43 7         11 my $settings = $app->settings;
44             my $method = $app->request->method;
45 7         18 my $pageroot = $settings->{appdir};
46 7         29 if ($pageroot !~ /\/$/) {
47 7         338 $pageroot .= "/";
48 7         34 }
49 7 50       18 $pageroot .= $plugin->page_loc;
50 7         13  
51             my $path = $arg || $app->request->path;
52 7         15  
53             # If any one of these returns content then we stop processing any more of them
54 7   33     23 # Content is defined as an array ref (it's an Obj2HTML array), a hashref with a "content" element, or a scalar (assumed HTML string - it's not checked!)
55             # This can lead to some interesting results if someone doesn't explicitly return undef when they want to fall through to the next file
56             # as perl will return the last evaluated value, which would be intepretted as content according to the above rules
57              
58             my $merger = Hash::Merge->new('RIGHT_PRECEDENT');
59              
60             my $stash = $opts{stash} || {};
61 7         58  
62             # Safety first...
63 7   50     395 $path =~ s|/$|"/".$plugin->default_file|e;
64             $path =~ s|^/+||;
65             $path =~ s|\.\./||g;
66 7         16 $path =~ s|~||g;
  1         4  
67 7         22  
68 7         11 if (!$path) { $path = $plugin->default_file; }
69 7         11 if (-d $pageroot."/$path") {
70             if ($path =~ /\/$/) {
71 7 50       19 $path .= "/".$plugin->default_file;
  0         0  
72 7 50       176 } else {
73 0 0       0 return {
74 0         0 url => "/$path/",
75             redirect => 1,
76             done => 1
77 0         0 };
78             }
79             }
80              
81             OUTER:
82             foreach my $ext (@{$plugin->extension_list}) {
83             foreach my $m ("", "-$method") {
84             my $cururl = $path;
85 7         20 my @path = ();
  7         25  
86 12         60  
87 18         115 # This iterates back through the path to find the closest FILE downstream, using the rest of the url as a "path" argument
88 18         22 while (!-f $pageroot."/".$cururl.$m.$ext && $cururl =~ s/\/([^\/]*)$//) {
89             if ($1) { unshift(@path, $1); }
90             }
91 18   100     257  
92 5 50       17 # "Do" the file
  5         69  
93             if ($cururl) {
94             my $result;
95             if (defined $dofiles{$pageroot."/".$cururl.$m.$ext}) {
96 18 50       47 $result = $dofiles{$pageroot."/".$cururl.$m.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
97 18         21  
98 18 50       160 } elsif (-f $pageroot."/".$cururl.$m.$ext) {
    100          
99 0         0 our $args = { path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env };
100              
101             $result = do($pageroot."/".$cururl.$m.$ext);
102 10         54 if ($@ || $!) { $plugin->app->log( error => "Error processing $pageroot / $cururl.$m.$ext: $@ $!\n"); }
103             if (ref $result eq "CODE") {
104 10         2523 $dofiles{$pageroot."/".$cururl.$m.$ext} = $result;
105 10 50 33     151 $result = $result->($args);
  0         0  
106 10 50       23 }
107 0         0 }
108 0         0  
109             if (defined $result && ref $result eq "HASH") {
110             if (defined $result->{url} && !defined $result->{done}) {
111             $path = $result->{url};
112 18 100 66     77 next OUTER;
    50 33        
    50          
113 10 100 100     27 }
114 1         2 if (defined $result->{content} || $result->{url} || $result->{done}) {
115 1         3 return $result;
116             } else {
117 9 100 100     31 $stash = $merger->merge($stash, $result);
      66        
118 6         182 }
119             # Move on to the next file
120 3         10  
121             } elsif (ref $result eq "ARRAY") {
122             return { content => $result };
123              
124             } elsif (!ref $result && $result) {
125 0         0 # do we assume this is HTML? Or a file to use in templating? Who knows!
126             return { content => $result };
127              
128             }
129 0         0 }
130             }
131             }
132              
133             # If we got here we didn't find a do file that returned some content
134             return { status => 404 };
135             }
136              
137 1         32 my ( $self, $view ) = @_;
138             return path($view);
139             }
140             my ( $self, $layout ) = @_;
141 0     0 0   return $layout;
142 0           }
143              
144             1;
145 0     0 0    
146 0            
147             =pod
148              
149             =head1 NAME
150              
151             Dancer2::Plugin::DoFile - A file based MVC style plugin for Dancer2
152              
153             =head1 SYNOPSYS
154              
155             In your config.yml
156              
157             plugins:
158             DoFile:
159             page_loc: "dofiles/pages"
160             default_file: "index"
161             extension_list: ['.do','.view']
162              
163             Make sure you have created the directory used for page_loc
164              
165             Within a route in dancer2:
166              
167             my $result = dofile 'path/to/file'
168              
169             You must not include the extension of the file as part of the path, as this will
170             be added per the settings.
171              
172             Or a default route, with example handling of some return values:
173              
174             prefix '/';
175             any qr{.*} => sub {
176             my $self = shift;
177             my $result = dofile undef;
178             if ($result && ref $result eq "HASH") {
179             if (defined $result->{status}) {
180             status $result->{status};
181             }
182             if (defined $result->{url}) {
183             if (defined $result->{redirect} && $result->{redirect} eq "forward") {
184             return forward $result->{url};
185             } else {
186             return redirect $result->{url};
187             }
188             } elsif (defined $result->{content}) {
189             return $result->{content};
190             }
191             }
192             };
193              
194             When the 1st parameter to 'dofile' is undef it'll use the request URI to work
195             out what the file(s) to execute are.
196              
197             =head1 DESCRIPTION
198              
199             DoFile is a way of automatically pulling multiple perl files to execute as a way
200             to simplify routing complexity in Dancer2 for very large applications. In
201             particular it was designed to offload "as many as possible" URIs that related to
202             some standard functionality through a default route, just by having files
203             existing for the specific URI.
204              
205             The magic will look through your filesystem for files to 'do' (execute), and
206             there may be several. The intent is to split out controller files and
207             view files, and these may individually be rolled out or split out. In the
208             default configuration the controller files are suffixed .do, and the view files
209             .view
210              
211             =head2 File Search Ordering
212              
213             When presented with the URI C<path/to/file> DoFile will begin searching for
214             files that can be executed for this request, until it finds one that returns
215             something that looks like content, a URL or is told you're done, when it stops.
216              
217             Files are searched:
218              
219             =over 4
220              
221             =item * By extension
222              
223             The default extensions .do and .view are checked, unless defined in your
224             config.yml. The intention here is that .do files contain controller code and
225             don't typically return content, but may return redirects. After .do files have
226             been executed, .view files are executed. These are expected to return content.
227              
228             You can define as many extensions as you like. You could, for example have:
229             C<['.init','.do','.view','.final']>
230              
231             =item * Root/HTTP request method
232              
233             For each extension, first the "root" file C<file.ext> is tested, then a file
234             that matches C<file-METHOD.ext> is tested (where METHOD is the HTTP request
235             method for this request, .ext is the extension).
236              
237             =item * Iterating up the directory tree
238              
239             If your call to C<path/to/file> results in a miss for C<path/to/file.do>, DoFile
240             will then test for C<path/to.do> and finally C<path.do> before moving on to
241             C<path/to/file-METHOD.do>
242              
243             Once DoFile has found one it will not transcend the directory tree any further.
244             Therefore defining C<path/to/file.do> and C<path/to.do> will not result in
245             both being executed for the URI C<path/to/file> - only the first will be
246             executed.
247              
248             =back
249              
250             If you define files like so:
251              
252             path.do
253             path/
254             to.view
255             to/
256             file-POST.do
257              
258             A POST to the URI C<path/to/file> will execute C<path.do>, then
259             C<path/to/file-POST.do> and finally C<path/to.view>.
260              
261             =head2 Arguments to the executed files
262              
263             During execution of the file a hashref called $args is available that contains
264             some important things.
265              
266             If the executed file returns a coderef, the coderef is executed with this same
267             hashref as the only argument.
268              
269             =over 4
270              
271             =item * path (arrayref)
272              
273             Anything that appears after the currently executing file on the URI. For example
274             if I request C</path/to/file> and DoFile is executing C<path-POST.do>, the
275             C<path> element will contain ['to','file']
276              
277             =item * this_url (string)
278              
279             The currently executing file without any extension. In the above example this
280             would be C<path>.
281              
282             =item * stash (hashref)
283              
284             The stash can be initially passed from the router:
285              
286             dofile 'path/to/file', stash => { option => 1 }
287              
288             The stash can be read/written to from each file that executes:
289              
290             if ($args->{stash}->{option} == 1) {
291             $args->{stash}->{anotheroption} = 2;
292             }
293              
294             Or if the file being executed returns a hashref that does not contain any of
295             the elements C<contents>, C<url> or C<done> (see below), it's merged into the
296             stash automatically for passing on to the next file to be executed
297              
298             The stash is used to pass internal state down the file chain.
299              
300             =item * dofile_plugin (object)
301              
302             Just in case the file being executed wants to mess about with Dancer2 or
303             the plugin's internals.
304              
305             =back
306              
307             =head2 How DoFile interprets individual executed files response
308              
309             The result (returned value) of each file is checked; if something is returned
310             DoFile will inspect the value to determine what to do next.
311              
312             =head3 Coderef (anonymous sub)
313              
314             You can return a coderef; this will be cached within the plugin and the file
315             will not be checked again, but the coderef will be executed each time that
316             "file" is requested. Generally whether the file should be checked or not is
317             left up to the application (e.g. C<plackup -R ./ -r ...>).
318              
319             In the case a coderef is used, when the code is executed it is passed one
320             argument, a hashref, which is the stash. This saves needing to import the stash
321             from within the code of the file.
322              
323             The return of the coderef will be evaluated exactly as below.
324              
325             =head3 Internal Redirects
326              
327             If a hashref is returned it's checked for a C<url> element but NO C<done>
328             element. In this case, the DoFile restarts from the begining using the new URL.
329             This is a method for internally redirecting. For example, returning:
330              
331             {
332             url => "account/login"
333             }
334              
335             Will cause DoFile to start over with the new URI C<account/login>, without
336             processing any more files from the old URI. The stash is preserved.
337              
338             =head3 Content
339              
340             If a scalar or arrayref is returned, it's wrapped into a hashref into the
341             C<contents> element and sent back to the router.
342              
343             If a hashref is returned and contains a C<contents> element, no more files will
344             be processed. The entire hashref is returned to the router. NB: the
345             C<contents> element must contain something that evals to true, else it's
346             considered not there.
347              
348             =head3 Done
349              
350             If a hashref is returned and there is a C<done> element that evals to a true
351             value, DoFile will stop processing files and return the returned hashref to
352             the router.
353              
354             =head3 Continue
355              
356             If a hashref is returned and there is no C<url>, C<content> or C<done> element
357             then the contents of the hasref is combined with the stash and DoFile will look
358             for the next file.
359              
360             If nothing is returned at all, DoFile will continue with the next file.
361              
362             =head2 What the router gets back
363              
364             DoFile will always return a hashref, even if the files being executed do not
365             return a hashref. This hashref may have anything, but the recommended design
366             is to return one of the following:
367              
368             =over 4
369              
370             =item * A C<contents> element
371              
372             The implication is that you've had the web page to be served back. Note that
373             DoFile doesn't care if this is a scalar string or an arrayref. This Plugin
374             was designed to work with Obj2HTML, so in the case of an arrayref the
375             implication is that Obj2HTML should be asked to convert that to HTML.
376              
377             =item * A C<url> element
378              
379             In this case the router should probably send a 30x response redirecting the
380             client, or perform an internal forward... implementors choice.
381              
382             =item * A C<status> element
383              
384             This could be used to set the status code for returning to the client
385              
386             =back
387              
388             DoFile may however return pretty much whatever you want to handle in your final
389             router code.
390              
391             =head1 EXAMPLES
392              
393             As noted, what's returned from a DoFile can contain anything. That gives you
394             the opportunity to do pretty much whatever you want with what's returned.
395              
396              
397             =head1 AUTHOR
398              
399             Pero Moretti
400              
401             =head1 COPYRIGHT AND LICENSE
402              
403             This software is copyright (c) 2022 by Pero Moretti.
404              
405             This is free software; you can redistribute it and/or modify it under
406             the same terms as the Perl 5 programming language system itself.