File Coverage

blib/lib/CXC/Form/Tiny/Plugin/OptArgs2/Meta.pm
Criterion Covered Total %
statement 173 186 93.0
branch 31 42 73.8
condition 17 33 51.5
subroutine 33 35 94.2
pod 3 3 100.0
total 257 299 85.9


line stmt bran cond sub pod time code
1             package CXC::Form::Tiny::Plugin::OptArgs2::Meta;
2              
3             # ABSTRACT: Form metaclass role for OptArgs2
4              
5 6     6   1852088 use v5.20;
  6         33  
6              
7 6     6   39 use warnings;
  6         12  
  6         617  
8              
9             our $VERSION = '0.12';
10              
11 6     6   3160 use Clone ();
  6         3327  
  6         241  
12 6     6   42 use Scalar::Util qw( blessed );
  6         11  
  6         482  
13 6     6   2159 use Ref::Util qw( is_plain_hashref is_arrayref is_regexpref is_ref );
  6         5223  
  6         573  
14 6     6   710 use Form::Tiny::Utils 'get_package_form_meta';
  6         1859  
  6         422  
15             use Types::Standard
16 6     6   766 qw( ArrayRef Bool CodeRef Dict Enum Int Optional RegexpRef Str Tuple Undef Value );
  6         169816  
  6         98  
17 6     6   32170 use Type::Params qw( signature_for );
  6         29368  
  6         74  
18 6     6   3097 use Types::Common::String qw ( NonEmptySimpleStr NonEmptyStr );
  6         58884  
  6         86  
19              
20 6     6   9216 use Moo::Role;
  6         17207  
  6         64  
21              
22 6     6   5824 use experimental 'signatures', 'postderef', 'lexical_subs';
  6         1412  
  6         56  
23              
24 6     6   4672 use namespace::clean;
  6         95587  
  6         51  
