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.0701';
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.0701
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 17     17   3058869 use v5.14;
  17         65  
46 17     17   95 use warnings;
  17         27  
  17         1156  
47 17     17   9776 use Hash::Util qw(lock_keys lock_keys_plus unlock_keys);
  17         64593  
  17         134  
48 17     17   2149 use Scalar::Util qw(refaddr);
  17         49  
  17         887  
49 17     17   96 use Carp;
  17         28  
  17         856  
50 17     17   1486 use Data::Dumper;
  17         21032  
  17         829  
51 17     17   123 use List::Util qw(first);
  17         28  
  17         2135  
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 453     453   663 my $caller = shift;
72 453         653 state $pkg = __PACKAGE__ =~ s/::/_/gr;
73 17     17   181 no strict 'refs';
  17         44  
  17         4946  
74 453   100     1767 $__DB__{$caller} //= \%{"$caller\::$pkg\::__DB__"};
  30         2084  
75             }
76              
77             #
78             # Get or create member/configuration metadata array for a package
79             #
80 207   100 207   407 sub __Member__ { __DB__($_[0])->{Member} //= [] }
81 246   100 246   480 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 30     30   112629 my $pkg = shift;
105 30         91 my $caller = caller;
106 17     17   120 no strict 'refs';
  17         36  
  17         3546  
107 30         59 push @{"$caller\::ISA"}, __PACKAGE__;
  30         355  
108 30         131 *{"$caller\::$_"} = \&$_ for @EXPORT;
  30         158  
109 30         92 my $config = __Config__($caller);
110 30 50       110 unless (%$config) {
111 30         148 unlock_keys %$config;
112 30 50       507 %$config = %DefaultConfig or die "Failed to initialize config";
113 30         142 lock_keys %$config;
114             }
115             }
116              
117             #
118             # Remove exported functions from caller's namespace
119             #
120             sub unimport {
121 19     19   6459 my $caller = caller;
122 17     17   113 no strict 'refs';
  17         35  
  17         14703  
123 19         60 delete ${"$caller\::"}{$_} for @EXPORT;
  19         6705  
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 476358 my $class = shift;
132 8         21 my $config = do {
133 8 100       42 if (ref $class) {
134 2         11 $class->_conf;
135             } else {
136 6 50       29 my $ctx = $class ne __PACKAGE__ ? $class : caller;
137 6         25 __Config__($ctx);
138             }
139             };
140 8         57 while (my($key, $value) = splice @_, 0, 2) {
141 8 100       32 if ($key eq 'DEFAULT') {
142 3 100       100 ref($value) eq 'ARRAY' or die "DEFAULT must be arrayref";
143 2 100       61 @$value % 2 == 0 or die "DEFAULT has wrong members";
144             }
145 6         31 $config->{$key} = $value;
146             }
147 6         17 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 190     190 1 1403560 my($key, @param) = @_;
168 190 100       447 if (@param % 2) {
169 20 100       40 my $default = ref $param[0] eq 'CODE' ? 'action' : 'spec';
170 20         39 unshift @param, $default;
171             }
172 190         525 my %param = @param;
173 190 50 66     737 if (defined $param{spec} and $param{spec} =~ s/\s*#\s*(.*)//) {
174 0         0 push @param, help => $1;
175             }
176 190 100       429 my @name = ref $key eq 'ARRAY' ? @$key : $key;
177 190         329 my $caller = caller;
178 190         2396 my $member = __Member__($caller);
179 190         369 for my $name (@name) {
180 216         407 my $append = $name =~ s/^\+//;
181 216     1689   1103 my $i = first { $member->[$_][0] eq $name } keys @$member;
  1689         2440  
182 216 100       695 if ($append) {
183 23 100       1940 defined $i or die "$name: Not found\n";
184 22         45 push @{$member->[$i]}, @param;
  22         2356  
185             } else {
186 193 50       407 defined $i and die "$name: Duplicated\n";
187 193         354 my $config = __Config__($caller);
188 193         482 push @$member, [ $name, @{$config->{DEFAULT}}, @param ];
  193         968  
189             }
190             }
191             }
192              
193             #
194             # Constructor: create hash object with initialized members and accessors
195             #
196             sub new {
197 17     17 1 853643 my $class = shift;
198 17         54 my $obj = bless {}, $class;
199 17 100       74 my $ctx = $class ne __PACKAGE__ ? $class : caller;
200 17         78 my $master = __Member__($ctx);
201 17         114 my $member = $obj->{__Member__} = [];
202 17         40 my $config = $obj->{__Config__} = { %{__Config__($ctx)} }; # make copy
  17         65  
203 17         77 for my $m (@$master) {
204 139         500 my($name, %param) = @$m;
205 139         454 push @$member, [ $name => \%param ];
206 139 100       307 next if $name eq '<>';
207 136 100       370 if (my $is = $param{is}) {
208 17     17   136 no strict 'refs';
  17         32  
  17         6859  
209 44         87 my $sub = "$class\::" . $config->{ACCESSOR_PREFIX} . $name;
210 44 50       205 if (defined &$sub) {
211 0         0 croak "&$sub already exists.\n";
212             }
213 44 100 100     132 $is = 'lv' if $is eq 'rw' && $config->{ACCESSOR_LVALUE};
214 44         96 *$sub = _accessor($is, $name);
215             }
216 136         314 $obj->{$name} = do {
217 136         220 local $_ = $param{default};
218 136 100       380 if (ref eq 'ARRAY') { [ @$_ ] }
  8 100       32  
    100          
219 5         21 elsif (ref eq 'HASH' ) { ({ %$_ }) }
220 4         27 elsif (ref eq 'CODE' ) { $_->() }
221 119         492 else { $_ }
222             };
223             }
224 17 50       166 lock_keys %$obj if $config->{LOCK_KEYS};
225 17         378 $_masters{refaddr($obj)} = 1;
226 17         102 $obj;
227             }
228              
229             #
230             # Destructor: remove accessor methods from package namespace
231             #
232             sub DESTROY {
233 18     18   53080 my $obj = shift;
234 18 100       124 return unless delete $_masters{refaddr($obj)};
235 17 100       118 return unless $obj->_conf->{REMOVE_ACCESSOR};
236 16         62 my $pkg = ref $obj;
237 17     17   124 my $hash = do { no strict 'refs'; \%{"$pkg\::"} };
  17         38  
  17         4614  
  16         32  
  16         26  
  16         90  
238 16         51 my $prefix = $obj->_conf->{ACCESSOR_PREFIX};
239 16         29 for (@{ $obj->_member }) {
  16         114  
240 138 100       1252 next unless $_->[1]->{is};
241 44         57 my $name = $prefix . $_->[0];
242 44 100       420 delete $hash->{$name} if exists $hash->{$name};
243             }
244             }
245              
246             #
247             # Generate option specification list for Getopt::Long
248             #
249             sub optspec {
250 12     12 1 36 my $obj = shift;
251 12         28 map $obj->_opt_pair($_), @{$obj->_member};
  12         66  
252             }
253              
254             #
255             # Call GetOptions or GetOptionsFromArray with generated option specs
256             #
257             sub getopt {
258 8     8 1 105 my $obj = shift;
259 8 100 33     66 if (@_ == 0) {
    50          
260 7         42 my $getopt = caller . "::" . $obj->_conf->{GETOPT};
261 17     17   126 no strict 'refs';
  17         32  
  17         1745  
262 7         33 $getopt->($obj->optspec());
263             }
264             elsif (@_ == 1 and ref $_[0] eq 'ARRAY') {
265 1         32 my $getopt = caller . "::" . $obj->_conf->{GETOPT_FROM_ARRAY};
266 17     17   107 no strict 'refs';
  17         36  
  17         31859  
267 1         14 $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 100     100   456 sub _conf { $_[0]->{__Config__} }
287              
288             #
289             # Get object's member metadata array
290             #
291 28     28   139 sub _member { $_[0]->{__Member__} }
292              
293             #
294             # Generate accessor method based on type (ro/rw/lv)
295             #
296             sub _accessor {
297 44     44   76 my($is, $name) = @_;
298             {
299             ro => sub {
300 28 100   28   248 @_ > 1 and die "$name is readonly\n";
301 27         161 $_[0]->{$name};
302             },
303             rw => sub {
304 3 100   3   16 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         4  
  1         3  
305 2         12 $_[0]->{$name};
306             },
307             lv => sub :lvalue {
308 10 100   10   81 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         3  
  1         3  
309 9         78 $_[0]->{$name};
310             },
311 44 50       751 }->{$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 131     131   233 my $obj = shift;
319 131         185 my $member = shift;
320 131   100     276 my $spec_str = $obj->_opt_str($member) // return ();
321 124         339 ( $spec_str => $obj->_opt_dest($member) );
322             }
323              
324             #
325             # Generate option spec string from member definition
326             #
327             sub _opt_str {
328 131     131   230 my $obj = shift;
329 131         202 my($name, $m) = @{+shift};
  131         332  
330              
331 131 100       298 $name eq '<>' and return $name;
332 128   100     341 my $spec = $m->{spec} // return undef;
333 121 100       259 if (my $alias = $m->{alias}) {
334 4         14 $spec .= " $alias";
335             }
336 121         288 $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 121     121   171 my $obj = shift;
345 121         255 my($name, $args) = @_;
346 121         278 my @args = split ' ', $args;
347 121         374 my $spec_re = qr/[!+=:]/;
348 121         680 my @spec = grep /$spec_re/, @args;
349 121         414 my @alias = grep !/$spec_re/, @args;
350 121         238 my $spec = do {
351 121 100       302 if (@spec == 0) { '' }
  23 50       46  
352 98         231 elsif (@spec == 1) { $spec[0] }
353 0         0 else { die "Multiple option specs found: @spec" }
354             };
355 121         251 my @names = ($name, @alias);
356 121         228 for ($name, @alias) {
357 141 50 66     401 push @names, tr[_][-]r if /_/ && $obj->_conf->{REPLACE_UNDERSCORE};
358 141 100 100     438 push @names, tr[_][]dr if /_/ && $obj->_conf->{REMOVE_UNDERSCORE};
359             }
360 121 100 66     1339 push @names, '' if @names and $spec !~ /^($spec_re|$)/;
361 121         724 join('|', @names) . $spec;
362             }
363              
364             #
365             # Generate option destination (reference or coderef) with optional validation
366             #
367             sub _opt_dest {
368 124     124   202 my $obj = shift;
369 124         171 my($name, $m) = @{+shift};
  124         245  
370              
371 124         225 my $action = $m->{action};
372 124 100       258 if (my $is_valid = _validator($m)) {
    100          
373 26   100     94 $action ||= \&_generic_setter;
374             sub {
375 32     32   9782 local $_ = $obj;
376 32 100       87 &$is_valid or die &{$obj->_conf->{INVALID_MSG}};
  9         678  
377 23         51 &$action;
378 26         199 };
379             }
380             elsif ($action) {
381 14     20   122 sub { &$action for $obj };
  20         17168  
382             }
383             else {
384 84 50       298 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} eq 'SCALAR') {
387 8         57 $obj->{$name};
388             } else {
389 76         322 \$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 124     124   179 my $m = shift;
427 124         330 map $tester{$_}, grep { defined $m->{$_} } keys %tester;
  496         1222  
428             }
429              
430             #
431             # Create validator coderef that combines multiple test functions
432             #
433             sub _validator {
434 124     124   175 my $m = shift;
435 124 100       270 my @test = _tester($m) or return undef;
436             sub {
437 32     32   52 local $_ = $m;
438 32         68 for my $test (@test) {
439 38 100       73 return 0 if not &$test;
440             }
441 23         82 return 1;
442             }
443 26         144 }
444              
445             #
446             # Generic setter for array, hash, and scalar values
447             #
448             sub _generic_setter {
449 20     20   196 my $dest = $_->{$_[0]};
450 7         44 (ref $dest eq 'ARRAY') ? do { push @$dest, $_[1] } :
451 6         33 (ref $dest eq 'HASH' ) ? do { $dest->{$_[1]} = $_[2] }
452 20 100       155 : do { $_->{$_[0]} = $_[1] };
  7 100       18  
    100          
453             }
454              
455             #
456             # Generate error message for option validation failures
457             #
458             sub _invalid_msg {
459 9     9   34 my $opt = do {
460 9         34 $_[0] = $_[0] =~ tr[_][-]r;
461 9 100       101 if (@_ <= 2) {
462 7         31 '--' . join '=', @_;
463             } else {
464 2         43 sprintf "--%s %s=%s", @_[0..2];
465             }
466             };
467 9         64 "$opt: option validation error\n";
468             }
469              
470             1;
471              
472             __END__