File Coverage

blib/lib/CXC/Form/Tiny/Plugin/OptArgs2/Meta.pm
Criterion Covered Total %
statement 172 185 92.9
branch 29 40 72.5
condition 18 33 54.5
subroutine 33 35 94.2
pod 3 3 100.0
total 255 296 86.1


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 5     5   5115 use v5.20;
  5         22  
6              
7 5     5   28 use warnings;
  5         11  
  5         204  
8              
9             our $VERSION = '0.11';
10              
11 5     5   2273 use Clone ();
  5         12421  
  5         167  
12 5     5   36 use Scalar::Util qw( blessed );
  5         17  
  5         322  
13 5     5   1549 use Ref::Util qw( is_plain_hashref is_arrayref is_regexpref is_ref );
  5         1837  
  5         363  
14 5     5   36 use Form::Tiny::Utils 'get_package_form_meta';
  5         13  
  5         396  
15             use Types::Standard
16 5     5   39 qw( ArrayRef Bool CodeRef Dict Enum Int Optional RegexpRef Str Tuple Undef Value );
  5         12  
  5         81  
17 5     5   23745 use Type::Params qw( signature_for );
  5         22105  
  5         65  
18 5     5   1762 use Types::Common::String qw ( NonEmptySimpleStr NonEmptyStr );
  5         14  
  5         59  
19              
20 5     5   5865 use Moo::Role;
  5         14  
  5         46  
21              
22 5     5   2548 use experimental 'signatures', 'postderef', 'lexical_subs';
  5         35  
  5         46  
