File Coverage

lib/Acme/Sub/Parms.pm
Criterion Covered Total %
statement 208 229 90.8
branch 72 96 75.0
condition 23 42 54.7
subroutine 12 12 100.0
pod 0 2 0.0
total 315 381 82.6


line stmt bran cond sub pod time code
1             package Acme::Sub::Parms;
2              
3 4     4   7530 use strict;
  4         23  
  4         114  
4 4     4   18 use warnings;
  4         8  
  4         106  
5 4     4   2222 use Filter::Util::Call;
  4         4066  
  4         404  
6              
7             BEGIN {
8 4     4   15 $Acme::Sub::Parms::VERSION = '1.03';
9 4         7 %Acme::Sub::Parms::args = ();
10 4         15 %Acme::Sub::Parms::raw_args = ();
11 4         12179 $Acme::Sub::Parms::line_counter = 0;
12             }
13              
14 445     445   644 sub _NORMALIZE () { return ':normalize'; };
15 473     473   582 sub _NO_VALIDATION () { return ':no_validation'; };
16 445     445   834 sub _DUMP () { return ':dump_to_stdout'; };
17             sub _DEBUG () { 0; };
18              
19             sub _legal_option {
20             return {
21             _NORMALIZE() => 1,
22             _NO_VALIDATION() => 1,
23             _DUMP() => 1,
24 5     5   7 }->{$_[0]};
25             }
26              
27             ####
28              
29             sub import {
30 4     4   41 my $class = shift;
31 4         13 my $options = {
32             _NORMALIZE() => 0,
33             _NO_VALIDATION() => 0,
34             _DUMP() => 0,
35             };
36 4         15 foreach my $item (@_) {
37 5 50       11 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         13 $options->{$item} = 1;
43             }
44 4         10 $Acme::Sub::Parms::line_counter = 0;
45 4         9 my $ref = {'options' => $options, 'bind_block' => 0 };
46 4         13 filter_add(bless $ref); # imported from Filter::Util::Call
47             }
48              
49             ####
50              
51             sub _parse_bind_spec {
52 28     28   40 my ($self, $raw_spec) = @_;
53              
54 28         36 my $spec = $raw_spec;
55              
56 28         62 my $spec_tokens = {
57             'is_defined' => 0,
58             'required' => 1,
59             'optional' => 0,
60             };
61 28         66 while ($spec ne '') {
62 56 100       291 if ($spec =~ s/^required(\s*,\s*|$)//) { # 'required' flag
    100          
    100          
    50          
63 8         13 $spec_tokens->{'required'} = 1;
64 8         16 $spec_tokens->{'optional'} = 0;
65              
66             } elsif ($spec =~ s/^optional(\s*,\s*|$)//) { # 'optional' flag
67 16         36 $spec_tokens->{'required'} = 0;
68 16         32 $spec_tokens->{'optional'} = 1;
69              
70             } elsif ($spec =~ s/^is_defined(\s*,\s*|$)//) { # 'is_defined' flag
71 8         18 $spec_tokens->{'is_defined'} = 1;
72              
73             } elsif ($spec =~ s/^(can|isa|type|callback|default)\s*=\s*//) { # 'something="somevalue"'
74 24         53 my $spec_key = $1;
75              
76             # Simple unquoted text with no embedded ws
77 24 100       128 if ($spec =~ s/^([^\s"',]+)(\s*,\s*|$)//) {
    50          
    50          
    50          
78 20         67 $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         10 my $quote = $1;
91 4         8 my $upend_spec = reverse $spec;
92 4         8 my $block_done = 0;
93 4         4 my $escape_next = 0;
94 4         8 my $token = $quote;
95 4   66     50 until ($block_done || ($upend_spec eq '')) {
96 32         61 my $ch = chop $upend_spec;
97 32 50 33     88 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         12 $block_done = 1;
107              
108             } else {
109 28         74 $token .= $ch;
110             }
111             }
112 4 50       10 if ($escape_next) {
113 0         0 die("Syntax error in BindParms spec: $raw_spec\n");
114             }
115 4         8 $spec = reverse $upend_spec;
116 4         25 $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         57 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 34 my $self = shift;
146 28         43 my ($raw_spec, $field_name) = @_;
147              
148 28         36 my $options = $self->{'options'};
149 28         42 my $no_validation = $options->{_NO_VALIDATION()};
150              
151 28         187 my $spec_tokens = $self->_parse_bind_spec($raw_spec);
152              
153 28         34 my $has_side_effects = 0;
154 28         32 my $output = '';
155              
156 28         93 my @spec_tokens_list = keys %$spec_tokens;
157 28 0 33     89 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       56 if (defined $spec_tokens->{'default'}) {
164 4 50       12 if ($spec_tokens->{'optional'}) {
165 4         30 $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         8 $has_side_effects = 1;
170             }
171              
172             ######################
173             # callback="some_subroutine"
174 28 100       45 if ($spec_tokens->{'callback'}) {
175             $output .= "\{ my (\$callback_is_valid, \$callback_error) = "
176 8         59 . $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         14 $has_side_effects = 1;
180             }
181              
182             ######################
183             # required
184 28 100 100     89 if ((! $no_validation) && $spec_tokens->{'required'}) {
185 4         20 $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       47 if ($spec_tokens->{'is_defined'}) {
191 8         29 $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         36 my $type_requirements = $spec_tokens->{'type'};
195 28         37 my $isa_requirements = $spec_tokens->{'isa'};
196 28         30 my $can_requirements = $spec_tokens->{'can'};
197              
198 28 100 100     118 if (defined ($type_requirements ) || defined($isa_requirements) || defined($can_requirements)) {
      100        
199 12         37 $output .= "if (exists (\$Acme::Sub::Parms::args\{\'$field_name\'\})) \{";
200              
201             #####################
202             # type="SomeRefType" or type="SomeRefType, SomeOtherRefType, ..."
203 12 100       25 if (defined $type_requirements) {
204 4         9 $type_requirements =~ s/^['"]//;
205 4         8 $type_requirements =~ s/['"]$//;
206 4         11 my @type_classes = split(/[,\s]+/, $type_requirements);
207 4         15 $output .= "unless (";
208 4         8 my @type_tests = ();
209 4         6 foreach my $class_name (@type_classes) {
210 4         14 push (@type_tests, "ref(\$Acme::Sub::Parms::args\{'$field_name'\}) eq '$class_name')");
211             }
212 4         19 $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       34 if (defined $isa_requirements) {
218 4         17 $isa_requirements =~ s/^['"]//;
219 4         9 $isa_requirements =~ s/['"]$//;
220 4         15 my @isa_classes = split(/[,\s]+/, $isa_requirements);
221 4         8 $output .= "unless (";
222 4         8 my @isa_tests = ();
223 4         7 foreach my $class_name (@isa_classes) {
224 4         23 push (@isa_tests, "\$Acme::Sub::Parms::args\{'$field_name'\}->isa('$class_name')");
225             }
226 4         21 $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       39 if (defined $can_requirements) {
232 4         13 $can_requirements =~ s/^['"]//;
233 4         18 $can_requirements =~ s/['"]$//;
234 4         22 my @can_methods = split(/[,\s]+/, $can_requirements);
235 4         35 $output .= "unless (";
236 4         17 my @can_tests = ();
237 4         8 foreach my $method_name (@can_methods) {
238 4         15 push (@can_tests, "\$Acme::Sub::Parms::args\{'$field_name'\}->can('$method_name')");
239             }
240 4         28 $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         19 $output .= "\}";
244             }
245              
246 28         132 return ($has_side_effects,$output);
247             }
248              
249             ####
250              
251             sub filter {
252 436     436 0 803 my $self = shift;
253              
254 436         518 my $options = $self->{'options'};
255 436         597 my $dump_to_stdout = $options->{_DUMP()};
256 436         570 my $normalize = $options->{_NORMALIZE()};
257 436         522 my $no_validation = $options->{_NO_VALIDATION()};
258 436         464 my $bind_block = $self->{'bind_block'};
259              
260 436         422 my $status;
261              
262 436 100       1430 if ($status = filter_read() > 0) { # imported from Filter::Util::Call
263 432         485 $Acme::Sub::Parms::line_counter++;
264            
265 432         390 if (_DEBUG) {
266             print STDERR "input line $Acme::Sub::Parms::line_counter: $_";
267             }
268            
269             #############################################
270             # If we are in a bind block, handle it
271 432 100       539 if ($bind_block) {
272 36         46 my $bind_entries = $self->{'bind_entries'};
273 36         37 my $simple_bind = $self->{'simple_bind'};
274              
275             ##############################
276             # Last line of the bind block? Generate the working code.
277 36 100       373 if (m/^\s*\)(\s*$|\s*#.*$)/) {
    100          
    50          
278            
279 4         10 my $block_trailing_comment = $2;
280 4 50       13 $block_trailing_comment = defined($block_trailing_comment) ? $block_trailing_comment : '';
281 4         8 $block_trailing_comment =~ s/[\r\n]+$//s;
282 4         22 my $side_effects = 0;
283 4         7 my $args = 'local %Acme::Sub::Parms::args; '; # needed?
284 4 100       10 if ($normalize) {
285 2         6 $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";
286             } else {
287 2         7 $args .= '%Acme::Sub::Parms::args = @_;' . "\n";
288             }
289             # If we have validation or defaults, handle them
290 4         4 my $padding_lines = 0;
291 4 50       12 if (! $simple_bind) {
292 4         9 my @parm_declarations = ();
293 4         9 foreach my $entry (@$bind_entries) {
294 32         49 my $variable_decl = $entry->{'variable'};
295 32         41 my $field_name = $entry->{'field'};
296 32         34 my $spec = $entry->{'spec'};
297 32         39 my $trailing_comment = $entry->{'trailing_comment'};
298 32 100 66     129 if ( (! defined($spec)) || ($spec eq '')) {
299             # push(@parm_declarations, $trailing_comment);
300 4         11 next;
301             }
302             # The hard case. We have validation requirements.
303 28         61 my ($has_side_effects, $bind_spec_output) = $self->bind_spec($spec, $field_name);
304 28         40 $side_effects += $has_side_effects;
305 28         90 push (@parm_declarations, "$bind_spec_output$trailing_comment");
306             }
307 4         58 $args .= join("\n",@parm_declarations,'');
308             }
309              
310             # Generate the actual parameter data binding
311 4         12 my @var_declarations = ();
312 4         6 my @hard_var_declarations = ();
313 4         8 my @field_declarations = ();
314 4         5 my @fields_list = ();
315 4         8 foreach my $entry (@$bind_entries) {
316 32         43 my $spec = $entry->{'spec'};
317 32 100 66     122 next if ((not defined $spec) || ($spec eq ''));
318 28         35 my $raw_var = $entry->{'variable'};
319 28         34 my $field_name = $entry->{'field'};
320            
321 28         57 push (@fields_list, "'$field_name'");
322 28         110 my ($variable_name) = $raw_var =~ m/^my\s+(\S+)$/;
323 28 50       53 if (defined $variable_name) { # simple 'my $variable :' entries are special-cased for performance
324 28         33 push (@var_declarations, $variable_name);
325 28         61 push (@field_declarations, "'$field_name'");
326              
327             } else { # Otherwise make a seperate entry for this binding
328 0         0 push (@hard_var_declarations, "$raw_var = \$Acme::Sub::Parms::args\{$field_name\};");
329             }
330             }
331 4         17 my $hard_args = join(' ',@hard_var_declarations);
332 4         6 my $arg_line = '';
333 4 50       44 if (0 < @var_declarations) {
334            
335 4 50 33     60 if ($simple_bind && (! $normalize) && $no_validation && (0 == $side_effects) && (0 == @hard_var_declarations)) {
      33        
      0        
      0        
336 0         0 $args = "\n my (" . join(",", @var_declarations) . ') = @{{@_}}{' . join(',',@field_declarations) . '}; ';
337              
338             } else {
339            
340 4         52 $arg_line = 'my (' . join(",", @var_declarations) . ') = @Acme::Sub::Parms::args{' . join(',',@field_declarations) . '}; ';
341             }
342             }
343 4         11 my $unknown_parms_check = '';
344 4 100       13 unless ($no_validation) {
345 2         13 $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)); } ';
346              
347             }
348 4         10 $self->{'bind_block'} = 0;
349 4         8 my $original_block_length = $Acme::Sub::Parms::line_counter - $self->{'line_block_start'};
350 4         43 my $new_block = $args . join(' ',$arg_line, $hard_args, $unknown_parms_check) . "$block_trailing_comment\n";
351 4         65 $new_block =~ s/\n+/\n/gs;
352 4         19 my $new_block_lines = $new_block =~ m/\n/gs;
353            
354 4         8 my $additional_lines = $original_block_length - $new_block_lines;
355             #warn("Need $additional_lines extra lines\n---\n$new_block---\n");
356 4 50       23 if ($additional_lines > 0) {
357 4         65 $_ = $new_block . ("\n" x $additional_lines);
358             } else {
359 0         0 $_ = $new_block;
360             }
361              
362             ########################
363             # Bind block parameter line
364             } elsif (my($bind_var, $bind_field,$trailing_comment) = m/^\s*(\S.*?)\s+:\s+([^'"\s\[]+.*?)\s*(;\s*|;\s*#.*)$/) {
365 28 50       72 $trailing_comment = defined($trailing_comment) ? $trailing_comment : '';
366 28         90 $trailing_comment =~ s/[\r\n]+$//s;
367 28         64 $trailing_comment =~ s/^;//;
368 28         110 my $bind_entry = { 'variable' => $bind_var, 'field' => $bind_field, trailing_comment => $trailing_comment };
369 28         50 push (@$bind_entries, $bind_entry);
370 28 50       88 if ($bind_var !~ m/^my \$\S+$/) {
371 0         0 $self->{'simple_bind'} = 0;
372             }
373 28 100       97 if ($bind_field =~ m/^(\S+)\s*\[(.*)\]$/) { # Complex spec
    50          
374 26         62 $bind_entry->{'field'} = $1;
375 26         68 $bind_entry->{'spec'} = $2;
376 26 100 100     119 unless ($no_validation && ($bind_field !~ m/[\s\[,](default|callback)\s*=\s*/)) {
377 18         26 $self->{'simple_bind'} = 0;
378             }
379             } elsif ($bind_field =~ m/^\w+$/) { # my $thing : something;
380 2         4 $bind_entry->{'spec'} = 'required';
381 2 50       7 unless ($no_validation) {
382 0         0 $self->{'simple_bind'} = 0;
383             }
384             } else {
385 0         0 die("Failed to parse BindParms block line $Acme::Sub::Parms::line_counter: $_");
386             }
387 28         53 undef $trailing_comment;
388 28         33 undef $bind_var;
389 28         64 undef $bind_field;
390 28         60 $_ = '';
391              
392             ############################
393             # Blank and comment only lines
394             } elsif (m/^(\s*|\s*#.*)$/) {
395 4         22 my $trailing_comment = $1;
396 4 50       14 $trailing_comment = defined ($trailing_comment) ? $trailing_comment : '';
397 4         15 $trailing_comment =~ s/[\r\n]+$//s;
398            
399 4         12 my $bind_entry = { spec => '', trailing_comment => $trailing_comment};
400 4         8 push (@$bind_entries, $bind_entry);
401 4         8 $_ = '';
402            
403             } else {
404 0         0 die("Failed to parse BindParms block line $Acme::Sub::Parms::line_counter: $_");
405             }
406              
407             } else { # Start of a bind block
408 396 100       741 if (m/^\s*BindParms\s+:\s+\((\s*#.*$|\s*$)/) {
409 4         12 $self->{'simple_bind'} = 1;
410 4         10 $self->{'bind_entries'} = [];
411 4         8 $self->{'bind_block'} = 1;
412 4         15 $self->{'line_block_start'} = $Acme::Sub::Parms::line_counter;
413 4         23 my $block_head_comment = $2;
414 4 50       26 $block_head_comment = defined ($block_head_comment) ? $block_head_comment : '';
415 4         8 $block_head_comment =~ s/[\r\n]+$//s;
416 4         9 $_ = $block_head_comment;
417              
418             #######
419             # ################################
420             # # Invokation : $self;
421             # } elsif (my ($ihead,$ivar,$itail) = m/^(\s*)Invokation\s+:\s+(\S+.*?)\s*;(.*)$/) {
422             # $_ = $ihead . " my $ivar = shift @_;$itail\n";
423             #
424             # ################################
425             # # ParmsHash : %args;
426             # } elsif (my ($fhead,$func_hash_ident,$ftail) = m/^(\s*)ParmsHash\s+:\s+(\S+.*?)\s*;(.*)$/) {
427             # if ($normalize) {
428             # $_ = "${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";
429             # } else {
430             # $_ = "${fhead}my $func_hash_ident = \@\_;$ftail\n";
431             # }
432             #
433             # ################################
434             # # MethodParms : $self, %args;
435             # } elsif (my ($mhead,$method_invokation,$method_hash_ident,$mtail) = m/^(\s*)MethodParms\s+:\s+(\S+.*?)\s*,\s*(\S+.*?)\s*;(.*)$/) {
436             # if ($normalize) {
437             # $_ = "${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";
438             # } else {
439             # $_ = "${mhead}my $method_invokation = shift; my $method_hash_ident = \@\_; $mtail\n";
440             # }
441             #######
442             }
443             }
444             }
445 436         413 if (_DEBUG) {
446             print STDERR "output as: $_";
447             }
448 436 100       562 if ($dump_to_stdout) { print $_; }
  110         154  
449              
450 436         11578 return $status;
451             }
452              
453             ####
454              
455             1;
456