File Coverage

blib/lib/Getopt/EX/Hashed.pm
Criterion Covered Total %
statement 199 213 93.4
branch 74 86 86.0
condition 23 28 82.1
subroutine 40 43 93.0
pod 7 7 100.0
total 343 377 90.9


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