File Coverage

blib/lib/CXC/Form/Tiny/Plugin/OptArgs2/Meta.pm
Criterion Covered Total %
statement 164 177 92.6
branch 29 40 72.5
condition 18 33 54.5
subroutine 30 32 93.7
pod 2 2 100.0
total 243 284 85.5


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