File Coverage

lib/Util/EvalSnippet.pm
Criterion Covered Total %
statement 70 72 97.2
branch 18 28 64.2
condition 8 11 72.7
subroutine 13 13 100.0
pod 1 1 100.0
total 110 125 88.0


line stmt bran cond sub pod time code
1             package Util::EvalSnippet;
2 1     1   70738 use 5.020;
  1         5  
3 1     1   8 use strict;
  1         3  
  1         25  
4 1     1   4 use warnings;
  1         2  
  1         29  
5 1     1   407 use PadWalker qw(peek_my peek_our);
  1         653  
  1         58  
6 1     1   469 use File::Slurp qw(read_file);
  1         13017  
  1         69  
7 1     1   9 use Cwd 'abs_path';
  1         3  
  1         101  
8              
9             our $VERSION = '0.02';
10              
11             sub import {
12 3     3   3837 my ($package, $msg) = @_;
13 3 100 66     50 if ($msg and $msg eq 'safe' and !$ENV{ALLOW_SNIPPETS}) {
      100        
14 1         52 die "\n\nCan't use Util::EvalSnippet in 'safe' mode if ALLOW_SNIPPETS env var is not set";
15             }
16 2         8 my $callerpkg = caller(0);
17 1     1   7 no strict 'refs';
  1         2  
  1         783  
18 2         27 *{"$callerpkg\::eval_snippet"} = \&eval_snippet;
  2         52  
19             }
20              
21             our $snippet_intro=q{# --snippet-info-header--
22             # This is a snippet. It will run in the context of the place where it was called from
23             # For documentation, "perldoc Util::EvalSnippet"
24             # This snippet was created here:
25             # line: %s
26             # file: %s
27             # (note, line number may have changed since this snippet was created!)
28             # --snippet-info-header--
29             };
30              
31             sub eval_snippet {
32              
33 6   100 6 1 15506 my $snippet_name = shift || '';
34 6 50       37 $snippet_name =~ /^\w*$/ or die "Snippet name must be word characters only";
35              
36 6         50 my ($caller_package,$caller_filename,$caller_line) = (caller(0));
37              
38 6         20 my $snippet_dir = _snippet_dir();
39              
40 6         17 my $snippet_path = $snippet_dir.'/'.$caller_package;
41 6 100       18 $snippet_name and $snippet_path.="-$snippet_name";
42              
43 6 100       139 unless (-f $snippet_path) {
44 1 50       30 unless (-d $snippet_dir) {
45 0 0       0 mkdir($snippet_dir) or die "Can't make snippet dir ($snippet_dir): $!";
46             }
47 1 50       120 open(my $fh,">",$snippet_path)
48             or die "Can't create snippet ($snippet_path)";
49 1         100 my $path = abs_path($caller_filename);
50 1         21 printf $fh $snippet_intro,$caller_line,$path;
51 1         64 close($fh);
52             }
53              
54             # interpolate variables
55 6         86 my $peek_my = peek_my(1);
56 6         33 my $peek_our = peek_our(1);
57              
58 6         30 my $content = read_file($snippet_path);
59              
60 6         573 $content = _process(
61             content => $content,
62             type => 'my',
63             vars => [keys %$peek_my],
64             );
65              
66 6         28 $content = _process(
67             content => $content,
68             type => 'our',
69             vars => [keys %$peek_our],
70             );
71              
72             # we want all symbols to be in the scope of the caller, so switch to the caller's namespace
73 6         23 $content = "package $caller_package;".$content;
74 6     1   664 my $return_val = eval $content;
  1         867  
75 6 50       29 $@ and die $@;
76 6         29 return $return_val;
77             }
78              
79             sub _delete {
80 1     1   489 my $snippet_id = shift;
81 1         9 my ($snippet_filename) = (caller(0));
82 1 50       8 $snippet_id
83             and $snippet_filename .= "-$snippet_id";
84              
85 1         5 my $snippet_path = _snippet_dir().'/'.$snippet_filename;
86 1 50       53 -f $snippet_path
87             or die "Can't delete snippet, it doesn't exist: ".$snippet_path;
88 1 50       86 unlink($snippet_path)
89             or die "Can't delete snippet: $!";
90             }
91              
92             sub _process {
93 12     12   52 my %arg = @_;
94 12         27 my $content = $arg{content};
95 12         16 my $type = $arg{type};
96 12         12 my @vars = @{$arg{vars}};
  12         25  
97              
98 12         43 foreach my $var (@vars) {
99             # array
100 30 100       153 if ($var =~ /^\@(.*)/) {
    100          
    50          
101 7         19 my $dollar_var = '$'.$1;
102              
103             # array elements - $x[0]
104 7         54 $content =~ s/
105             (?\{')
106             \Q$dollar_var\E\b
107             \[
108             /\${\$peek_${type}->{'$var'}}[/gsx;
109              
110             # array @x
111 7         53 $content =~ s/
112             (?\{')
113             \Q$var\E\b
114             /\@{\$peek_${type}->{'$var'}}/gsx;
115             }
116             # hash
117             elsif ($var =~ /^\%(.*)/) {
118 7         21 my $dollar_var = '$'.$1;
119              
120             # hash element $x{key}
121 7         59 $content =~ s/
122             (?\{')
123             \Q$dollar_var\E\b
124             \{
125             /\${\$peek_${type}->{'$var'}}\{/gsx;
126              
127             # hash %x
128 7         67 $content =~ s/
129             (?\{')
130             \Q$var\E\b
131             /\%{\$peek_${type}->{'$var'}}/gsx;
132             }
133             # scalar / ref
134             elsif ($var =~ /^\$/) {
135              
136 16         367 $content =~ s/
137             (?\{')
138             \Q$var\E\b
139             (?![\[\{])
140             /\${\$peek_${type}->{'$var'}}/gsx;
141             }
142             else {
143 0         0 warn "no idea how to deal with sigil for $var";
144             }
145             }
146 12         79 return $content;
147             }
148              
149             sub _snippet_dir {
150 8   33 8   5067 return $ENV{SNIPPET_DIR} || $ENV{HOME}.'/eval-snippets';
151             }
152              
153             1;
154              
155             =head1 NAME
156              
157             Util::EvalSnippet - eval snippets of code in the context of a caller marker
158              
159             =head1 SYNOPSIS
160              
161             Use snippet files to make instant changes to apps that normally require a
162             restart. Example usage:
163              
164             use Util::EvalSnippet;
165              
166             sub some_method {
167             eval_snippet();
168             }
169              
170             =head1 DESCRIPTION
171              
172             When developing in many frameworks (Catalyst, mod_perl etc), every save involves
173             an app reload that can take from a few seconds to over a minute on your dev
174             server. This module helps you minimize the inconvenience by allowing you to
175             develop code in snippets, saving as you go, and then merge your changes back
176             into your application's module when you're done.
177              
178             =head1 EXPORTS
179              
180             Default: L
181              
182             =head1 FUNCTIONS
183              
184             =head2 eval_snippet([SNIPPET_NAME])
185              
186             Place the function in your module, in a method that the app is not caching, and
187             reload the app that will call that code.
188              
189             A snippet will automatically appear in the ~/eval-snippets directory.
190              
191             You can change the eval snippets directory by setting the SNIPPET_DIR environment
192             variable, if you prefer.
193              
194             If you need more than one snippet in a module, name them:
195              
196             package Some::Module;
197             use Util::EvalSnippet;
198              
199             sub some_method {
200             eval_snippet('one');
201             eval_snippet('two');
202             }
203              
204             When you run the code, the module creates the following snippet files in your
205             snippets directory:
206              
207             Some::Module-one
208             Some::Module-two
209              
210             Snippets are created with header comments. Do not delete them.
211              
212             Make changes and save in your snippets directory, then reload your view to
213             see the updated code in action without having to wait for an app restart.
214              
215             When you're finished with development, merge the module's snippets back
216             into the module by running, in a shell:
217              
218             perl-eval-snippet --merge Some::Module
219              
220             That will merge the snippets into the module, and remove the
221             "use Util::EvalSnippet;" statement.
222              
223             After merging, it's probably a good idea for you to examine the code in situ
224             to confirm the spacing looks good and, of course, to confirm all is well.
225              
226             If you're only done with one snippet, you can merge it in on it's own using:
227              
228             perl-eval-snippet --merge Some::Module-one
229              
230             The "use Util::EvalSnippet;" statement is only removed if there are no more
231             eval_snippet() calls left in the code.
232              
233             Note: If you're working on multiple instances of a module namespace (different
234             branches etc), either ensure you neame snippets uniquely, or ensure the
235             SNIPPET_DIR environment variable is set differently for each. This tool is
236             not designed (yet :D) to work across multiple instances of the same file.
237              
238             =head1 ENVIRONMENT
239              
240             =item SNIPPET_DIR
241              
242             By default, snippets are created in the ~/eval-snippets directory. If you
243             want to change that, set the environment var SNIPPET_DIR to point to where
244             you would like snippets saved.
245              
246             =item ALLOW_SNIPPETS
247              
248             If you want to, you can add a sanity check so that snippet code won't run
249             outside of your development environment. If you set the ALLOW_SNIPPETS
250             env var to a true value and use the module like this:
251              
252             use Util::EvalSnippet 'safe';
253              
254             it will die unless the env var is set.
255              
256             That way, the paranoid amongst you can be sure that the snippets are never run
257             outside of your dev environment - since the code involves blind eval of a
258             text file, this may or may not be a security concern for you.
259              
260             If you end up using the module a lot, adding a git hook to reject commits
261             containing Util::EvalSnippet code may also be useful.
262              
263             =head1 CAVEATS
264              
265             As, basically, a templating solution, there are some things that are not easily
266             dealt with.
267              
268             For example, this module does not do anything clever with string interpolation,
269             so some things will not work. The main inconvenience is embedded literal
270             variables in strings. Eg, say you have a view that returns content to the
271             browser:
272              
273             sub some_view {
274             my $self = shift;
275             eval_snippet();
276             }
277              
278             and you have this in a snippet:
279              
280             return '

The variable is called $x

';
281              
282             it will not do what you think it should.
283              
284             This should not be issues in most environments as you should be using templates
285             for your views.
286              
287             Another place where this may not work is for dynamically created variables.
288              
289             Both of these issues involve bad design patterns anyway, so they won't affect
290             you, right? :D
291              
292             =head1 COPYRIGHT AND LICENSE
293              
294             Copyright 2018 Clive Holloway
295              
296             Licensed for distribution under the GNU GENERAL PUBLIC LICENSE