File Coverage

lib/Getopt/Type/Tiny.pm
Criterion Covered Total %
statement 113 118 95.7
branch 70 82 85.3
condition 18 27 66.6
subroutine 10 10 100.0
pod 1 1 100.0
total 212 238 89.0


line stmt bran cond sub pod time code
1             package Getopt::Type::Tiny;
2              
3             # ABSTRACT: Clean Getopt::Long wrapper with Type::Tiny support
4              
5 2     2   398788 use v5.20.0;
  2         9  
6 2     2   14 use warnings;
  2         4  
  2         136  
7 2     2   1264 use experimental 'signatures';
  2         12265  
  2         17  
8              
9 2         36 use Type::Library -extends => [
10             qw(
11             Types::Standard
12             Types::Common::Numeric
13             Types::Common::String
14             )
15 2     2   3515 ];
  2         109581  
16              
17 2     2   549882 use Getopt::Long 'GetOptionsFromArray';
  2         35935  
  2         14  
18 2     2   2122 use Pod::Usage qw(pod2usage);
  2         164230  
  2         207  
19 2     2   21 use Carp qw(croak);
  2         4  
  2         4717  
20              
21             our $VERSION = '0.03';
22              
23             push our @EXPORT_OK, qw(get_opts);
24              
25 30     30   64 sub _infer_getopt_modifier ( $type, $coerce = 0 ) {
  30         58  
  30         94  
  30         48  
26              
27             # Use display_name since parameterized types have __ANON__ as name
28 30         91 my $name = $type->display_name;
29              
30             # Check if it's ArrayRef or HashRef
31 30 100       267 return unless $name =~ /^(ArrayRef|HashRef)/;
32              
33 21         82 my $container = $1;
34 21 100       121 my $suffix = $container eq 'ArrayRef' ? '@' : '%';
35              
36             # Get inner type parameter
37 21         82 my $param = $type->type_parameter;
38 21 50       191 return unless $param; # Bare ArrayRef/HashRef - will error later
39              
40             # Check ancestry (order matters: Int before Num since Int is subtype of Num)
41 21 50 33     182 my $sigil
    100          
    100          
    100          
    100          
42             = $param->is_a_type_of(Int) ? 'i'
43             : $param->is_a_type_of(Num) ? 'f'
44             : $param->is_a_type_of(Str) ? 's'
45             : $param->equals(Any) ? 's'
46             : $coerce && $param->has_coercion ? 's'
47             : undef;
48              
49 21 50       23626 return unless $sigil;
50 21         130 return "=$sigil$suffix";
51             }
52              
53 34     34   62 sub _validate_multi_value_type ( $name, $type, $coerce = 0 ) {
  34         76  
  34         51  
  34         88  
  34         55  
54              
55             # Use display_name since parameterized types have __ANON__ as name
56 34         241 my $type_name = $type->display_name;
57              
58             # Only validate ArrayRef and HashRef
59 34 100       483 return unless $type_name =~ /^(ArrayRef|HashRef)/;
60              
61 25         75 my $container = $1;
62 25         159 my $param = $type->type_parameter;
63              
64 25 100       425 unless ($param) {
65 2 100       17 my $suffix = $container eq 'ArrayRef' ? '@' : '%';
66 2         59 croak <<"END_ERROR";
67             Unsupported type '$type_name' for option '$name'.
68              
69             ArrayRef and HashRef require a type parameter (e.g., ArrayRef[Str]).
70              
71             To fix this, either:
72             1. Use explicit GetOpt::Long syntax: '$name=s$suffix' => { isa => $type_name }
73             2. Specify the inner type: ArrayRef[Str], ArrayRef[Int], or ArrayRef[Num]
74             END_ERROR
75             }
76              
77 23   100     274 my $is_supported
78             = $param->is_a_type_of(Int)
79             || $param->is_a_type_of(Num)
80             || $param->is_a_type_of(Str)
81             || $param->equals(Any)
82             || $coerce && $param->has_coercion;
83              
84 23 100       36031 unless ($is_supported) {
85 2 100       10 my $suffix = $container eq 'ArrayRef' ? '@' : '%';
86 2         42 croak <<"END_ERROR";
87             Unsupported type '$type_name' for option '$name'.
88              
89             GetOpt::Long only supports ArrayRef and HashRef with inner types that are
90             subtypes of Str, Int, or Num.
91              
92             To fix this, either:
93             1. Use explicit GetOpt::Long syntax: '$name=s$suffix' => { isa => $type_name }
94             2. Simplify your type to ArrayRef[Str], ArrayRef[Int], or ArrayRef[Num]
95             END_ERROR
96             }
97             }
98              
99 34     34 1 415632 sub get_opts (@arg_specs) {
  34         114  
  34         70  
100              
101             # forces Carp to ignore this package
102 34         140 local $Carp::Internal{ +__PACKAGE__ } = 1;
103 34         328 my %opt_for; # store the options
104             my @getopt_specs; # the option specs
105 34         0 my %defaults; # default values
106 34         0 my %renames; # rename option keys
107 34         0 my %types; # type constraints
108 34         0 my %coerce; # should coerce?
109 34         0 my %required; # required options
110              
111 34         143 while (@arg_specs) {
112 38         84 my $this_getopt_spec = shift @arg_specs;
113 38 100       270 my $options = is_HashRef( $arg_specs[0] ) ? shift @arg_specs : {};
114              
115 38         292 my ($name) = $this_getopt_spec =~ /^([\w_]+)/;
116              
117 38 100       147 if ( exists $options->{isa} ) {
118 34         128 $types{$name} = $options->{isa};
119              
120 34 50 66     173 if ( $options->{coerce} && !$types{$name}->has_coercion ) {
121 0         0 croak("Cannot coerce to a type without a coercion");
122             }
123              
124             # Validate multi-value types (croaks if unsupported)
125             _validate_multi_value_type(
126             $name, $types{$name},
127             $options->{coerce}
128 34         11936 );
129              
130             # Check for multi-value type inference
131             my $inferred_modifier
132 30         213 = _infer_getopt_modifier( $types{$name}, $options->{coerce} );
133              
134 30 100 33     135 if ($inferred_modifier) {
    50          
135              
136             # Multi-value type detected
137 21 100       104 if ( $this_getopt_spec =~ /=[a-z][@%]$/ ) {
138              
139             # Explicit modifier exists - check for mismatch
140 6         36 my ($explicit_modifier) = $this_getopt_spec =~ /(=[a-z][@%])$/;
141 6 100 100     41 if ( $explicit_modifier ne $inferred_modifier
142             && !$options->{nowarn} )
143             {
144             warn "Option '$name' has explicit spec '$explicit_modifier' but type '"
145 1         8 . $types{$name}->display_name
146             . "' suggests '$inferred_modifier'.\n"
147             . "Type::Tiny will still validate the values. "
148             . "Use 'nowarn => 1' to suppress this warning.\n";
149             }
150             }
151             else {
152             # No explicit modifier - append inferred one
153 15         42 $this_getopt_spec .= $inferred_modifier;
154             }
155             }
156             elsif ($types{$name}->name ne 'Bool'
157             && $this_getopt_spec !~ /=/ )
158             {
159              
160             # Non-Bool scalar type without modifier
161 9 50       140 if ( $this_getopt_spec =~ s/=[a-z]$/=s/ ) {
162              
163             # Replaced explicit type with =s
164             }
165             else {
166 9         30 $this_getopt_spec .= '=s';
167             }
168             }
169             }
170 34         138 push @getopt_specs, $this_getopt_spec;
171              
172 34 50 66     143 if ( exists $options->{default} && exists $options->{required} ) {
173 0         0 croak("Option '$name' cannot be both required and have a default value");
174             }
175 34 100       116 if ( exists $options->{default} ) {
    100          
176 6         20 $defaults{$name} = $options->{default};
177             }
178             elsif ( exists $options->{isa} ) {
179              
180             # Auto-default for ArrayRef and HashRef
181 25         101 my $type_name = $options->{isa}->display_name;
182 25 100       188 if ( $type_name =~ /^ArrayRef/ ) {
    100          
183 14         56 $defaults{$name} = [];
184             }
185             elsif ( $type_name =~ /^HashRef/ ) {
186 4         11 $defaults{$name} = {};
187             }
188             }
189 34 100       144 $renames{$name} = $options->{rename} if exists $options->{rename};
190             $required{$name} = $options->{required}
191 34 100       120 if exists $options->{required};
192 34 100       91 $coerce{$name} = $options->{coerce} if exists $options->{coerce};
193              
194             # If no type is specified and the option doesn't have =s or =i,
195             # assume Bool
196 34 100 33     228 $types{$name} //= Bool unless $this_getopt_spec =~ /=/;
197             }
198              
199             # this has proven so incredibly useful that it's now a default,
200             # but perhaps it should be optional. Will need to think of the cleanest
201             # interface for that.
202 30         154 push @getopt_specs, 'help|?';
203 30         62 push @getopt_specs, 'man';
204              
205             # Note: this will mutate @ARGV if any options are present
206 30         417 my $result = GetOptionsFromArray( \@ARGV, \%opt_for, @getopt_specs );
207              
208 30 50       17079 if ( $opt_for{help} ) {
209 0         0 pod2usage(1);
210             }
211              
212 30 50       114 if ( $opt_for{man} ) {
213 0         0 pod2usage( -exitval => 0, -verbose => 2 );
214             }
215              
216 30 50       84 unless ($result) {
217 0         0 croak( pod2usage(2) );
218             }
219              
220             # Apply defaults
221 30         120 for my $name ( keys %defaults ) {
222 24 100       94 next if exists $opt_for{$name};
223             $opt_for{$name}
224             = is_CodeRef( $defaults{$name} )
225             ? $defaults{$name}->()
226 8 100       54 : $defaults{$name};
227             }
228              
229             # Type checking
230 30         101 for my $name ( keys %types ) {
231 34         109 my $type = $types{$name};
232 34         62 my $coerce = $coerce{$name};
233 34 100 100     227 if ( exists $opt_for{$name} && !$type->check( $opt_for{$name} ) ) {
234 4 100       62 if ($coerce) {
235 2         16 my $new_val = $type->coerce( $opt_for{$name} );
236 2 50       2362 if ( $type->check($new_val) ) {
237 2         29 $opt_for{$name} = $new_val;
238 2         6 next;
239             }
240             }
241 2         16 croak( "Invalid value for option '$name': " . $type->get_message( $opt_for{$name} ) );
242             }
243             }
244              
245             # Check required options
246 28         2540 for my $name ( keys %required ) {
247 2 100 66     16 if ( $required{$name} && !exists $opt_for{$name} ) {
248 1         30 croak("Required option '$name' is missing");
249             }
250             }
251              
252             # Rename keys
253 27         95 for my $name ( keys %renames ) {
254             $opt_for{ $renames{$name} } = delete $opt_for{$name}
255 1 50       12 if exists $opt_for{$name};
256             }
257              
258 27         266 return %opt_for;
259             }
260             1;
261              
262             __END__