File Coverage

blib/lib/Getopt/EX/Hashed.pm
Criterion Covered Total %
statement 221 237 93.2
branch 90 102 88.2
condition 25 30 83.3
subroutine 43 46 93.4
pod 7 7 100.0
total 386 422 91.4


line stmt bran cond sub pod time code
1             package Getopt::EX::Hashed;
2              
3             our $VERSION = '1.0702';
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             Getopt::EX::Hashed - Hash object automation for Getopt::Long
10              
11             =head1 VERSION
12              
13             Version 1.0702
14              
15             =head1 SYNOPSIS
16              
17             # script/foo
18             use App::foo;
19             App::foo->new->run();
20              
21             # lib/App/foo.pm
22             package App::foo;
23              
24             use Getopt::EX::Hashed; {
25             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
26             has start => ' =i s begin ' , default => 1;
27             has end => ' =i e ' ;
28             has file => ' =s@ f ' , any => qr/^(?!\.)/;
29             has score => ' =i ' , min => 0, max => 100;
30             has answer => ' =i ' , must => sub { $_[1] == 42 };
31             has mouse => ' =s ' , any => [ 'Frankie', 'Benjy' ];
32             has question => ' =s ' , any => qr/^(life|universe|everything)$/i;
33             } no Getopt::EX::Hashed;
34              
35             sub run {
36             my $app = shift;
37             use Getopt::Long;
38             $app->getopt or pod2usage();
39             if ($app->answer == 42) {
40             $app->question //= 'life';
41             ...
42              
43             =cut
44              
45 18     18   2164287 use v5.14;
  18         53  
46 18     18   64 use warnings;
  18         26  
  18         832  
47 18     18   7662 use Hash::Util qw(lock_keys lock_keys_plus unlock_keys);
  18         47351  
  18         91  
48 18     18   1708 use Scalar::Util qw(refaddr);
  18         37  
  18         684  
49 18     18   66 use Carp;
  18         23  
  18         639  
50 18     18   1465 use Data::Dumper;
  18         20712  
  18         646  
51 18     18   68 use List::Util qw(first);
  18         24  
  18         1539  
52              
53             #
54             # Store metadata in caller context
55             #
56             my %__DB__;
57              
58             #
59             # Track objects created by new() to distinguish from Clone copies.
60             # Only "master" objects (created by new) are allowed to remove accessors
61             # on destruction, preventing clone objects from destroying shared class-level
62             # accessors that the original object still needs.
63             #
64             my %_masters;
65              
66             #
67             # Get or create package-specific metadata storage namespace
68             # Returns a hash reference for storing member and config data
69             #
70             sub __DB__ {
71 466     466   463 my $caller = shift;
72 466         474 state $pkg = __PACKAGE__ =~ s/::/_/gr;
73 18     18   131 no strict 'refs';
  18         35  
  18         3516  
74 466   100     1238 $__DB__{$caller} //= \%{"$caller\::$pkg\::__DB__"};
  31         292  
75             }
76              
77             #
78             # Get or create member/configuration metadata array for a package
79             #
80 213   100 213   295 sub __Member__ { __DB__($_[0])->{Member} //= [] }
81 253   100 253   320 sub __Config__ { __DB__($_[0])->{Config} //= {} }
82              
83             my %DefaultConfig = (
84             DEBUG_PRINT => 0,
85             LOCK_KEYS => 1,
86             REPLACE_UNDERSCORE => 1,
87             REMOVE_UNDERSCORE => 0,
88             GETOPT => 'GetOptions',
89             GETOPT_FROM_ARRAY => 'GetOptionsFromArray',
90             ACCESSOR_PREFIX => '',
91             ACCESSOR_LVALUE => 1,
92             REMOVE_ACCESSOR => 1,
93             DEFAULT => [],
94             INVALID_MSG => \&_invalid_msg,
95             );
96             lock_keys %DefaultConfig;
97              
98             our @EXPORT = qw(has);
99              
100             #
101             # Module import: set up inheritance, export functions, and initialize config
102             #
103             sub import {
104 31     31   78470 my $pkg = shift;
105 31         64 my $caller = caller;
106 18     18   83 no strict 'refs';
  18         26  
  18         2640  
107 31         46 push @{"$caller\::ISA"}, __PACKAGE__;
  31         274  
108 31         97 *{"$caller\::$_"} = \&$_ for @EXPORT;
  31         117  
109 31         63 my $config = __Config__($caller);
110 31 50       78 unless (%$config) {
111 31         91 unlock_keys %$config;
112 31 50       373 %$config = %DefaultConfig or die "Failed to initialize config";
113 31         82 lock_keys %$config;
114             }
115             }
116              
117             #
118             # Remove exported functions from caller's namespace
119             #
120             sub unimport {
121 20     20   4548 my $caller = caller;
122 18     18   95 no strict 'refs';
  18         24  
  18         12230  
123 20         48 delete ${"$caller\::"}{$_} for @EXPORT;
  20         9876  
124             }
125              
126             #
127             # Configure package or object settings
128             # Can be called as class method or instance method
129             #
130             sub configure {
131 8     8 1 274209 my $class = shift;
132 8         11 my $config = do {
133 8 100       26 if (ref $class) {
134 2         7 $class->_conf;
135             } else {
136 6 50       19 my $ctx = $class ne __PACKAGE__ ? $class : caller;
137 6         18 __Config__($ctx);
138             }
139             };
140 8         44 while (my($key, $value) = splice @_, 0, 2) {
141 8 100       37 if ($key eq 'DEFAULT') {
142 3 100       60 ref($value) eq 'ARRAY' or die "DEFAULT must be arrayref";
143 2 100       58 @$value % 2 == 0 or die "DEFAULT has wrong members";
144             }
145 6         22 $config->{$key} = $value;
146             }
147 6         12 return $class;
148             }
149              
150             #
151             # Reset class to original state, clearing all members and config
152             #
153             sub reset {
154 0     0 1 0 my $caller = caller;
155 0         0 my $member = __Member__($caller);
156 0         0 my $config = __Config__($caller);
157 0         0 @$member = ();
158 0         0 %$config = %DefaultConfig;
159 0         0 return $_[0];
160             }
161              
162             #
163             # Declare option parameters using DSL syntax
164             # Supports single or multiple option names and incremental updates with '+'
165             #
166             sub has {
167 192     192 1 1101907 my($key, @param) = @_;
168 192 100       292 if (@param % 2) {
169 22 100       58 my $default = ref $param[0] eq 'CODE' ? 'action' : 'spec';
170 22         48 unshift @param, $default;
171             }
172 192         326 my %param = @param;
173 192 50 66     477 if (defined $param{spec} and $param{spec} =~ s/\s*#\s*(.*)//) {
174 0         0 push @param, help => $1;
175             }
176 192 100       298 my @name = ref $key eq 'ARRAY' ? @$key : $key;
177 192         244 my $caller = caller;
178 192         1908 my $member = __Member__($caller);
179 192         256 for my $name (@name) {
180 218         255 my $append = $name =~ s/^\+//;
181 218     1690   609 my $i = first { $member->[$_][0] eq $name } keys @$member;
  1690         1652  
182 218 100       434 if ($append) {
183 23 100       3592 defined $i or die "$name: Not found\n";
184 22         51 push @{$member->[$i]}, @param;
  22         71  
185             } else {
186 195 50       251 defined $i and die "$name: Duplicated\n";
187 195         240 my $config = __Config__($caller);
188 195         212 push @$member, [ $name, @{$config->{DEFAULT}}, @param ];
  195         535  
189             }
190             }
191             }
192              
193             #
194             # Constructor: create hash object with initialized members and accessors
195             #
196             sub new {
197 21     21 1 557991 my $class = shift;
198 21         61 my $obj = bless {}, $class;
199 21 100       73 my $ctx = $class ne __PACKAGE__ ? $class : caller;
200 21         63 my $master = __Member__($ctx);
201 21         97 my $member = $obj->{__Member__} = [];
202 21         32 my $config = $obj->{__Config__} = { %{__Config__($ctx)} }; # make copy
  21         47  
203 21         62 for my $m (@$master) {
204 147         335 my($name, %param) = @$m;
205 147         275 push @$member, [ $name => \%param ];
206 147 100       223 next if $name eq '<>';
207 144 100       244 if (my $is = $param{is}) {
208 18     18   106 no strict 'refs';
  18         22  
  18         4931  
209 44         64 my $sub = "$class\::" . $config->{ACCESSOR_PREFIX} . $name;
210 44 50       132 if (defined &$sub) {
211 0         0 croak "&$sub already exists.\n";
212             }
213 44 100 100     74 $is = 'lv' if $is eq 'rw' && $config->{ACCESSOR_LVALUE};
214 44         61 *$sub = _accessor($is, $name);
215             }
216 144         215 $obj->{$name} = do {
217 144         158 local $_ = $param{default};
218 144 100       266 if (ref eq 'ARRAY') { [ @$_ ] }
  12 100       32  
    100          
219 9         39 elsif (ref eq 'HASH' ) { ({ %$_ }) }
220 4         16 elsif (ref eq 'CODE' ) { $_->() }
221 119         281 else { $_ }
222             };
223             }
224 21 50       149 lock_keys %$obj if $config->{LOCK_KEYS};
225 21         313 $_masters{refaddr($obj)} = 1;
226 21         64 $obj;
227             }
228              
229             #
230             # Destructor: remove accessor methods from package namespace
231             #
232             sub DESTROY {
233 22     22   38425 my $obj = shift;
234 22 100       113 return unless delete $_masters{refaddr($obj)};
235 21 100       66 return unless $obj->_conf->{REMOVE_ACCESSOR};
236 20         83 my $pkg = ref $obj;
237 18     18   90 my $hash = do { no strict 'refs'; \%{"$pkg\::"} };
  18         26  
  18         3371  
  20         29  
  20         24  
  20         81  
238 20         43 my $prefix = $obj->_conf->{ACCESSOR_PREFIX};
239 20         30 for (@{ $obj->_member }) {
  20         81  
240 146 100       1191 next unless $_->[1]->{is};
241 44         57 my $name = $prefix . $_->[0];
242 44 100       402 delete $hash->{$name} if exists $hash->{$name};
243             }
244             }
245              
246             #
247             # Generate option specification list for Getopt::Long
248             #
249             sub optspec {
250 16     16 1 33 my $obj = shift;
251 16         20 map $obj->_opt_pair($_), @{$obj->_member};
  16         64  
252             }
253              
254             #
255             # Call GetOptions or GetOptionsFromArray with generated option specs
256             #
257             sub getopt {
258 12     12 1 76 my $obj = shift;
259 12 100 33     74 if (@_ == 0) {
    50          
260 7         30 my $getopt = caller . "::" . $obj->_conf->{GETOPT};
261 18     18   87 no strict 'refs';
  18         30  
  18         1156  
262 7         26 $getopt->($obj->optspec());
263             }
264             elsif (@_ == 1 and ref $_[0] eq 'ARRAY') {
265 5         17 my $getopt = caller . "::" . $obj->_conf->{GETOPT_FROM_ARRAY};
266 18     18   80 no strict 'refs';
  18         21  
  18         22706  
267 5         16 $getopt->($_[0], $obj->optspec());
268             }
269             else {
270 0         0 die "getopt: wrong parameter.";
271             }
272             }
273              
274             #
275             # Add new keys to locked hash object
276             #
277             sub use_keys {
278 0     0 1 0 my $obj = shift;
279 0         0 unlock_keys %$obj;
280 0         0 lock_keys_plus %$obj, @_;
281             }
282              
283             #
284             # Get object's configuration hash
285             #
286 112     112   419 sub _conf { $_[0]->{__Config__} }
287              
288             #
289             # Get object's member metadata array
290             #
291 36     36   115 sub _member { $_[0]->{__Member__} }
292              
293             #
294             # Generate accessor method based on type (ro/rw/lv)
295             #
296             sub _accessor {
297 44     44   81 my($is, $name) = @_;
298             {
299             ro => sub {
300 28 100   28   184 @_ > 1 and die "$name is readonly\n";
301 27         124 $_[0]->{$name};
302             },
303             rw => sub {
304 3 100   3   10 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         2  
  1         2  
305 2         8 $_[0]->{$name};
306             },
307             lv => sub :lvalue {
308 10 100   10   57 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         3  
  1         1  
309 9         32 $_[0]->{$name};
310             },
311 44 50       392 }->{$is} or die "$name has invalid 'is' parameter.\n";
312             }
313              
314             #
315             # Generate option spec and destination pair for a member
316             #
317             sub _opt_pair {
318 139     139   171 my $obj = shift;
319 139         151 my $member = shift;
320 139   100     210 my $spec_str = $obj->_opt_str($member) // return ();
321 132         261 ( $spec_str => $obj->_opt_dest($member) );
322             }
323              
324             #
325             # Generate option spec string from member definition
326             #
327             sub _opt_str {
328 139     139   129 my $obj = shift;
329 139         123 my($name, $m) = @{+shift};
  139         193  
330              
331 139 100       231 $name eq '<>' and return $name;
332 136   100     233 my $spec = $m->{spec} // return undef;
333 129 100       183 if (my $alias = $m->{alias}) {
334 4         8 $spec .= " $alias";
335             }
336 129         215 $obj->_compile($name, $spec);
337             }
338              
339             #
340             # Compile option spec into Getopt::Long format
341             # Handles aliases and underscore conversion
342             #
343             sub _compile {
344 129     129   175 my $obj = shift;
345 129         174 my($name, $args) = @_;
346 129         206 my @args = split ' ', $args;
347 129         273 my $spec_re = qr/[!+=:]/;
348 129         439 my @spec = grep /$spec_re/, @args;
349 129         271 my @alias = grep !/$spec_re/, @args;
350 129         203 my $spec = do {
351 129 100       256 if (@spec == 0) { '' }
  23 50       27  
352 106         176 elsif (@spec == 1) { $spec[0] }
353 0         0 else { die "Multiple option specs found: @spec" }
354             };
355 129         178 my @names = ($name, @alias);
356 129         161 for ($name, @alias) {
357 149 50 66     283 push @names, tr[_][-]r if /_/ && $obj->_conf->{REPLACE_UNDERSCORE};
358 149 100 100     256 push @names, tr[_][]dr if /_/ && $obj->_conf->{REMOVE_UNDERSCORE};
359             }
360 129 100 66     915 push @names, '' if @names and $spec !~ /^($spec_re|$)/;
361 129         520 join('|', @names) . $spec;
362             }
363              
364             #
365             # Generate option destination (reference or coderef) with optional validation
366             #
367             sub _opt_dest {
368 132     132   134 my $obj = shift;
369 132         140 my($name, $m) = @{+shift};
  132         164  
370              
371 132         167 my $action = $m->{action};
372 132 100       178 if (my $is_valid = _validator($m)) {
    100          
373 26   100     72 $action ||= \&_generic_setter;
374             sub {
375 32     32   6134 local $_ = $obj;
376 32 100       44 &$is_valid or die &{$obj->_conf->{INVALID_MSG}};
  9         30  
377 23         33 &$action;
378 26         121 };
379             }
380             elsif ($action) {
381 14     20   78 sub { &$action for $obj };
  20         10847  
382             }
383             else {
384 92 50       199 if (ref $obj->{$name} eq 'CODE') {
    100          
385 0     0   0 sub { &{$obj->{$name}} for $obj };
  0         0  
  0         0  
386             } elsif (ref $obj->{$name}) {
387 16         54 $obj->{$name};
388             } else {
389 76         191 \$obj->{$name};
390             }
391             }
392             }
393              
394             #
395             # Validation test functions for min, max, must, and any parameters
396             #
397             my %tester = (
398             min => sub { $_[-1] >= $_->{min} },
399             max => sub { $_[-1] <= $_->{max} },
400             must => sub {
401             my $must = $_->{must};
402             for (ref($must) eq 'ARRAY' ? @$must : $must) {
403             return 0 if not &$_;
404             }
405             return 1;
406             },
407             any => sub {
408             my $any = $_->{any};
409             for (ref($any) eq 'ARRAY' ? @$any : $any) {
410             if (ref eq 'Regexp') {
411             return 1 if $_[-1] =~ $_;
412             } elsif (ref eq 'CODE') {
413             return 1 if &$_;
414             } else {
415             return 1 if $_[-1] eq $_;
416             }
417             }
418             return 0;
419             },
420             );
421              
422             #
423             # Get applicable tester functions for a member
424             #
425             sub _tester {
426 132     132   128 my $m = shift;
427 132         224 map $tester{$_}, grep { defined $m->{$_} } keys %tester;
  528         890  
428             }
429              
430             #
431             # Create validator coderef that combines multiple test functions
432             #
433             sub _validator {
434 132     132   117 my $m = shift;
435 132 100       179 my @test = _tester($m) or return undef;
436             sub {
437 32     32   31 local $_ = $m;
438 32         42 for my $test (@test) {
439 36 100       45 return 0 if not &$test;
440             }
441 23         63 return 1;
442             }
443 26         90 }
444              
445             #
446             # Generic setter for array, hash, and scalar values
447             #
448             sub _generic_setter {
449 20     20   90 my $dest = $_->{$_[0]};
450 7         25 (ref $dest eq 'ARRAY') ? do { push @$dest, $_[1] } :
451 6         23 (ref $dest eq 'HASH' ) ? do { $dest->{$_[1]} = $_[2] }
452 20 100       98 : do { $_->{$_[0]} = $_[1] };
  7 100       11  
    100          
453             }
454              
455             #
456             # Generate error message for option validation failures
457             #
458             sub _invalid_msg {
459 9     9   11 my $opt = do {
460 9         18 $_[0] = $_[0] =~ tr[_][-]r;
461 9 100       60 if (@_ <= 2) {
462 7         28 '--' . join '=', @_;
463             } else {
464 2         11 sprintf "--%s %s=%s", @_[0..2];
465             }
466             };
467 9         35 "$opt: option validation error\n";
468             }
469              
470             1;
471              
472             __END__