23              
24 5     5   3776 use namespace::clean;
  5         60932  
  5         36  
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   516 builder => sub { !!1 },
54             );
55              
56              
57              
58              
59              
60              
61              
62             has inherit_optargs => (
63             is => 'rwp',
64             isa => Bool,
65 13     13   58487 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   14717 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   582 builder => sub { '_' },
94             );
95              
96              
97              
98              
99              
100              
101              
102              
103              
104              
105              
106              
107 10         19 has _optargs => (
108             is => 'rwp',
109             lazy => 1,
110             init_arg => undef,
111             ## no critic (Subroutines::ProtectPrivateSubs )
112 10     10   168 builder => sub ( $self ) { $self->_build_opt_args->_optargs },
  10         40  
  10         47  
113             );
114              
115 10     10 1 202 sub optargs ( $self ) {
  10         22  
  10         19  
116 10         258 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 6 sub rename_options ( $self, $opt ) {
  2         8  
  2         4  
  2         3  
136 2         53 my $rename = $self->rename;
137 2         33 for my $from ( keys $opt->%* ) {
138 20         41 my $to = $rename->{$from};
139 20 50       43 croak( "unexpected option key: $from\n" )
140             if !defined $to;
141 20         51 $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 69 sub inflate_optargs ( $self, $optargs ) {
  2         7  
  2         6  
  2         5  
163              
164 2         8 state $folder = do {
165 2         18 require Hash::Fold;
166 2         23 Hash::Fold->new( delimiter => chr( 0 ) );
167             };
168              
169             # make a copy of the flattened hash
170 2         6863 my %flat = $optargs->%*;
171              
172             # translate the OptArgs names into that required by the Form::Tiny structure
173 2         22 $self->rename_options( \%flat );
174              
175 2         13 return $folder->unfold( \%flat );
176             }
177              
178              
179 10     10   18 sub _build_opt_args ( $self ) {
  10         20  
  10         18  
180 10         24 my %rename;
181              
182             my @optargs;
183 10         49 for my $aref ( $self->_create_options( \%rename )->@* ) {
184 51         109 my ( $name, $spec ) = $aref->@*;
185 51         218 my %spec = $spec->%*;
186 51         101 delete $spec{order};
187 51         129 push @optargs, $name, \%spec;
188             }
189              
190 10         93 $self->_set__optargs( \@optargs );
191 10         51 $self->_set_rename( \%rename );
192 10         240 return $self;
193             }
194              
195 18     18   32 my sub _match_inherit_optargs ( $matches, $package ) {
  18         25  
  18         28  
  18         25  
196              
197 18         27 my $excluded = 0;
198              
199 18         34 for my $match ( $matches->@* ) {
200 26         52 my ( $retval, $qr ) = $match->@*;
201 26 100       149 return $retval if $package =~ $qr;
202 16 100       43 $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         38 return $excluded != 0;
210             }
211              
212 88     88   167 sub _inherit_optargs ( $self, $package ) {
  88         145  
  88         131  
  88         133  
213              
214 88   66     548 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             # this has too many arguments
222             sub _create_options (
223 27         41 $self, $rename,
  27         44  
224 27         50 $path = [],
225 27         40 $opt_path = [],
226 27         129 $blueprint = $self->blueprint( recurse => 0 ),
227 27     27   180 )
  27         13295  
228             {
229 27         53 my @optargs;
230              
231 27         148 for my $field ( sort keys $blueprint->%* ) {
232              
233 92         203 my $def = $blueprint->{$field};
234              
235 92 100 100     1750 if ( is_plain_hashref( $def ) || ( my $is_subform = $def->is_subform ) ) {
236              
237             # Normally a sub-form's options get a prefix based on the field name, e.g.
238             # db.opts => --db-opts. Sometimes the extra levels are overkill for the option names,
239             # so if the options entry contains 'name' specification, use that for the prefix.
240             # unfortunately if the field name is nested, we only get here at the bottom of the
241             # hierarchy, so need to backtrack.
242              
243 23         239 my @paths = ( [ $path->@*, $field ], [ $opt_path->@*, $field ] );
244              
245 23 100       58 if ( $is_subform ) {
246              
247 19   33     57 my $addons = $def->addons->{ +__PACKAGE__ } // croak( 'no addons for field ' . $def->name );
248              
249             # bail if we're not inheriting
250 19 100       46 next unless $self->_inherit_optargs( $addons->{package} );
251              
252 13 50 50     93 if ( defined( my $name = ( $addons->{optargs} // {} )->{name} ) ) {
253              
254             ## no critic (ControlStructures::ProhibitDeepNests)
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         99 push @optargs, get_package_form_meta( blessed $def->type )->_create_options( $rename, @paths )->@*;
267             }
268              
269             else {
270 4         34 push @optargs, $self->_create_options( $rename, @paths, $def )->@*;
271             }
272              
273             }
274             else {
275 69   33     761 my $addons = $def->addons->{ +__PACKAGE__ } // croak( 'no addons for field ' . $def->name );
276 69 50       171 next unless defined( my $orig_optargs = $addons->{optargs} );
277              
278             croak( "optargs initialized, but no option or argument specification for field $field?" )
279 69 50       150 if !defined $orig_optargs->{spec};
280              
281 69         114 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 69         1089 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 69         607 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 69   66     337 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 69 50       191 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 69         241 $rename->{$fq_option_name} = $fq_field_name;
317              
318             $optargs->{default} = $def->default->()
319 69 100 66     223 if $optargs->{show_default} && $def->has_default;
320              
321             push @optargs, [ $fq_option_name, $optargs ]
322 69 100       194 if $self->_inherit_optargs( $addons->{package} );
323             }
324             }
325              
326             ## no critic (BuiltinFunctions::RequireSimpleSortBlock)
327             return [
328             # no order, pass 'em through
329 73         184 ( 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         85 grep { defined $_->[1]{order} } @optargs
  73         250  
341             ) ];
342             }
343              
344 34     34   67 sub _add_optarg ( $self, $field, $spec ) {
  34         58  
  34         55  
  34         64  
  34         55  
345 34   50     140 my $stash = $field->addons->{ +__PACKAGE__ } //= {};
346 34   50     158 my $optargs = ( $stash->{optargs} //= {} );
347             croak( sprintf( 'duplicate definition for field %s', $field->name ) )
348 34 50       97 if defined $optargs->{spec};
349              
350 34 100 66     252 $spec->{required} //= !!$field->required
351             if $self->inherit_required;
352 34         126 $optargs->{spec} = $spec;
353 34         177 return;
354             }
355              
356 5     5   13065 use constant OptionTypeEnums => qw( ArrayRef Flag Bool Counter HashRef Int Num Str );
  5         18  
  5         631  
357 5     5   57 use constant OptionTypeMap => { map { $_ => "--$_" } OptionTypeEnums };
  5         12  
  5         18  
  40         863  
358              
359             use constant OptionType => Enum( [ values OptionTypeMap->%* ] )
360 5 50   5   43 ->plus_coercions( NonEmptySimpleStr, sub { /^--/ ? $_ : "--$_" } );
  5         12  
  5         48  
  17         54378  
361              
362 5     5   26339 use constant ArgumentTypeEnums => qw( ArrayRef HashRef Int Num Str SubCmd );
  5         10  
  5         357  
363 5     5   34 use constant ArgumentType => Enum [ArgumentTypeEnums];
  5         22  
  5         31  
364 5     5   16739 use constant ArgumentTypeMap => { map { $_ => $_ } ArgumentTypeEnums() };
  5         10  
  5         23  
  30         3446  
365              
366 15     15   28 sub _resolve_type ( $field, $type_set ) {
  15         26  
  15         25  
  15         23  
367              
368             # dynamic fields don't have types
369             return undef
370 15 50 33     140 unless defined $field
      33        
371             && $field->isa( 'Form::Tiny::FieldDefinition' )
372             && $field->has_type;
373              
374 15         38 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 15 100       52 if index( '|Any|Path|File|Dir|', q{|} . $type->name . q{|} ) != -1;
382              
383 9         90 while ( defined $type ) {
384 15 100       86 return $type_set->{ $type->name } if exists $type_set->{ $type->name };
385 6         56 $type = $type->parent;
386             }
387              
388 0           return undef;
389             }
390              
391             signature_for _dsl_add_option => (
392             method => 1,
393             head => 1, # field context
394             bless => !!0,
395             named => [
396             name => Optional [NonEmptySimpleStr],
397             alias => Optional [NonEmptySimpleStr],
398             comment => NonEmptySimpleStr,
399             default => Optional [ Value | CodeRef ],
400             required => Optional [Bool],
401             hidden => Optional [Bool],
402             isa => Optional [OptionType],
403             isa_name => Optional [NonEmptySimpleStr],
404             show_default => Optional [Bool],
405             trigger => Optional [CodeRef],
406             ],
407             );
408             sub _dsl_add_option ( $self, $context, $spec ) {
409             croak( q{The 'option' directive must be used after a field definition} )
410             if !defined( $context );
411             my %spec = $spec->%*;
412             $spec{isa} //= _resolve_type( $context, OptionTypeMap )
413             // croak( sprintf( q{'isa' attribute not specified or resolved for %s}, $context->name ) );
414             $self->_add_optarg( $context, \%spec );
415             }
416              
417             signature_for _dsl_add_argument => (
418             method => 1,
419             head => 1,
420             bless => !!0,
421             named => [
422             name => Optional [NonEmptySimpleStr],
423             comment => NonEmptySimpleStr,
424             default => Optional [ Value | CodeRef ],
425             greedy => Optional [Bool],
426             fallthru => Optional [Bool],
427             isa => Optional [ArgumentType],
428             isa_name => Optional [NonEmptySimpleStr],
429             required => Optional [Bool],
430             show_default => Optional [Bool],
431             order => Int,
432             ],
433             );
434             sub _dsl_add_argument ( $self, $context, $spec ) {
435             croak( q{The 'argument' directive must be used after a field definition} )
436             if !defined( $context );
437             my %spec = $spec->%*;
438             $spec{isa} //= _resolve_type( $context, ArgumentTypeMap )
439             // croak( sprintf( q{'isa' attribute not specified or resolved for %s}, $context->name ) );
440             $self->_add_optarg( $context, \%spec );
441             }
442              
443 5     5   45 use constant { INCLUDE => q{+}, EXCLUDE => q{-} };
  5         14  
  5         3254  
444              
445             my sub parse_inherit_matches ( $default, $entries ) {
446              
447             my @matches;
448             my $include = $default;
449             for my $entry ( $entries->@* ) {
450              
451             if ( is_arrayref( $entry ) ) {
452             push @matches, __SUB__->( $include, $entry )->@*;
453             }
454              
455             elsif ( is_regexpref( $entry ) ) {
456             push @matches, [ $include eq INCLUDE, $entry ];
457             }
458              
459             elsif ( $entry eq EXCLUDE || $entry eq EXCLUDE ) {
460             $include = $entry;
461             next; # avoid reset of $include to default below
462             }
463              
464             # every thing else is a regexp as a string; turn into a regexp
465             else {
466             push @matches, [ $include eq INCLUDE, qr/$entry/ ];
467             }
468              
469             # reset include to default
470             $include = $default;
471             }
472              
473             return \@matches;
474             }
475              
476              
477             signature_for _dsl_optargs_opts => (
478             method => 1,
479             head => 1,
480             named => [
481             inherit_required => Optional [Bool],
482             inherit_optargs => Optional [Bool],
483             inherit_optargs_match => Optional [ArrayRef],
484             nested_path_sep => Optional [Str],
485             ],
486             );
487             sub _dsl_optargs_opts ( $self, $context, $args ) {
488              
489             croak( q{The 'optargs_opts' directive must be used before any fields are defined} )
490             if defined( $context );
491              
492             $self->_set_inherit_required( $args->inherit_required )
493             if $args->has_inherit_required;
494              
495             $self->_set_inherit_optargs( $args->inherit_optargs )
496             if $args->has_inherit_optargs;
497              
498             if ( $args->has_inherit_optargs_match ) {
499             my $match = $args->inherit_optargs_match;
500             $match = [$match] unless is_arrayref( $match );
501              
502             my $matches = parse_inherit_matches( INCLUDE, $match );
503             $self->_set_inherit_optargs_match( $matches );
504             }
505              
506             $self->_set_nested_path_sep( $args->nested_path_sep )
507             if $args->has_nested_path_sep;
508              
509              
510             }
511              
512             #
513             # This file is part of CXC-Form-Tiny-Plugin-OptArgs2
514             #
515             # This software is Copyright (c) 2023 by Smithsonian Astrophysical Observatory.
516             #
517             # This is free software, licensed under:
518             #
519             # The GNU General Public License, Version 3, June 2007
520             #
521              
522             1;
523              
524             __END__