File Coverage

lib/Acme/Sub/Parms.pm
Criterion Covered Total %
statement 207 228 90.7
branch 73 96 76.0
condition 23 42 54.7
subroutine 11 11 100.0
pod 0 2 0.0
total 314 379 82.8


line stmt bran cond sub pod time code
1             package Acme::Sub::Parms;
2              
3 4     4   11894 use strict;
  4         8  
  4         167  
4 4     4   4556 use Filter::Util::Call;
  4         5985  
  4         624  
5              
6             BEGIN {
7 4     4   10 $Acme::Sub::Parms::VERSION = '1.02';
8 4         26 %Acme::Sub::Parms::args = ();
9 4         10 %Acme::Sub::Parms::raw_args = ();
10 4         21499 $Acme::Sub::Parms::line_counter = 0;
11             }
12              
13 445     445   801 sub _NORMALIZE () { return ':normalize'; };
14 473     473   904 sub _NO_VALIDATION () { return ':no_validation'; };
15 445     445   1031 sub _DUMP () { return ':dump_to_stdout'; };
16             sub _DEBUG () { 0; };
17              
18             sub _legal_option {
19             return {
20 5     5   10 _NORMALIZE() => 1,
21             _NO_VALIDATION() => 1,
22             _DUMP() => 1,
23             }->{$_[0]};
24             }
25              
26             ####
27              
28             sub import {
29 4     4   67 local $^W = 1; # We _like_ warnings
30 4         13 my $class = shift;
31 4         21 my $options = {
32             _NORMALIZE() => 0,
33             _NO_VALIDATION() => 0,
34             _DUMP() => 0,
35             };
36 4         21 foreach my $item (@_) {
37 5 50       15 if (not _legal_option($item)) {
38 0         0 my $package = __PACKAGE__;
39 0         0 require Carp;
40 0         0 Carp::croak("'$item' not a valid option for 'use $package'\n");
41             }
42 5         25 $options->{$item} = 1;
43             }
44 4         9 $Acme::Sub::Parms::line_counter = 0;
45 4         19 my $ref = {'options' => $options, 'bind_block' => 0 };
46 4         26 filter_add(bless $ref); # imported from Filter::Util::Call
47             }
48              
49             ####
50              
51             sub _parse_bind_spec {
52 28     28   39 my ($self, $raw_spec) = @_;
53              
54 28         40 my $spec = $raw_spec;
55              
56 28         83 my $spec_tokens = {
57             'is_defined' => 0,
58             'required' => 1,
59             'optional' => 0,
60             };
61 28         77 while ($spec ne '') {
62 56 100       5653 if ($spec =~ s/^required(\s*,\s*|$)//) { # 'required' flag
    100          
    100          
    50          
63 8         15 $spec_tokens->{'required'} = 1;
64 8         25 $spec_tokens->{'optional'} = 0;
65              
66             } elsif ($spec =~ s/^optional(\s*,\s*|$)//) { # 'optional' flag
67 16         24 $spec_tokens->{'required'} = 0;
68 16         42 $spec_tokens->{'optional'} = 1;
69              
70             } elsif ($spec =~ s/^is_defined(\s*,\s*|$)//) { # 'is_defined' flag
71 8         25 $spec_tokens->{'is_defined'} = 1;
72              
73             } elsif ($spec =~ s/^(can|isa|type|callback|default)\s*=\s*//) { # 'something="somevalue"'
74 24         45 my $spec_key = $1;
75              
76             # Simple unquoted text with no embedded ws
77 24 100       313 if ($spec =~ s/^([^\s"',]+)(\s*,\s*|$)//) {
    50          
    50          
    50          
78 20         101 $spec_tokens->{$spec_key} = $1;
79              
80             # Single quoted text with no embedded quotes
81             } elsif ($spec =~ s/^'([^'\/]+)'\s*,\s*//) {
82 0         0 $spec_tokens->{$spec_key} = "'$1'";
83              
84             # Double quoted text with no embedded quotes or escapes
85             } elsif ($spec =~ s/^"([^"\/]+)"\s*,\s*//) {
86 0         0 $spec_tokens->{$spec_key} = '"' . $1 . '"';
87              
88             # It is a tricky case with quoted characters. One character at a time it is.
89             } elsif ($spec =~ s/^(['"])//) {
90 4         11 my $quote = $1;
91 4         18 my $upend_spec = reverse $spec;
92 4         18 my $block_done = 0;
93 4         7 my $escape_next = 0;
94 4         8 my $token = $quote;
95 4   66     82 until ($block_done || ($upend_spec eq '')) {
96 32         52 my $ch = chop $upend_spec;
97 32 50 33     119 if ($escape_next) {
    50          
    100          
98 0         0 $token .= $ch;
99 0         0 $escape_next = 0;
100              
101             } elsif (($ch eq "\\") && (not $escape_next)) {
102 0         0 $token .= $ch;
103 0         0 $escape_next = 1;
104              
105             } elsif ($ch eq $quote) {
106 4         18 $block_done = 1;
107              
108             } else {
109 28         111 $token .= $ch;
110             }
111             }
112 4 50       15 if ($escape_next) {
113 0         0 die("Syntax error in BindParms spec: $raw_spec\n");
114             }
115 4         10 $spec = reverse $upend_spec;
116 4         23 $spec_tokens->{$spec_key} = $token . $quote;
117              
118             } else {
119 0         0 die("Syntax error in BindParms spec: $raw_spec\n");
120             }
121             } else {
122 0         0 die("Syntax error in BindParms spec: $raw_spec\n");
123             }
124             }
125 28         58 return $spec_tokens;
126             }
127              
128             ###############################################################################
129             # bind_spec is intentionally a a non-POD documented'public' method. It can be overridden in a sub-class
130             # to provide alternative features.
131             #
132             # It takes two parameters:
133             #
134             # $raw_spec - this is the content of the [....] block (not including the '[' and ']' block delimitters)
135             # $field_name - the hash key for the field being processed
136             #
137             # As each line of the BindParms block is processed the two parameters for each line are passed to the bind_spec
138             # method for evaluation. bind_spec should return a string containing any Perl code generated as a result of
139             # the bind specification.
140             #
141             # Good style dictates that the returned output should be *ONE* line (it could be a very *long* line)
142             # so that line numbering in the source file is preserved for any error messages.
143             #
144             sub bind_spec {
145 28     28 0 150 my $self = shift;
146 28         46 my ($raw_spec, $field_name) = @_;
147              
148 28         80 my $options = $self->{'options'};
149 28         53 my $no_validation = $options->{_NO_VALIDATION()};
150              
151 28         91 my $spec_tokens = $self->_parse_bind_spec($raw_spec);
152              
153 28         55 my $has_side_effects = 0;
154 28         35 my $output = '';
155              
156 28         305 my @spec_tokens_list = keys %$spec_tokens;
157 28 50 33     146 if ((0 == @spec_tokens_list) || ((1 == @spec_tokens_list) && ($spec_tokens->{'optional'}))) {
      33        
158 0         0 return;
159             }
160              
161             ######################
162             # default="some value"
163 28 100       73 if (defined $spec_tokens->{'default'}) {
164 4 50       26 if ($spec_tokens->{'optional'}) {
165 4         25 $output .= "unless (exists (\$Acme::Sub::Parms::args\{'$field_name'\})) \{ \$Acme::Sub::Parms::args\{'$field_name'\} = " . $spec_tokens->{'default'} . ";\} ";
166             } else { # required
167 0         0 $output .= "unless (defined (\$Acme::Sub::Parms::args\{'$field_name'\})) \{ \$Acme::Sub::Parms::args\{'$field_name'\} = " . $spec_tokens->{'default'} . ";\} ";
168             }
169 4         7 $has_side_effects = 1;
170             }
171              
172             ######################
173             # callback="some_subroutine"
174 28 100       63 if ($spec_tokens->{'callback'}) {
175 8         60 $output .= "\{ my (\$callback_is_valid, \$callback_error) = "
176             . $spec_tokens->{'callback'}
177             . "(\'$field_name\', \$Acme::Sub::Parms::args\{\'$field_name\'\}, \\\%Acme::Sub::Parms::args);"
178             . "unless (\$callback_is_valid) { require Carp; Carp::croak(\"$field_name error: \$callback_error\"); }} ";
179 8         13 $has_side_effects = 1;
180             }
181              
182             ######################
183             # required
184 28 100 100     98 if ((! $no_validation) && $spec_tokens->{'required'}) {
185 4         25 $output .= "unless (exists (\$Acme::Sub::Parms::args\{\'$field_name\'\})) { require Carp; Carp::croak(\"Missing required parameter \'$field_name\'\"); } ";
186             }
187              
188             ######################
189             # is_defined
190 28 100       324 if ($spec_tokens->{'is_defined'}) {
191 8         32 $output .= "if (exists (\$Acme::Sub::Parms::args\{\'$field_name\'\}) and (! defined (\$Acme::Sub::Parms::args\{\'$field_name\'\}))) { require Carp; Carp::croak(\"parameter \'$field_name\' cannot be undef\"); } ";
192             }
193              
194 28         45 my $type_requirements = $spec_tokens->{'type'};
195 28         41 my $isa_requirements = $spec_tokens->{'isa'};
196 28         38 my $can_requirements = $spec_tokens->{'can'};
197              
198 28 100 100     188 if (defined ($type_requirements ) || defined($isa_requirements) || defined($can_requirements)) {
      100        
199 12         39 $output .= "if (exists (\$Acme::Sub::Parms::args\{\'$field_name\'\})) \{";
200              
201             #####################
202             # type="SomeRefType" or type="SomeRefType, SomeOtherRefType, ..."
203 12 100       36 if (defined $type_requirements) {
204 4         12 $type_requirements =~ s/^['"]//;
205 4         22 $type_requirements =~ s/['"]$//;
206 4         15 my @type_classes = split(/[,\s]+/, $type_requirements);
207 4         10 $output .= "unless (";
208 4         7 my @type_tests = ();
209 4         9 foreach my $class_name (@type_classes) {
210 4         30 push (@type_tests, "ref(\$Acme::Sub::Parms::args\{'$field_name'\}) eq '$class_name')");
211             }
212 4         24 $output .= join(' || ',@type_tests) . " \{ require Carp; Carp::croak(\'parameter \\\'$field_name\\\' must be a " . join(' or ',@type_classes) . "\'); \}";
213             }
214              
215             #####################
216             # isa="SomeRefType" or isa="SomeRefType, SomeOtherRefType, ..."
217 12 100       31 if (defined $isa_requirements) {
218 4         19 $isa_requirements =~ s/^['"]//;
219 4         10 $isa_requirements =~ s/['"]$//;
220 4         18 my @isa_classes = split(/[,\s]+/, $isa_requirements);
221 4         11 $output .= "unless (";
222 4         13 my @isa_tests = ();
223 4         18 foreach my $class_name (@isa_classes) {
224 4         18 push (@isa_tests, "\$Acme::Sub::Parms::args\{'$field_name'\}->isa('$class_name')");
225             }
226 4         30 $output .= join(' || ',@isa_tests) . ") \{ require Carp; Carp::croak(\'parameter \\\'$field_name\\\' must be a " . join(' or ',@isa_classes) . " instance or subclass\'); \}";
227             }
228              
229             #####################
230             # can="somemethod" or can="somemethod, someothermethod, ..."
231 12 100       28 if (defined $can_requirements) {
232 4         15 $can_requirements =~ s/^['"]//;
233 4         12 $can_requirements =~ s/['"]$//;
234 4         24 my @can_methods = split(/[,\s]+/, $can_requirements);
235 4         10 $output .= "unless (";
236 4         10 my @can_tests = ();
237 4         18 foreach my $method_name (@can_methods) {
238 4         19 push (@can_tests, "\$Acme::Sub::Parms::args\{'$field_name'\}->can('$method_name')");
239             }
240 4         49 $output .= join(' && ',@can_tests) . ") \{ require Carp; Carp::croak(\'parameter \\\'$field_name\\\' must be an object with a " . join(' and a ',@can_methods) . " method\'); \}";
241             }
242              
243 12         21 $output .= "\}";
244             }
245              
246 28         142 return ($has_side_effects,$output);
247             }
248              
249             ####
250              
251             sub filter {
252 436     436 0 1436 local $^W = 1; # We _like_ warnings
253 436         556 my $self = shift;
254              
255 436         748 my $options = $self->{'options'};
256 436         967 my $dump_to_stdout = $options->{_DUMP()};
257 436         774 my $normalize = $options->{_NORMALIZE()};
258 436         715 my $no_validation = $options->{_NO_VALIDATION()};
259 436         669 my $bind_block = $self->{'bind_block'};
260              
261 436         467 my $status;
262              
263 436 100       2992 if ($status = filter_read() > 0) { # imported from Filter::Util::Call
264 432         484 $Acme::Sub::Parms::line_counter++;
265            
266 432         387 if (_DEBUG) {
267             print STDERR "input line $Acme::Sub::Parms::line_counter: $_";
268             }
269            
270             #############################################
271             # If we are in a bind block, handle it
272 432 100       687 if ($bind_block) {
273 36         56 my $bind_entries = $self->{'bind_entries'};
274 36         50 my $simple_bind = $self->{'simple_bind'};
275              
276             ##############################
277             # Last line of the bind block? Generate the working code.
278 36 100       470 if (m/^\s*\)(\s*$|\s*#.*$)/) {
    100          
    50          
279            
280 4         12 my $block_trailing_comment = $2;
281 4 50       25 $block_trailing_comment = defined($block_trailing_comment) ? $block_trailing_comment : '';
282 4         14 $block_trailing_comment =~ s/[\r\n]+$//s;
283 4         9 my $side_effects = 0;
284 4         9 my $args = 'local %Acme::Sub::Parms::args; '; # needed?
285 4 100       13 if ($normalize) {
286 2         7 $args .= '{ local $_; local %Acme::Sub::Parms::raw_args = @_; %Acme::Sub::Parms::args = map { lc($_) => $Acme::Sub::Parms::raw_args{$_} } keys %Acme::Sub::Parms::raw_args; }' . "\n";
287             } else {
288 2         5 $args .= '%Acme::Sub::Parms::args = @_;' . "\n";
289             }
290             # If we have validation or defaults, handle them
291 4         9 my $padding_lines = 0;
292 4 50       14 if (! $simple_bind) {
293 4         8 my @parm_declarations = ();
294 4         12 foreach my $entry (@$bind_entries) {
295 32         70 my $variable_decl = $entry->{'variable'};
296 32         43 my $field_name = $entry->{'field'};
297 32         191 my $spec = $entry->{'spec'};
298 32         141 my $trailing_comment = $entry->{'trailing_comment'};
299 32 100 66     190 if ( (! defined($spec)) || ($spec eq '')) {
300             # push(@parm_declarations, $trailing_comment);
301 4         13 next;
302             }
303             # The hard case. We have validation requirements.
304 28         88 my ($has_side_effects, $bind_spec_output) = $self->bind_spec($spec, $field_name);
305 28         56 $side_effects += $has_side_effects;
306 28         82 push (@parm_declarations, "$bind_spec_output$trailing_comment");
307             }
308 4         55 $args .= join("\n",@parm_declarations,'');
309             }
310              
311             # Generate the actual parameter data binding
312 4         10 my @var_declarations = ();
313 4         10 my @hard_var_declarations = ();
314 4         8 my @field_declarations = ();
315 4         9 my @fields_list = ();
316 4         10 foreach my $entry (@$bind_entries) {
317 32         61 my $spec = $entry->{'spec'};
318 32 100 66     159 next if ((not defined $spec) || ($spec eq ''));
319 28         46 my $raw_var = $entry->{'variable'};
320 28         37 my $field_name = $entry->{'field'};
321            
322 28         56 push (@fields_list, "'$field_name'");
323 28         105 my ($variable_name) = $raw_var =~ m/^my\s+(\S+)$/;
324 28 50       52 if (defined $variable_name) { # simple 'my $variable :' entries are special-cased for performance
325 28         62 push (@var_declarations, $variable_name);
326 28         83 push (@field_declarations, "'$field_name'");
327              
328             } else { # Otherwise make a seperate entry for this binding
329 0         0 push (@hard_var_declarations, "$raw_var = \$Acme::Sub::Parms::args\{$field_name\};");
330             }
331             }
332 4         12 my $hard_args = join(' ',@hard_var_declarations);
333 4         9 my $arg_line = '';
334 4 50       14 if (0 < @var_declarations) {
335            
336 4 50 33     34 if ($simple_bind && (! $normalize) && $no_validation && (0 == $side_effects) && (0 == @hard_var_declarations)) {
      33        
      0        
      0        
337 0         0 $args = "\n my (" . join(",", @var_declarations) . ') = @{{@_}}{' . join(',',@field_declarations) . '}; ';
338              
339             } else {
340            
341 4         30 $arg_line = 'my (' . join(",", @var_declarations) . ') = @Acme::Sub::Parms::args{' . join(',',@field_declarations) . '}; ';
342             }
343             }
344 4         10 my $unknown_parms_check = '';
345 4 100       14 unless ($no_validation) {
346 2         9 $unknown_parms_check = 'delete @Acme::Sub::Parms::args{' . join(',',@fields_list) . '}; if (0 < @Acme::Sub::Parms::args) { require Carp; Carp::croak(\'Unexpected parameters passed: \' . join(\', \',@Acme::Sub::Parms::args)); } ';
347              
348             }
349 4         13 $self->{'bind_block'} = 0;
350 4         11 my $original_block_length = $Acme::Sub::Parms::line_counter - $self->{'line_block_start'};
351 4         53 my $new_block = $args . join(' ',$arg_line, $hard_args, $unknown_parms_check) . "$block_trailing_comment\n";
352 4         81 $new_block =~ s/\n+/\n/gs;
353 4         20 my $new_block_lines = $new_block =~ m/\n/gs;
354            
355 4         8 my $additional_lines = $original_block_length - $new_block_lines;
356             #warn("Need $additional_lines extra lines\n---\n$new_block---\n");
357 4 50       14 if ($additional_lines > 0) {
358 4         67 $_ = $new_block . ("\n" x $additional_lines);
359             } else {
360 0         0 $_ = $new_block;
361             }
362              
363             ########################
364             # Bind block parameter line
365             } elsif (my($bind_var, $bind_field,$trailing_comment) = m/^\s*(\S.*?)\s+:\s+([^'"\s\[]+.*?)\s*(;\s*|;\s*#.*)$/) {
366 28 50       71 $trailing_comment = defined($trailing_comment) ? $trailing_comment : '';
367 28         16357 $trailing_comment =~ s/[\r\n]+$//s;
368 28         77 $trailing_comment =~ s/^;//;
369 28         984 my $bind_entry = { 'variable' => $bind_var, 'field' => $bind_field, trailing_comment => $trailing_comment };
370 28         60 push (@$bind_entries, $bind_entry);
371 28 50       235 if ($bind_var !~ m/^my \$\S+$/) {
372 0         0 $self->{'simple_bind'} = 0;
373             }
374 28 100       128 if ($bind_field =~ m/^(\S+)\s*\[(.*)\]$/) { # Complex spec
    50          
375 26         75 $bind_entry->{'field'} = $1;
376 26         77 $bind_entry->{'spec'} = $2;
377 26 100 100     140 unless ($no_validation && ($bind_field !~ m/[\s\[,](default|callback)\s*=\s*/)) {
378 18         39 $self->{'simple_bind'} = 0;
379             }
380             } elsif ($bind_field =~ m/^\w+$/) { # my $thing : something;
381 2         5 $bind_entry->{'spec'} = 'required';
382 2 50       9 unless ($no_validation) {
383 0         0 $self->{'simple_bind'} = 0;
384             }
385             } else {
386 0         0 die("Failed to parse BindParms block line $Acme::Sub::Parms::line_counter: $_");
387             }
388 28         44 undef $trailing_comment;
389 28         45 undef $bind_var;
390 28         33 undef $bind_field;
391 28         56 $_ = '';
392              
393             ############################
394             # Blank and comment only lines
395             } elsif (m/^(\s*|\s*#.*)$/) {
396 4         12 my $trailing_comment = $1;
397 4 50       21 $trailing_comment = defined ($trailing_comment) ? $trailing_comment : '';
398 4         29 $trailing_comment =~ s/[\r\n]+$//s;
399            
400 4         17 my $bind_entry = { spec => '', trailing_comment => $trailing_comment};
401 4         13 push (@$bind_entries, $bind_entry);
402 4         11 $_ = '';
403            
404             } else {
405 0         0 die("Failed to parse BindParms block line $Acme::Sub::Parms::line_counter: $_");
406             }
407              
408             } else { # Start of a bind block
409 396 100       1411 if (m/^\s*BindParms\s+:\s+\((\s*#.*$|\s*$)/) {
410 4         14 $self->{'simple_bind'} = 1;
411 4         11 $self->{'bind_entries'} = [];
412 4         24 $self->{'bind_block'} = 1;
413 4         11 $self->{'line_block_start'} = $Acme::Sub::Parms::line_counter;
414 4         15 my $block_head_comment = $2;
415 4 50       17 $block_head_comment = defined ($block_head_comment) ? $block_head_comment : '';
416 4         9 $block_head_comment =~ s/[\r\n]+$//s;
417 4         9 $_ = $block_head_comment;
418              
419             #######
420             # ################################
421             # # Invokation : $self;
422             # } elsif (my ($ihead,$ivar,$itail) = m/^(\s*)Invokation\s+:\s+(\S+.*?)\s*;(.*)$/) {
423             # $_ = $ihead . " my $ivar = shift @_;$itail\n";
424             #
425             # ################################
426             # # ParmsHash : %args;
427             # } elsif (my ($fhead,$func_hash_ident,$ftail) = m/^(\s*)ParmsHash\s+:\s+(\S+.*?)\s*;(.*)$/) {
428             # if ($normalize) {
429             # $_ = "${fhead}my $func_hash_ident; { local \%Acme::Sub::Parms::raw_args = \@\_; $func_hash_ident = map \{ lc(\$\_\) \=\> \$Acme::Sub::Parms::raw_args\{\$\_\} \} keys \%Acme::Sub::Parms::raw_args; } $ftail\n";
430             # } else {
431             # $_ = "${fhead}my $func_hash_ident = \@\_;$ftail\n";
432             # }
433             #
434             # ################################
435             # # MethodParms : $self, %args;
436             # } elsif (my ($mhead,$method_invokation,$method_hash_ident,$mtail) = m/^(\s*)MethodParms\s+:\s+(\S+.*?)\s*,\s*(\S+.*?)\s*;(.*)$/) {
437             # if ($normalize) {
438             # $_ = "${mhead}my $method_invokation = shift; my $method_hash_ident; { local \$_; local \%Acme::Sub::Parms::raw_args = \@\_; $method_hash_ident = map \{ lc(\$\_\) \=\> \$Acme::Sub::Parms::raw_args\{\$\_\} \} keys \%Acme::Sub::Parms::raw_args; } $mtail\n";
439             # } else {
440             # $_ = "${mhead}my $method_invokation = shift; my $method_hash_ident = \@\_; $mtail\n";
441             # }
442             #######
443             }
444             }
445             }
446 436         538 if (_DEBUG) {
447             print STDERR "output as: $_";
448             }
449 436 100       1257 if ($dump_to_stdout) { print $_; }
  110         159  
450              
451 436         18236 return $status;
452             }
453              
454             ####
455              
456             1;
457