25              
26             my sub croak {
27 0     0   0 require Carp;
28 0         0 goto \&Carp::croak;
29             }
30              
31             # need to stash which form this field was added to in order to handle
32             # inheritance of inherited fields which aren't options, but which
33             # contain nested forms which *are* options.
34              
35             around add_field => sub ( $orig, $self, @parameters ) {
36             # this may return either a FieldDefinition or a FieldDefinition, but
37             # in either case, it has an addons methods.
38             my $field = $self->$orig( @parameters );
39             $field->addons->{ +__PACKAGE__ }{package} = $self->package;
40             return $field;
41             };
42              
43              
44              
45              
46              
47              
48              
49              
50             has inherit_required => (
51             is => 'rwp',
52             isa => Bool,
53 13     13   44741 builder => sub { !!1 },
54             );
55              
56              
57              
58              
59              
60              
61              
62             has inherit_optargs => (
63             is => 'rwp',
64             isa => Bool,
65 13     13   790 builder => sub { !!0 },
66             );
67              
68              
69              
70              
71              
72              
73              
74              
75              
76             has inherit_optargs_match => (
77             is => 'rwp',
78             isa => Undef | ArrayRef [ Tuple [ Bool, RegexpRef ] ],
79 15     15   16981 builder => sub { undef },
80             );
81              
82              
83              
84              
85              
86              
87              
88              
89              
90              
91             has nested_path_sep => (
92             is => 'rwp',
93 15     15   548 builder => sub { '_' },
94             );
95              
96              
97              
98              
99              
100              
101              
102              
103              
104              
105              
106              
107 10         20 has _optargs => (
108             is => 'rwp',
109             lazy => 1,
110             init_arg => undef,
111             ## no critic (Subroutines::ProtectPrivateSubs )
112 10     10   114 builder => sub ( $self ) { $self->_build_opt_args->_optargs },
  10         59  
  10         43  
113             );
114              
115 10     10 1 195 sub optargs ( $self ) {
  10         20  
  10         22  
116 10         360 return Clone::clone( $self->_optargs );
117             }
118              
119 0         0 has rename => (
120             is => 'rwp',
121             lazy => 1,
122             init_arg => undef,
123 0     0   0 builder => sub ( $self ) { $self->_build_opt_args->rename },
  0         0  
  0         0  
124             );
125              
126              
127              
128              
129              
130              
131              
132              
133              
134              
135 2     2 1 5 sub rename_options ( $self, $opt ) {
  2         4  
  2         3  
  2         4  
136 2         37 my $rename = $self->rename;
137 2         22 for my $from ( keys $opt->%* ) {
138 20         32 my $to = $rename->{$from};
139 20 50       33 croak( "unexpected option key: $from\n" )
140             if !defined $to;
141 20         40 $opt->{$to} = delete $opt->{$from};
142             }
143             }
144              
145              
146              
147              
148              
149              
150              
151              
152              
153              
154              
155              
156              
157              
158              
159              
160              
161              
162 2     2 1 50 sub inflate_optargs ( $self, $optargs ) {
  2         6  
  2         3  
  2         4  
163              
164 2         6 state $folder = do {
165 2         69 require Hash::Fold;
166 2         23 Hash::Fold->new( delimiter => chr( 0 ) );
167             };
168              
169             # make a copy of the flattened hash
170 2         6593 my %flat = $optargs->%*;
171              
172             # translate the OptArgs names into that required by the Form::Tiny structure
173 2         14 $self->rename_options( \%flat );
174              
175 2         10 return $folder->unfold( \%flat );
176             }
177              
178              
179 10     10   53 sub _build_opt_args ( $self ) {
  10         21  
  10         15  
180 10         33 my %rename;
181              
182             my @optargs;
183 10         43 for my $aref ( $self->_create_options( \%rename )->@* ) {
184 52         121 my ( $name, $spec ) = $aref->@*;
185 52         249 my %spec = $spec->%*;
186 52         101 delete $spec{order};
187 52         188 push @optargs, $name, \%spec;
188             }
189              
190 10         80 $self->_set__optargs( \@optargs );
191 10         44 $self->_set_rename( \%rename );
192 10         276 return $self;
193             }
194              
195 18     18   27 my sub _match_inherit_optargs ( $matches, $package ) {
  18         25  
  18         22  
  18         26  
196              
197 18         22 my $excluded = 0;
198              
199 18         35 for my $match ( $matches->@* ) {
200 26         48 my ( $retval, $qr ) = $match->@*;
201 26 100       164 return $retval if $package =~ $qr;
202 16 100       39 $excluded++ unless $retval;
203             }
204              
205             # if no exclusions, then user forgot to add the exclude all
206             # catch-all at the end. just having inclusions doesn't make
207             # sense.
208              
209 8         35 return $excluded != 0;
210             }
211              
212 89     89   129 sub _inherit_optargs ( $self, $package ) {
  89         136  
  89         136  
  89         129  
213              
214 89   66     640 return $package eq $self->package
215             || (
216             $self->inherit_optargs
217             && ( !defined $self->inherit_optargs_match
218             || _match_inherit_optargs( $self->inherit_optargs_match, $package ) ) );
219             }
220              
221             ## no critic( ValuesAndExpressions::ProhibitImplicitNewlines )
222             ## no critic( Subroutines::ProhibitManyArgs )
223             sub _create_options (
224 27         47 $self, $rename,
  27         41  
225 27         81 $path = [],
226 27         45 $opt_path = [],
227 27         208 $blueprint = $self->blueprint( recurse => 0 ),
228 27     27   204 )
  27         17160  
