File Coverage

blib/lib/Plack/Middleware/ReviseEnv.pm
Criterion Covered Total %
statement 128 136 94.1
branch 60 76 78.9
condition 23 32 71.8
subroutine 14 15 93.3
pod 8 8 100.0
total 233 267 87.2


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