File Coverage

blib/lib/Getopt/EX/Hashed.pm
Criterion Covered Total %
statement 212 227 93.3
branch 81 92 88.0
condition 23 27 85.1
subroutine 42 45 93.3
pod 7 7 100.0
total 365 398 91.7


line stmt bran cond sub pod time code
1             package Getopt::EX::Hashed;
2              
3             our $VERSION = '1.0503';
4              
5             =head1 NAME
6              
7             Getopt::EX::Hashed - Hash store object automation for Getopt::Long
8              
9             =head1 VERSION
10              
11             Version 1.0503
12              
13             =head1 SYNOPSIS
14              
15             # script/foo
16             use App::foo;
17             App::foo->new->run();
18              
19             # lib/App/foo.pm
20             package App::foo;
21              
22             use Getopt::EX::Hashed; {
23             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
24             has start => ' =i s begin ' , default => 1;
25             has end => ' =i e ' ;
26             has file => ' =s@ f ' , any => qr/^(?!\.)/;
27             has score => ' =i ' , min => 0, max => 100;
28             has answer => ' =i ' , must => sub { $_[1] == 42 };
29             has mouse => ' =s ' , any => [ 'Frankie', 'Benjy' ];
30             has question => ' =s ' , any => qr/^(life|universe|everything)$/i;
31             } no Getopt::EX::Hashed;
32              
33             sub run {
34             my $app = shift;
35             use Getopt::Long;
36             $app->getopt or pod2usage();
37             if ($app->answer == 42) {
38             $app->question //= 'life';
39             ...
40              
41             =cut
42              
43 16     16   850975 use v5.14;
  16         170  
44 16     16   85 use warnings;
  16         23  
  16         507  
45 16     16   9463 use Hash::Util qw(lock_keys lock_keys_plus unlock_keys);
  16         41594  
  16         105  
46 16     16   1441 use Carp;
  16         26  
  16         652  
47 16     16   575 use Data::Dumper;
  16         5721  
  16         562  
48 16     16   83 use List::Util qw(first);
  16         26  
  16         2142  
49              
50             # store metadata in caller context
51             my %__DB__;
52             sub __DB__ {
53 416     416   467 my $caller = shift;
54 416         506 state $pkg = __PACKAGE__ =~ s/::/_/gr;
55 16     16   107 no strict 'refs';
  16         47  
  16         3110  
56 416   100     1216 $__DB__{$caller} //= \%{"$caller\::$pkg\::__DB__"};
  27         409  
57             }
58 191   100 191   269 sub __Member__ { __DB__($_[0])->{Member} //= [] }
59 225   100 225   353 sub __Config__ { __DB__($_[0])->{Config} //= {} }
60              
61             my %DefaultConfig = (
62             DEBUG_PRINT => 0,
63             LOCK_KEYS => 1,
64             REPLACE_UNDERSCORE => 1,
65             REMOVE_UNDERSCORE => 0,
66             GETOPT => 'GetOptions',
67             GETOPT_FROM_ARRAY => 'GetOptionsFromArray',
68             ACCESSOR_PREFIX => '',
69             ACCESSOR_LVALUE => 1,
70             DEFAULT => [],
71             INVALID_MSG => \&_invalid_msg,
72             );
73             lock_keys %DefaultConfig;
74              
75             our @EXPORT = qw(has);
76              
77             sub import {
78 27     27   75842 my $caller = caller;
79 16     16   105 no strict 'refs';
  16         27  
  16         2185  
80 27         42 push @{"$caller\::ISA"}, __PACKAGE__;
  27         272  
81 27         93 *{"$caller\::$_"} = \&$_ for @EXPORT;
  27         119  
82 27         66 my $config = __Config__($caller);
83 27 50       78 unless (%$config) {
84 27         90 unlock_keys %$config;
85 27 50       361 %$config = %DefaultConfig or die "something wrong!";
86 27         89 lock_keys %$config;
87             }
88             }
89              
90             sub unimport {
91 16     16   4672 my $caller = caller;
92 16     16   109 no strict 'refs';
  16         40  
  16         8960  
93 16         43 delete ${"$caller\::"}{$_} for @EXPORT;
  16         3766  
94             }
95              
96             sub configure {
97 6     6 1 578 my $class = shift;
98 6         20 my $config = do {
99 6 100       18 if (ref $class) {
100 2         6 $class->_conf;
101             } else {
102 4 50       11 my $ctx = $class ne __PACKAGE__ ? $class : caller;
103 4         10 __Config__($ctx);
104             }
105             };
106 6         32 while (my($key, $value) = splice @_, 0, 2) {
107 6 100       17 if ($key eq 'DEFAULT') {
108 3 100       56 ref($value) eq 'ARRAY' or die "DEFAULT must be arrayref";
109 2 100       51 @$value % 2 == 0 or die "DEFAULT have wrong member";
110             }
111 4         20 $config->{$key} = $value;
112             }
113 4         8 return $class;
114             }
115              
116             sub reset {
117 0     0 1 0 my $caller = caller;
118 0         0 my $member = __Member__($caller);
119 0         0 my $config = __Config__($caller);
120 0         0 @$member = ();
121 0         0 %$config = %DefaultConfig;
122 0         0 return $_[0];
123             }
124              
125             sub has {
126 177     177 1 1850 my($key, @param) = @_;
127 177 100       371 if (@param % 2) {
128 20 100       40 my $default = ref $param[0] eq 'CODE' ? 'action' : 'spec';
129 20         35 unshift @param, $default;
130             }
131 177 100       338 my @name = ref $key eq 'ARRAY' ? @$key : $key;
132 177         238 my $caller = caller;
133 177         237 my $member = __Member__($caller);
134 177         247 for my $name (@name) {
135 203         316 my $append = $name =~ s/^\+//;
136 203     1466   485 my $i = first { $member->[$_][0] eq $name } 0 .. $#{$member};
  1466         1576  
  203         498  
137 203 100       482 if ($append) {
138 23 100       86 defined $i or die "$name: Not found\n";
139 22         26 push @{$member->[$i]}, @param;
  22         55  
140             } else {
141 180 50       294 defined $i and die "$name: Duplicated\n";
142 180         275 my $config = __Config__($caller);
143 180         227 push @$member, [ $name, @{$config->{DEFAULT}}, @param ];
  180         546  
144             }
145             }
146             }
147              
148             sub new {
149 14     14 1 466 my $class = shift;
150 14         38 my $obj = bless {}, $class;
151 14 100       52 my $ctx = $class ne __PACKAGE__ ? $class : caller;
152 14         38 my $master = __Member__($ctx);
153 14         70 my $member = $obj->{__Member__} = [];
154 14         26 my $config = $obj->{__Config__} = { %{__Config__($ctx)} }; # make copy
  14         29  
155 14         46 for my $m (@$master) {
156 128         372 my($name, %param) = @$m;
157 128         231 push @$member, [ $name => \%param ];
158 128 100       227 next if $name eq '<>';
159 125 100       217 if (my $is = $param{is}) {
160 16     16   114 no strict 'refs';
  16         34  
  16         3709  
161 39         71 my $sub = "$class\::" . $config->{ACCESSOR_PREFIX} . $name;
162 39 50       130 if (defined &$sub) {
163 0         0 croak "&$sub already exists.\n";
164             }
165 39 100 100     74 $is = 'lv' if $is eq 'rw' && $config->{ACCESSOR_LVALUE};
166 39         59 *$sub = _accessor($is, $name);
167             }
168 125         229 $obj->{$name} = do {
169 125         153 local $_ = $param{default};
170 125 100       276 if (ref eq 'ARRAY') { [ @$_ ] }
  8 100       24  
    100          
171 5         13 elsif (ref eq 'HASH' ) { ({ %$_ }) }
172 4         15 elsif (ref eq 'CODE' ) { $_->() }
173 108         272 else { $_ }
174             };
175             }
176 14 50       96 lock_keys %$obj if $config->{LOCK_KEYS};
177 14         225 $obj;
178             }
179              
180             sub DESTROY {
181 14     14   28236 my $obj = shift;
182 14         34 my $pkg = ref $obj;
183 16     16   105 my $hash = do { no strict 'refs'; \%{"$pkg\::"} };
  16         30  
  16         2982  
  14         27  
  14         30  
  14         95  
184 14         53 my $prefix = $obj->_conf->{ACCESSOR_PREFIX};
185 14         29 for (@{ $obj->_member }) {
  14         58  
186 128 100       1072 next unless $_->[1]->{is};
187 40         62 my $name = $prefix . $_->[0];
188 40 100       351 delete $hash->{$name} if exists $hash->{$name};
189             }
190             }
191              
192             sub optspec {
193 12     12 1 35 my $obj = shift;
194 12         21 map $obj->_opt_pair($_), @{$obj->_member};
  12         30  
195             }
196              
197             sub getopt {
198 8     8 1 73 my $obj = shift;
199 8 100 33     30 if (@_ == 0) {
    50          
200 7         45 my $getopt = caller . "::" . $obj->_conf->{GETOPT};
201 16     16   111 no strict 'refs';
  16         52  
  16         1142  
202 7         34 $getopt->($obj->optspec());
203             }
204             elsif (@_ == 1 and ref $_[0] eq 'ARRAY') {
205 1         4 my $getopt = caller . "::" . $obj->_conf->{GETOPT_FROM_ARRAY};
206 16     16   103 no strict 'refs';
  16         74  
  16         21548  
207 1         2 $getopt->($_[0], $obj->optspec());
208             }
209             else {
210 0         0 die "getopt: wrong parameter.";
211             }
212             }
213              
214             sub use_keys {
215 0     0 1 0 my $obj = shift;
216 0         0 unlock_keys %$obj;
217 0         0 lock_keys_plus %$obj, @_;
218             }
219              
220 65     65   246 sub _conf { $_[0]->{__Config__} }
221              
222 26     26   92 sub _member { $_[0]->{__Member__} }
223              
224             sub _accessor {
225 39     39   59 my($is, $name) = @_;
226             {
227             ro => sub {
228 28 100   28   325 @_ > 1 and die "$name is readonly\n";
229 27         101 $_[0]->{$name};
230             },
231             rw => sub {
232 3 100   3   9 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         2  
  1         2  
233 2         19 $_[0]->{$name};
234             },
235             lv => sub :lvalue {
236 7 100   7   25 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         4  
  1         3  
237 6         22 $_[0]->{$name};
238             },
239 39 50       298 }->{$is} or die "$name has invalid 'is' parameter.\n";
240             }
241              
242             sub _opt_pair {
243 123     123   177 my $obj = shift;
244 123         134 my $member = shift;
245 123   100     199 my $spec_str = $obj->_opt_str($member) // return ();
246 116         249 ( $spec_str => $obj->_opt_dest($member) );
247             }
248              
249             sub _opt_str {
250 123     123   134 my $obj = shift;
251 123         140 my($name, $m) = @{+shift};
  123         191  
252              
253 123 100       233 $name eq '<>' and return $name;
254 120   100     229 my $spec = $m->{spec} // return undef;
255 113 100       191 if (my $alias = $m->{alias}) {
256 4         10 $spec .= " $alias";
257             }
258 113         190 $obj->_compile($name, $spec);
259             }
260              
261             sub _compile {
262 113     113   132 my $obj = shift;
263 113         164 my($name, $args) = @_;
264 113         223 my @args = split ' ', $args;
265 113         261 my $spec_re = qr/[!+=:]/;
266 113         491 my @spec = grep /$spec_re/, @args;
267 113         347 my @alias = grep !/$spec_re/, @args;
268 113         141 my $spec = do {
269 113 100       259 if (@spec == 0) { '' }
  23 50       33  
270 90         151 elsif (@spec == 1) { $spec[0] }
271 0         0 else { die }
272             };
273 113         185 my @names = ($name, @alias);
274 113         181 for ($name, @alias) {
275 133 50 66     303 push @names, tr[_][-]r if /_/ && $obj->_conf->{REPLACE_UNDERSCORE};
276 133 100 100     279 push @names, tr[_][]dr if /_/ && $obj->_conf->{REMOVE_UNDERSCORE};
277             }
278 113 100 66     869 push @names, '' if @names and $spec !~ /^($spec_re|$)/;
279 113         498 join('|', @names) . $spec;
280             }
281              
282             sub _opt_dest {
283 116     116   136 my $obj = shift;
284 116         126 my($name, $m) = @{+shift};
  116         182  
285              
286 116         180 my $action = $m->{action};
287 116 100       165 if (my $is_valid = _validator($m)) {
    100          
288 26   100     79 $action ||= \&_generic_setter;
289             sub {
290 32     32   6456 local $_ = $obj;
291 32 100       50 &$is_valid or die &{$obj->_conf->{INVALID_MSG}};
  9         35  
292 23         40 &$action;
293 26         163 };
294             }
295             elsif ($action) {
296 14     20   92 sub { &$action for $obj };
  20         10686  
297             }
298             else {
299 76 50       180 if (ref $obj->{$name} eq 'CODE') {
300 0     0   0 sub { &{$obj->{$name}} for $obj };
  0         0  
  0         0  
301             } else {
302 76         238 \$obj->{$name};
303             }
304             }
305             }
306              
307             my %tester = (
308             min => sub { $_[-1] >= $_->{min} },
309             max => sub { $_[-1] <= $_->{max} },
310             must => sub {
311             my $must = $_->{must};
312             for $_ (ref($must) eq 'ARRAY' ? @$must : $must) {
313             return 0 if not &$_;
314             }
315             return 1;
316             },
317             any => sub {
318             my $any = $_->{any};
319             for (ref($any) eq 'ARRAY' ? @$any : $any) {
320             if (ref eq 'Regexp') {
321             return 1 if $_[-1] =~ $_;
322             } elsif (ref eq 'CODE') {
323             return 1 if &$_;
324             } else {
325             return 1 if $_[-1] eq $_;
326             }
327             }
328             return 0;
329             },
330             );
331              
332             sub _tester {
333 116     116   130 my $m = shift;
334 116         235 map $tester{$_}, grep { defined $m->{$_} } keys %tester;
  464         939  
335             }
336              
337             sub _validator {
338 116     116   136 my $m = shift;
339 116 100       157 my @test = _tester($m) or return undef;
340             sub {
341 32     32   41 local $_ = $m;
342 32         49 for my $test (@test) {
343 38 100       57 return 0 if not &$test;
344             }
345 23         75 return 1;
346             }
347 26         114 }
348              
349             sub _generic_setter {
350 20     20   114 my $dest = $_->{$_[0]};
351 7         20 (ref $dest eq 'ARRAY') ? do { push @$dest, $_[1] } :
352 6         17 (ref $dest eq 'HASH' ) ? do { $dest->{$_[1]} = $_[2] }
353 20 100       170 : do { $_->{$_[0]} = $_[1] };
  7 100       25  
354             }
355              
356             sub _invalid_msg {
357 9     9   13 my $opt = do {
358 9         35 $_[0] = $_[0] =~ tr[_][-]r;
359 9 100       66 if (@_ <= 2) {
360 7         20 '--' . join '=', @_;
361             } else {
362 2         14 sprintf "--%s %s=%s", @_[0..2];
363             }
364             };
365 9         40 "$opt: option validation error\n";
366             }
367              
368             1;
369              
370             __END__