File Coverage

blib/lib/CXC/Form/Tiny/Plugin/OptArgs2/Meta.pm
Criterion Covered Total %
statement 171 184 92.9
branch 29 40 72.5
condition 18 33 54.5
subroutine 32 34 94.1
pod 3 3 100.0
total 253 294 86.0


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