File Coverage

blib/lib/Plack/Middleware/ReviseEnv.pm
Criterion Covered Total %
statement 128 136 94.1
branch 66 82 80.4
condition 24 32 75.0
subroutine 14 15 93.3
pod 8 8 100.0
total 240 273 87.9


line stmt bran cond sub pod time code
1             package Plack::Middleware::ReviseEnv;
2              
3 7     7   5404 use strict;
  7         9  
  7         192  
4 7     7   25 use warnings;
  7         7  
  7         201  
5 7     7   32 use Carp qw< confess >;
  7         7  
  7         440  
6 7     7   3265 use English qw< -no_match_vars >;
  7         5144  
  7         30  
7             { our $VERSION = '0.004'; }
8              
9 7     7   2476 use parent 'Plack::Middleware';
  7         241  
  7         45  
10              
11             sub call {
12 8     8 1 98503 my ($self, $env) = @_;
13 8         37 my %vars = (env => $env, ENV => \%ENV);
14             REVISOR:
15 8 50       9 for my $revisor (@{$self->{revisors} || []}) {
  8         70  
16             my ($key, $value) = map {
17 103         85 my $retval = $revisor->{$_};
  206         187  
18              
19             # if array reference, there's more work to do
20 206 100       257 if (ref $retval) {
21 191         119 my $all_defs = 1;
22 311 100       451 my @parts = grep { defined($_) ? 1 : ($all_defs = 0) } map {
23 191         161 (!ref($_)) ? $_
24             : exists($vars{$_->{src}}{$_->{key}})
25             ? $vars{$_->{src}}{$_->{key}}
26 311 100       574 : undef;
    100          
27             } @$retval;
28              
29 191 100 100     516 $retval = ($revisor->{require_all} && (!$all_defs))
30             ? undef
31             : join '', @parts;
32             } ## end if (defined $retval)
33              
34             # last chance to have a say on $retval...
35             $retval = $revisor->{'default_' . $_}
36             if (!defined($retval))
37 206 100 66     417 || ((length($retval) == 0) && $revisor->{empty_as_default});
      66        
38              
39             # save for next iteration, if so requested
40 206 100       297 $revisor->{$_} = $retval if $revisor->{cache};
41              
42 206         302 $retval;
43             } qw< key value >;
44              
45 103 50       147 next unless defined $key;
46              
47             $env->{$key} = $value
48 103 100 100     245 if $revisor->{override} || (!exists($env->{$key}));
49 103 100       173 delete $env->{$key} unless defined $value;
50             } ## end REVISOR: for my $revisor (@{$self...})
51              
52 8         69 return $self->app()->($env);
53             } ## end sub call
54              
55             # Initialization code, this is executed once at application startup
56             # so we are more relaxed about *not* calling too many subs
57             sub prepare_app {
58 6     6 1 543 my ($self) = @_;
59 6         13 $self->normalize_input_structure(); # reorganize internally
60 6         6 my @inputs = @{delete $self->{revisors}}; # we will consume @inputs
  6         38  
61 6         22 my @revisors;
62              
63 6         17 while (@inputs) {
64 93         80 my $spec = shift @inputs;
65              
66             # allow for key => value or \%spec
67 93 100       127 if (!ref($spec)) {
68 60 50       84 confess "stray revisor '$spec'" unless @inputs;
69 60         69 (my $key, $spec) = ($spec, shift @inputs);
70 60 100       112 $spec = {value => $spec} unless ref($spec) eq 'HASH';
71              
72             # override key only if not already present. The external key
73             # can then be used for ordering revisors also in the hash
74             # scenario
75 60 100       105 $spec->{key} = $key unless defined $spec->{key};
76             } ## end if (!ref($spec))
77              
78 93         112 push @revisors, $self->generate_revisor($spec);
79             } ## end while (@inputs)
80              
81             # if we arrived here, it's safe
82 6         17 $self->{revisors} = \@revisors;
83              
84 6         21 return $self;
85             } ## end sub prepare_app
86              
87             sub generate_revisor {
88 93     93 1 81 my ($self, $spec) = @_;
89 93 50       122 confess "one spec has no (defined) key" unless defined $spec->{key};
90              
91 93         94 my $opts = $self->{opts};
92 93 100       118 my $start = defined($spec->{start}) ? $spec->{start} : $opts->{start};
93 93 50       110 confess "start sequence cannot be empty" unless length $start;
94              
95 93 100       109 my $stop = defined($spec->{stop}) ? $spec->{stop} : $opts->{stop};
96 93 50       115 confess "stop sequence cannot be empty" unless length $stop;
97              
98 93 100       104 my $esc = defined($spec->{esc}) ? $spec->{esc} : $opts->{esc};
99 93 50       117 confess "escape sequence cannot be empty" unless length $esc;
100 93 50       136 confess "escape sequence cannot start with a space, sorry"
101             if substr($esc, 0, 1) eq ' ';
102 93 50 33     280 confess "escape sequence cannot be equal to start or stop sequence"
103             if ($esc eq $start) || ($esc eq $stop);
104              
105 93         287 my %m = %$spec;
106 93 100       170 $m{override} = 1 unless exists $m{override};
107 93         114 $m{key} = $self->parse_template($m{key}, $start, $stop, $esc);
108 93         113 $m{value} = $self->parse_template($m{value}, $start, $stop, $esc);
109 93 100       182 $m{cache} = $opts->{cache} unless exists $m{cache};
110              
111 93         225 return \%m;
112             } ## end sub generate_revisor
113              
114             sub parse_template {
115 186     186 1 184 my ($self, $template, $start, $stop, $esc) = @_;
116 186 100       252 return undef unless defined $template;
117 177         130 my $pos = 0;
118 177         99 my $len = length $template;
119 177         115 my @chunks;
120             CHUNK:
121 177         250 while ($pos < $len) {
122              
123             # find start, if any
124 197         197 my $i = $self->escaped_index($template, $start, $esc, $pos);
125 197 100       313 my $text = substr $template, $pos, ($i < 0 ? $len : $i) - $pos;
126 197         216 push @chunks, $self->unescape($text, $esc);
127 197 100       303 last CHUNK if $i < 0; # nothing more left to search
128              
129             # advance position marker immediately after start sequence
130 93         71 $pos = $i + length $start;
131              
132             # start sequence found, let's look for the stop
133 93         99 $i = $self->escaped_index($template, $stop, $esc, $pos);
134 93 50       123 confess "unclosed start sequence in '$template'" if $i < 0;
135              
136 93         102 my $chunk = substr $template, $pos, $i - $pos;
137              
138             # trim intelligently, then unescape
139 93         104 $chunk = $self->unescape($self->escaped_trim($chunk, $esc), $esc);
140              
141 93         148 my ($src, $key) = split /:/, $chunk, 2;
142 93 50 66     250 confess "invalid source '$src' in chunk '$chunk'"
143             if ($src ne 'env') && ($src ne 'ENV');
144 93 50       122 confess "no key in chunk '$chunk'" unless defined $key;
145 93         174 push @chunks, {src => $src, key => $key};
146              
147             # advance position marker for next iteration
148 93         153 $pos = $i + length $stop;
149              
150             } ## end CHUNK: while ($pos < $len)
151              
152 177         242 return \@chunks;
153             } ## end sub parse_template
154              
155             sub unescape {
156 290     290 1 243 my ($self, $str, $esc) = @_;
157 290         556 $str =~ s{\Q$esc\E(.)}{$1}gmxs;
158 290         370 return $str;
159             }
160              
161             sub escaped_trim {
162 93     93 1 85 my ($self, $str, $esc) = @_;
163 93         206 $str =~ s{\A\s+}{}mxs; # trimming the initial part is easy
164              
165 93         70 my $pos = 0;
166 93         71 while ('necessary') {
167              
168             # find next un-escaped space
169 94         97 my $i = $self->escaped_index($str, ' ', $esc, $pos);
170 94 100       124 last if $i < 0; # no further spaces... nothing to trim
171              
172             # now look for escapes after that, because we're interested only
173             # in un-escaped spaces at the end of $str
174 84         65 my $e = index $str, $esc, $i + 1;
175              
176 84 100       109 if ($e < 0) { # no escapes past last space found
177              
178             # Now we split our string at $i, which represents the first
179             # space character that is not escaped and has no escapes after it.
180             # The string before it MUST NOT be subject to trimming, the part
181             # from $i on is safe to trim.
182 83         100 my $keep = substr $str, 0, $i, '';
183 83         155 $str =~ s{\s+\z}{}mxs;
184              
185             # merge the two parts back and we're good to go
186 83         159 return $keep . $str;
187             } ## end if ($e < 0)
188              
189             # we found an escape sequence after the last space we found, we have
190             # to look further past this escape sequence and the char it escapes
191 1         2 $pos = $e + length($esc) + 1;
192             } ## end while ('necessary')
193              
194             # no trailing spaces to be trimmed found, $str is fine
195 10         26 return $str;
196             } ## end sub escaped_trim
197              
198             sub escaped_index {
199 384     384 1 314 my ($self, $str, $delimiter, $escaper, $pos) = @_;
200              
201 384         225 my $len = length $str;
202 384         448 while ($pos < $len) {
203 396         338 my $dpos = index $str, $delimiter, $pos; # next delimiter
204 396         275 my $epos = index $str, $escaper, $pos; # next escaper
205 396 100 100     1136 return $dpos
      100        
206             if ($dpos < 0) # didn't find it
207             || ($epos < 0) # nothing escaped at all
208             || ($dpos < $epos); # nothing escaped before it
209              
210             # there's an escaper occurrence *before* a delimiter, so we have
211             # to honor the escaping and restart the quest past the escaped char
212 14         22 $pos = $epos + length($escaper) + 1;
213              
214             } ## end while ($pos < $len)
215              
216 2 50       6 return -1 if $pos == $len;
217              
218             # we got past the end of the string, there's an escaper at the end
219 0         0 confess "stray escaping in '$str'";
220             } ## end sub escaped_index
221              
222             sub normalize_input_structure {
223 6     6 1 7 my ($self) = @_;
224              
225 6         189 my $app = delete $self->{app}; # temporarily remove these keys
226 6   100     37 my $opts = delete($self->{opts}) || {};
227 6   50     27 $opts->{start} ||= '[%';
228 6   50     32 $opts->{stop} ||= '%]';
229 6   50     21 $opts->{esc} ||= '\\';
230 6 100       16 $opts->{cache} = 1 unless exists $opts->{cache};
231              
232             my $revisors = exists($self->{revisors})
233             ? delete($self->{revisors}) # just take it
234 6 100       18 : __exhaust_hash($self); # or move stuff out of $self
235              
236             # Fun fact: __exhaust_hash($self) could have been written as:
237             #
238             # { (@{[]}, %$self) = %$self }
239             #
240             # but let's avoid being too "clever" for readability's sake...
241              
242 6 50       18 if (scalar keys %$self > 0) {
243 0         0 my @keys = __stringified_list(keys %$self);
244 0         0 confess "stray keys found: @keys";
245             }
246              
247 6 100       38 $revisors = [map { $_ => $revisors->{$_} } sort keys %$revisors]
  40         47  
248             if ref($revisors) eq 'HASH';
249              
250 6         19 %$self = (
251             app => $app,
252             revisors => $revisors,
253             opts => $opts,
254             );
255 6         9 return $self;
256             } ## end sub normalize_input_structure
257              
258             # _PRIVATE_ convenience functions
259              
260             sub __stringified_list {
261             return map {
262 0 0   0   0 if (defined(my $v = $_)) {
  0         0  
263 0         0 $v =~ s{([\\'])}{\\$1}gmxs;
264 0         0 "'$v'";
265             }
266             else {
267 0         0 'undef';
268             }
269             } @_;
270             } ## end sub __stringified_list
271              
272             sub __exhaust_hash {
273 1     1   1 my ($target) = @_;
274 1         7 my $retval = {%$target};
275 1         4 %$target = ();
276 1         1 return $retval;
277             } ## end sub __exhaust_hash
278              
279             1;