File Coverage

blib/lib/Getopt/EX/Hashed.pm
Criterion Covered Total %
statement 197 211 93.3
branch 75 86 87.2
condition 23 27 85.1
subroutine 40 43 93.0
pod 7 7 100.0
total 342 374 91.4


line stmt bran cond sub pod time code
1             package Getopt::EX::Hashed;
2              
3             our $VERSION = '1.0502';
4              
5             =head1 NAME
6              
7             Getopt::EX::Hashed - Hash store object automation for Getopt::Long
8              
9             =head1 VERSION
10              
11             Version 1.0502
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   832507 use v5.14;
  16         157  
44 16     16   81 use warnings;
  16         29  
  16         459  
45 16     16   9330 use Hash::Util qw(lock_keys lock_keys_plus unlock_keys);
  16         40369  
  16         101  
46 16     16   1412 use Carp;
  16         28  
  16         652  
47 16     16   566 use Data::Dumper;
  16         5647  
  16         610  
48 16     16   84 use List::Util qw(first);
  16         28  
  16         2021  
49              
50             # store metadata in caller context
51             my %__DB__;
52             sub __DB__ {
53 416     416   496 my $caller = shift;
54 416         503 state $pkg = __PACKAGE__ =~ s/::/_/gr;
55 16     16   100 no strict 'refs';
  16         46  
  16         2968  
56 416   100     1191 $__DB__{$caller} //= \%{"$caller\::$pkg\::__DB__"};
  27         378  
57             }
58 191   100 191   288 sub __Member__ { __DB__($_[0])->{Member} //= [] }
59 225   100 225   327 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   76495 my $caller = caller;
79 16     16   104 no strict 'refs';
  16         30  
  16         2183  
80 27         44 push @{"$caller\::ISA"}, __PACKAGE__;
  27         279  
81 27         101 *{"$caller\::$_"} = \&$_ for @EXPORT;
  27         123  
82 27         70 my $config = __Config__($caller);
83 27 50       81 unless (%$config) {
84 27         121 unlock_keys %$config;
85 27 50       364 %$config = %DefaultConfig or die "something wrong!";
86 27         81 lock_keys %$config;
87             }
88             }
89              
90             sub unimport {
91 16     16   4698 my $caller = caller;
92 16     16   100 no strict 'refs';
  16         47  
  16         8451  
93 16         46 delete ${"$caller\::"}{$_} for @EXPORT;
  16         3508  
94             }
95              
96             sub configure {
97 6     6 1 606 my $class = shift;
98 6         12 my $config = do {
99 6 100       19 if (ref $class) {
100 2         4 $class->_conf;
101             } else {
102 4 50       16 my $ctx = $class ne __PACKAGE__ ? $class : caller;
103 4         25 __Config__($ctx);
104             }
105             };
106 6         38 while (my($key, $value) = splice @_, 0, 2) {
107 6 100       19 if ($key eq 'DEFAULT') {
108 3 100       89 ref($value) eq 'ARRAY' or die "DEFAULT must be arrayref";
109 2 100       54 @$value % 2 == 0 or die "DEFAULT have wrong member";
110             }
111 4         17 $config->{$key} = $value;
112             }
113 4         9 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 1892 my($key, @param) = @_;
127 177 100       345 if (@param % 2) {
128 20 100       39 my $default = ref $param[0] eq 'CODE' ? 'action' : 'spec';
129 20         40 unshift @param, $default;
130             }
131 177 100       344 my @name = ref $key eq 'ARRAY' ? @$key : $key;
132 177         242 my $caller = caller;
133 177         270 my $member = __Member__($caller);
134 177         266 for my $name (@name) {
135 203         323 my $append = $name =~ s/^\+//;
136 203     1466   482 my $i = first { $member->[$_][0] eq $name } 0 .. $#{$member};
  1466         1648  
  203         563  
137 203 100       493 if ($append) {
138 23 100       102 defined $i or die "$name: Not found\n";
139 22         28 push @{$member->[$i]}, @param;
  22         59  
140             } else {
141 180 50       306 defined $i and die "$name: Duplicated\n";
142 180         255 my $config = __Config__($caller);
143 180         231 push @$member, [ $name, @{$config->{DEFAULT}}, @param ];
  180         586  
144             }
145             }
146             }
147              
148             sub new {
149 14     14 1 5439 my $class = shift;
150 14         36 my $obj = bless {}, $class;
151 14 100       49 my $ctx = $class ne __PACKAGE__ ? $class : caller;
152 14         37 my $master = __Member__($ctx);
153 14         71 my $member = $obj->{__Member__} = [];
154 14         27 my $config = $obj->{__Config__} = { %{__Config__($ctx)} }; # make copy
  14         29  
155 14         43 for my $m (@$master) {
156 128         382 my($name, %param) = @$m;
157 128         252 push @$member, [ $name => \%param ];
158 128 100       228 if (my $is = $param{is}) {
159 16     16   112 no strict 'refs';
  16         36  
  16         4384  
160 40         80 my $sub = "$class\::" . $config->{ACCESSOR_PREFIX} . $name;
161 40 50       141 if (not defined &$sub) {
162 40 100 100     78 $is = 'lv' if $is eq 'rw' && $config->{ACCESSOR_LVALUE};
163 40         59 *$sub = _accessor($is, $name);
164             }
165             }
166 128         210 $obj->{$name} = do {
167 128         174 local $_ = $param{default};
168 128 100       273 if (ref eq 'ARRAY') { [ @$_ ] }
  8 100       25  
    100          
169 5         13 elsif (ref eq 'HASH' ) { ({ %$_ }) }
170 4         13 elsif (ref eq 'CODE' ) { $_->() }
171 111         300 else { $_ }
172             };
173             }
174 14 50       87 lock_keys %$obj if $config->{LOCK_KEYS};
175 14         215 $obj;
176             }
177              
178             sub optspec {
179 12     12 1 34 my $obj = shift;
180 12         26 map $obj->_opt_pair($_), @{$obj->_member};
  12         35  
181             }
182              
183             sub getopt {
184 8     8 1 62 my $obj = shift;
185 8 100 33     30 if (@_ == 0) {
    50          
186 7         37 my $getopt = caller . "::" . $obj->_conf->{GETOPT};
187 16     16   113 no strict 'refs';
  16         37  
  16         1076  
188 7         38 $getopt->($obj->optspec());
189             }
190             elsif (@_ == 1 and ref $_[0] eq 'ARRAY') {
191 1         5 my $getopt = caller . "::" . $obj->_conf->{GETOPT_FROM_ARRAY};
192 16     16   97 no strict 'refs';
  16         31  
  16         21022  
193 1         3 $getopt->($_[0], $obj->optspec());
194             }
195             else {
196 0         0 die "getopt: wrong parameter.";
197             }
198             }
199              
200             sub use_keys {
201 0     0 1 0 my $obj = shift;
202 0         0 unlock_keys %$obj;
203 0         0 lock_keys_plus %$obj, @_;
204             }
205              
206 51     51   237 sub _conf { $_[0]->{__Config__} }
207              
208 12     12   49 sub _member { $_[0]->{__Member__} }
209              
210             sub _accessor {
211 40     40   66 my($is, $name) = @_;
212             {
213             ro => sub {
214 28 100   28   183 @_ > 1 and die "$name is readonly\n";
215 27         110 $_[0]->{$name};
216             },
217             rw => sub {
218 3 100   3   18 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         2  
  1         2  
219 2         10 $_[0]->{$name};
220             },
221             lv => sub :lvalue {
222 7 100   7   23 @_ > 1 and do { $_[0]->{$name} = $_[1]; return $_[0] };
  1         2  
  1         3  
223 6         20 $_[0]->{$name};
224             },
225 40 50       338 }->{$is} or die "$name has invalid 'is' parameter.\n";
226             }
227              
228             sub _opt_pair {
229 123     123   159 my $obj = shift;
230 123         133 my $member = shift;
231 123   100     203 my $spec_str = $obj->_opt_str($member) // return ();
232 116         262 ( $spec_str => $obj->_opt_dest($member) );
233             }
234              
235             sub _opt_str {
236 123     123   144 my $obj = shift;
237 123         130 my($name, $m) = @{+shift};
  123         179  
238              
239 123 100       223 $name eq '<>' and return $name;
240 120   100     227 my $spec = $m->{spec} // return undef;
241 113 100       197 if (my $alias = $m->{alias}) {
242 4         13 $spec .= " $alias";
243             }
244 113         182 $obj->_compile($name, $spec);
245             }
246              
247             sub _compile {
248 113     113   137 my $obj = shift;
249 113         153 my($name, $args) = @_;
250 113         225 my @args = split ' ', $args;
251 113         289 my $spec_re = qr/[!+=:]/;
252 113         462 my @spec = grep /$spec_re/, @args;
253 113         331 my @alias = grep !/$spec_re/, @args;
254 113         140 my $spec = do {
255 113 100       232 if (@spec == 0) { '' }
  23 50       36  
256 90         150 elsif (@spec == 1) { $spec[0] }
257 0         0 else { die }
258             };
259 113         172 my @names = ($name, @alias);
260 113         176 for ($name, @alias) {
261 133 50 66     283 push @names, tr[_][-]r if /_/ && $obj->_conf->{REPLACE_UNDERSCORE};
262 133 100 100     289 push @names, tr[_][]dr if /_/ && $obj->_conf->{REMOVE_UNDERSCORE};
263             }
264 113 100 66     855 push @names, '' if @names and $spec !~ /^($spec_re|$)/;
265 113         525 join('|', @names) . $spec;
266             }
267              
268             sub _opt_dest {
269 116     116   146 my $obj = shift;
270 116         131 my($name, $m) = @{+shift};
  116         191  
271              
272 116         177 my $action = $m->{action};
273 116 100       176 if (my $is_valid = _validator($m)) {
    100          
274 26   100     88 $action ||= \&_generic_setter;
275             sub {
276 32     32   6597 local $_ = $obj;
277 32 100       52 &$is_valid or die &{$obj->_conf->{INVALID_MSG}};
  9         44  
278 23         45 &$action;
279 26         167 };
280             }
281             elsif ($action) {
282 14     20   143 sub { &$action for $obj };
  20         10693  
283             }
284             else {
285 76 50       152 if (ref $obj->{$name} eq 'CODE') {
286 0     0   0 sub { &{$obj->{$name}} for $obj };
  0         0  
  0         0  
287             } else {
288 76         231 \$obj->{$name};
289             }
290             }
291             }
292              
293             my %tester = (
294             min => sub { $_[-1] >= $_->{min} },
295             max => sub { $_[-1] <= $_->{max} },
296             must => sub {
297             my $must = $_->{must};
298             for $_ (ref($must) eq 'ARRAY' ? @$must : $must) {
299             return 0 if not &$_;
300             }
301             return 1;
302             },
303             any => sub {
304             my $any = $_->{any};
305             for (ref($any) eq 'ARRAY' ? @$any : $any) {
306             if (ref eq 'Regexp') {
307             return 1 if $_[-1] =~ $_;
308             } elsif (ref eq 'CODE') {
309             return 1 if &$_;
310             } else {
311             return 1 if $_[-1] eq $_;
312             }
313             }
314             return 0;
315             },
316             );
317              
318             sub _tester {
319 116     116   129 my $m = shift;
320 116         257 map $tester{$_}, grep { defined $m->{$_} } keys %tester;
  464         985  
321             }
322              
323             sub _validator {
324 116     116   134 my $m = shift;
325 116 100       163 my @test = _tester($m) or return undef;
326             sub {
327 32     32   38 local $_ = $m;
328 32         48 for my $test (@test) {
329 36 100       65 return 0 if not &$test;
330             }
331 23         75 return 1;
332             }
333 26         111 }
334              
335             sub _generic_setter {
336 20     20   110 my $dest = $_->{$_[0]};
337 7         64 (ref $dest eq 'ARRAY') ? do { push @$dest, $_[1] } :
338 6         20 (ref $dest eq 'HASH' ) ? do { $dest->{$_[1]} = $_[2] }
339 20 100       121 : do { $_->{$_[0]} = $_[1] };
  7 100       33  
340             }
341              
342             sub _invalid_msg {
343 9     9   11 my $opt = do {
344 9         20 $_[0] = $_[0] =~ tr[_][-]r;
345 9 100       66 if (@_ <= 2) {
346 7         42 '--' . join '=', @_;
347             } else {
348 2         17 sprintf "--%s %s=%s", @_[0..2];
349             }
350             };
351 9         39 "$opt: option validation error\n";
352             }
353              
354             1;
355              
356             __END__