229             {
230 27         101 my @optargs;
231              
232 27         144 for my $field ( sort keys $blueprint->%* ) {
233              
234 93         177 my $def = $blueprint->{$field};
235              
236 93 100 100     2113 if ( is_plain_hashref( $def ) || ( my $is_subform = $def->is_subform ) ) {
237              
238             # Normally a sub-form's options get a prefix based on the field name, e.g.
239             # db.opts => --db-opts. Sometimes the extra levels are overkill for the option names,
240             # so if the options entry contains 'name' specification, use that for the prefix.
241             # unfortunately if the field name is nested, we only get here at the bottom of the
242             # hierarchy, so need to backtrack.
243              
244 23         203 my @paths = ( [ $path->@*, $field ], [ $opt_path->@*, $field ] );
245              
246 23 100       51 if ( $is_subform ) {
247              
248 19   33     69 my $addons = $def->addons->{ +__PACKAGE__ } // croak( 'no addons for field ' . $def->name );
249              
250             # bail if we're not inheriting
251 19 100       58 next unless $self->_inherit_optargs( $addons->{package} );
252              
253 13 50 50     107 if ( defined( my $name = ( $addons->{optargs} // {} )->{name} ) ) {
254              
255 0 0       0 if ( my @fixup = $opt_path->@* ) {
256 0         0 my @comp = split( /[.]/, $def->name );
257 0         0 splice( @fixup, @fixup - @comp, @comp, $name );
258             # replace default opt_path
259 0         0 $paths[-1] = \@fixup;
260             }
261             else {
262 0         0 $paths[-1] = [$name];
263             }
264             }
265              
266 13         113 push @optargs, get_package_form_meta( blessed $def->type )->_create_options( $rename, @paths )->@*;
267             }
268              
269             else {
270 4         42 push @optargs, $self->_create_options( $rename, @paths, $def )->@*;
271             }
272              
273             }
274             else {
275 70   33     759 my $addons = $def->addons->{ +__PACKAGE__ } // croak( 'no addons for field ' . $def->name );
276 70 50       218 next unless defined( my $orig_optargs = $addons->{optargs} );
277              
278             croak( "optargs initialized, but no option or argument specification for field $field?" )
279 70 50       183 if !defined $orig_optargs->{spec};
280              
281 70         148 my $optargs = $orig_optargs->{spec};
282              
283             # This bit deals with creating the option name and then mapping it back onto the
284             # Form::Tiny blueprint for the form, which may introduce extra layers in the
285             # nested hash if the field name has multiple components.
286             # Special cases arise:
287             # 1) multi-component field name, e.g. 'output.parsed'
288             # 2) options name ne field name, e.g. '--raw-output' ne 'output.raw'.
289             # 3) field name has an underscore, which can get confused
290             # when the options are unflattened, as underscore is
291             # used to indicated nested structures
292              
293              
294             # if @path > 1, then a multi-component name was given to form_field.
295             # Form::Tiny doesn't keep track of sub-forms' parents, so it doesn't know
296             # so we keep track of the entire path via $path.
297             # we only need the last component to get the (leaf) form field name.
298 70         1299 my $field_name = $def->get_name_path->path->[-1];
299              
300             # this is the fully qualified normalized field name, with
301             # components separated by NUL and will be used create the
302             # correct hierarchy when the options hash is unflattened.
303 70         611 my $fq_field_name = join( chr( 0 ), $path->@*, $field_name );
304              
305             # generate the fully qualified option name using the
306             # specified field name. the field may specify an
307             # alternate option name, so use that if specified.
308 70   66     1579 my $fq_option_name = $optargs->{name} // join( $self->nested_path_sep, $opt_path->@*, $field_name );
309              
310             # store the mapping between option name and fully
311             # qualified normalized field name.
312              
313 70 50       190 if ( defined( my $old_rename = $rename->{$fq_option_name} ) ) {
314 0         0 croak( "redefined rename of $fq_option_name to $fq_field_name (originally to $old_rename)" );
315             }
316 70         1364 $rename->{$fq_option_name} = $fq_field_name;
317              
318             $optargs->{default} = $def->default->()
319 70 100 66     194 if $optargs->{show_default} && $def->has_default;
320              
321             push @optargs, [ $fq_option_name, $optargs ]
322 70 100       196 if $self->_inherit_optargs( $addons->{package} );
323             }
324             }
325              
326             ## no critic (BuiltinFunctions::RequireSimpleSortBlock)
327             return [
328             # no order, pass 'em through
329 74         175 ( grep { !defined $_->[1]{order} } @optargs ),
330              
331             # order, sort 'em, but complain if multiple arguments with the
332             # same order, as that is not deterministic
333             (
334             sort {
335 2         8 my $order = $a->[1]{order} <=> $b->[1]{order};
336 2 50       9 croak( "$a->[0] and $b->[0] have the same argument order" )
337             if $order == 0;
338             $order;
339             }
340 27         73 grep { defined $_->[1]{order} } @optargs
  74         307  
341             ) ];
342             }
343              
344 35     35   77 sub _add_optarg ( $self, $field, $spec ) {
  35         59  
  35         55  
  35         55  
  35         56  
345 35   50     152 my $stash = $field->addons->{ +__PACKAGE__ } //= {};
346 35   50     197 my $optargs = ( $stash->{optargs} //= {} );
347             croak( sprintf( 'duplicate definition for field %s', $field->name ) )
348 35 50       99 if defined $optargs->{spec};
349              
350 35 100 33     268 $spec->{required} //= !!$field->required
351             if $self->inherit_required;
352 35         216 $optargs->{spec} = $spec;
353 35         450 return;
354             }
355              
356 6     6   18407 use constant OptionTypeEnums => qw( ArrayRef Flag Bool Counter HashRef Int Num Str );
  6         14  
  6         831  
357 6     6   44 use constant OptionTypeMap => { map { $_ => "--$_" } OptionTypeEnums };
  6         13  
  6         20  
  48         1120  
358              
359             use constant OptionType => Enum( [ values OptionTypeMap->%* ] )
360 6 50   6   41 ->plus_coercions( NonEmptySimpleStr, sub { /^--/ ? $_ : "--$_" } );
  6         10  
  6         69  
  17         79197  
361              
362 6     6   55698 use constant ArgumentTypeEnums => qw( ArrayRef HashRef Int Num Str SubCmd );
  6         25  
  6         483  
363 6     6   36 use constant ArgumentType => Enum [ArgumentTypeEnums];
  6         13  
  6         31  
364 6     6   36178 use constant ArgumentTypeMap => { map { $_ => $_ } ArgumentTypeEnums() };
  6         14  
  6         23  
  36         5511  
365              
366 16     16   21 sub _resolve_type ( $field, $type_set ) {
  16         19  
  16         21  
  16         16  
367              
368             # dynamic fields don't have types
369             return undef
370 16 50 33     105 unless defined $field
      33        
371             && $field->isa( 'Form::Tiny::FieldDefinition' )
372             && $field->has_type;
373              
374 16         37 my $type = $field->type;
375              
376             # take care of top level Any. Many other types inherit (eventually) from Any,
377             # so the inheritance scan below will resolve types we don't support
378             # if we add Any to OptionTypeMap and ArgumentTypeMap
379              
380             return $type_set->{Str}
381 16 100       68 if index( '|Any|Path|File|Dir|', q{|} . $type->name . q{|} ) != -1;
382              
383             return $type_set->{Bool}
384 10 100       89 if $type->name eq 'BoolLike';
385              
386 9         42 while ( defined $type ) {
387 15 100       57 return $type_set->{ $type->name } if exists $type_set->{ $type->name };
388 6         36 $type = $type->parent;
389             }
390              
391 0           return undef;
392             }
393              
394             signature_for _dsl_add_option => (
395             method => 1,
396             head => 1, # field context
397             bless => !!0,
398             named => [
399             name => Optional [NonEmptySimpleStr],
400             alias => Optional [NonEmptySimpleStr],
401             comment => NonEmptySimpleStr,
402             default => Optional [ Value | CodeRef ],
403             required => Optional [Bool],
404             hidden => Optional [Bool],
405             isa => Optional [OptionType],
406             isa_name => Optional [NonEmptySimpleStr],
407             show_default => Optional [Bool],
408             trigger => Optional [CodeRef],
409             ],
410             );
411             sub _dsl_add_option ( $self, $context, $spec ) {
412             croak( q{The 'option' directive must be used after a field definition} )
413             if !defined( $context );
414             my %spec = $spec->%*;
415             $spec{isa} //= _resolve_type( $context, OptionTypeMap )
416             // croak( sprintf( q{'isa' attribute not specified or resolved for %s}, $context->name ) );
417             $self->_add_optarg( $context, \%spec );
418             }
419              
420             signature_for _dsl_add_argument => (
421             method => 1,
422             head => 1,
423             bless => !!0,
424             named => [
425             name => Optional [NonEmptySimpleStr],
426             comment => NonEmptySimpleStr,
427             default => Optional [ Value | CodeRef ],
428             greedy => Optional [Bool],
429             fallthru => Optional [Bool],
430             isa => Optional [ArgumentType],
431             isa_name => Optional [NonEmptySimpleStr],
432             required => Optional [Bool],
433             show_default => Optional [Bool],
434             order => Int,
435             ],
436             );
437             sub _dsl_add_argument ( $self, $context, $spec ) {
438             croak( q{The 'argument' directive must be used after a field definition} )
439             if !defined( $context );
440             my %spec = $spec->%*;
441             $spec{isa} //= _resolve_type( $context, ArgumentTypeMap )
442             // croak( sprintf( q{'isa' attribute not specified or resolved for %s}, $context->name ) );
443             $self->_add_optarg( $context, \%spec );
444             }
445              
446 6     6   54 use constant { INCLUDE => q{+}, EXCLUDE => q{-} };
  6         13  
  6         4003  
447              
448             my sub parse_inherit_matches ( $default, $entries ) {
449              
450             my @matches;
451             my $include = $default;
452             for my $entry ( $entries->@* ) {
453              
454             if ( is_arrayref( $entry ) ) {
455             push @matches, __SUB__->( $include, $entry )->@*;
456             }
457              
458             elsif ( is_regexpref( $entry ) ) {
459             push @matches, [ $include eq INCLUDE, $entry ];
460             }
461              
462             elsif ( $entry eq EXCLUDE || $entry eq EXCLUDE ) {
463             $include = $entry;
464             next; # avoid reset of $include to default below
465             }
466              
467             # every thing else is a regexp as a string; turn into a regexp
468             else {
469             push @matches, [ $include eq INCLUDE, qr/$entry/ ];
470             }
471              
472             # reset include to default
473             $include = $default;
474             }
475              
476             return \@matches;
477             }
478              
479              
480             signature_for _dsl_optargs_opts => (
481             method => 1,
482             head => 1,
483             named => [
484             inherit_required => Optional [Bool],
485             inherit_optargs => Optional [Bool],
486             inherit_optargs_match => Optional [ArrayRef],
487             nested_path_sep => Optional [Str],
488             ],
489             );
490             sub _dsl_optargs_opts ( $self, $context, $args ) {
491              
492             croak( q{The 'optargs_opts' directive must be used before any fields are defined} )
493             if defined( $context );
494              
495             $self->_set_inherit_required( $args->inherit_required )
496             if $args->has_inherit_required;
497              
498             $self->_set_inherit_optargs( $args->inherit_optargs )
499             if $args->has_inherit_optargs;
500              
501             if ( $args->has_inherit_optargs_match ) {
502             my $match = $args->inherit_optargs_match;
503             $match = [$match] unless is_arrayref( $match );
504              
505             my $matches = parse_inherit_matches( INCLUDE, $match );
506             $self->_set_inherit_optargs_match( $matches );
507             }
508              
509             $self->_set_nested_path_sep( $args->nested_path_sep )
510             if $args->has_nested_path_sep;
511              
512              
513             }
514              
515             #
516             # This file is part of CXC-Form-Tiny-Plugin-OptArgs2
517             #
518             # This software is Copyright (c) 2023 by Smithsonian Astrophysical Observatory.
519             #
520             # This is free software, licensed under:
521             #
522             # The GNU General Public License, Version 3, June 2007
523             #
524              
525             1;
526              
527             __END__