NAME

CXC::Form::Tiny::Plugin::OptArgs2 - A Plugin to interface Form::Tiny with OptArgs2

VERSION

version 0.12

SYNOPSIS

package My::Form {

    use Form::Tiny plugins => ['+CXC::Form::Tiny::Plugin::OptArgs2'];

    use Types::Standard       qw( ArrayRef HashRef Str );
    use Types::Common::String qw( NonEmptyStr );
    use Types::Path::Tiny     qw( Path );

    # configure plugin; these are the default values
    optargs_opts( inherit_required => !!1, inherit_optargs => !!0 );

    form_field 'file' => ( type => NonEmptyStr, default => sub { 'file.ext' } );

    # the 'option' keyword immediately follows the field definition
    option(
        isa      => 'Str',               # optional, can usually guess from the field
        comment  => 'Query in a file',
        isa_name => 'ADQL in a file',
    );

    # arguments can appear in any order; use the 'order' attribute to
    # specify their order on the command line

    form_field 'arg2' => ( type => ArrayRef, );
    argument(
        isa     => 'ArrayRef',           # optional, can guess from the field
        comment => 'every thing else',
        greedy  => 1,
        order   => 2,                    # this is the second argument on the command line
    );

    form_field 'arg1' => ( type => Path, coerce => 1 );
    argument(
        isa     => 'Str',                # not optional, can't guess from the field
        comment => 'first argument',
        order   => 1,                    # this is the second argument on the command line
    );

}

use OptArgs2;
use Data::Dumper;

# create form
my $form = My::Form->new;

# parse command line arguments and validate them with the form
@ARGV = qw( --file x.y.z val1 val2 val3 );
$form->set_input_from_optargs(
    optargs( comment => 'this program is cool', optargs => $form->optargs ) );
die Dumper( $form->errors_hash ) unless $form->valid;

say $form->fields->{file};       # x.y.z
say $form->fields->{arg1};       # val1
say $form->fields->{arg2}[0];    # val2
say $form->fields->{arg2}[1];    # val3

results in

x.y.z
val1
val2
val3

DESCRIPTION

CXC::Form::Tiny::Plugin::OptArgs2 is a Form::Tiny plugin which make it easier to use Form::Tiny to validate command line options and arguments parsed by OptArgs2.

It provides new keywords to be used alongside Form::Tiny's form_field keyword to specify OptArgs2 options and arguments and link them to Form::Tiny fields.

It adds a method to the form's class which returns the OptArgs2 compatible optargs specification and another which sets the form's input from the output of OptArgs2's optargs and class_optargs functions.

USAGE

The typical steps for handling command lines with this plugin are:

  1. Create Form::Tiny forms.

    1. Optionally configure the plugin with "optargs_opts"

    2. Use an "option" or "argument" keyword after each form field to define the OptArgs2 specification (but not after fields which contain nested forms).

  2. Extract OptArgs2 specification

  3. Parse the command line with OptArgs2

  4. Validate the parsed values with with the Form::Tiny forms.

Interfacing with OptArgs2

First, the OptArgs2 compatible optargs specifications are extracted from the form object via the optargs method:

my $form = My::Form->new;
my \@optargs = $form->optargs;

These are then passed on to OptArgs2 via either the optargs, cmd functions, or subcmd functions, e.g.

my $opts = optargs( comment => 'a comment', optargs => \@optargs );

Finally, the parsed results are passed back to the form for validation:

$form->set_input_from_optargs( $opts );
die unless $form->valid;

Nested Forms

Consider this structure:

package My::SubForm {
  ...

  form_field 'upload' => ( type => HashRef[Str] );
  option ( isa => 'HashRef', ... )
}

package My::Form {
  ...

  form_field 'stuff' => ( type => My::SubForm->new );
  form_field 'nonsense => ( type => My::SubForm->new );
}

The stuff and nonsense form fields refer to nested forms; they are not added as OptArgs2 options.

Note that the option element My::SubForm does not specify a name (it can; see below). If not specified it will be generated based upon its parent field, so in this case the following options will be generated for OptArgs2:

--stuff_upload
--stuff_nonsense

The path separator defaults to _, but may be changed via the "optargs_opts" configuration keyword.

When the validated form data is retrieved via $form-fields>, it will look like:

{ stuff => { upload => { ... } },
  nonsense => { upload => { ... } }
}

If the option element did specify a name, then the automatic creation of the hierarchical names would not be done, and the repeated use of the subform would result in duplicate option names, and an exception would (hopefully) be thrown.

Configuration Files

The typical steps for handling command lines with this plugin are:

  1. Create Forms

  2. Extract OptArgs2 specification

  3. Parse command line with OptArgs2

  4. Validate with Forms

For required options, the default behavior is for the OptArgs2 specifications to inherit the required attribute from their associated form field, and thus the initial completeness check for required options is performed by OptArgs2.

However, when a configuration file is used to provide values which the command line will augment or override, this no longer works. OptArgs2 is passed only what is on the command line, and typically the configuration file is specified by a command line option, so the integration of the values in the configuration file has to be performed after OptArgs2 parses the command line, and thus the completeness check has to happen after that.

Now the sequence looks like this:

3.

Parse command line with OptArgs2. Do *not* check for completeness

3.1

Read configuration file and merge the values with those obtained from OptArgs2

4.

Validate with Forms, checking for completeness.

In this scenario, the "inherit_required" plugin flag must be false, otherwise OptArgs2 will make that decision on possibly incomplete data.

[For a complete solution to this issue, check out App::Easer]

Multi-level Commands

OptArgs2 requires separate specifications for each level of command. On the command line, sub-commands can accept options for all of their higher level commands. For example, if command cmd accepts --version, so will its sub command subcmd. When OptArgs2 parses the command line it returns the combined options as a single structure, regardless of where the option is specified, e.g.

cmd --version  subcmd ...
cmd subcmd --version

results in the same structure.

Thus, there are more forms required to create the option specifications for OptArgs2 than are required to validate the results it retrieves from the command line.

The most straightforward way to handle this is to create a series of layered forms, one per command layer, with the form at one layer inheriting from the form for the next higher layer.

For example, given a command structure of cmd, subcmd, subsubcmd, the form structure would look like

package Form::Cmd { ... }
package Form::SubCmd { ...; extends 'Form::Cmd'; }
package Form::SubSubCmd { ...; extends 'Form::SubCmd'; }

OptArgs2 specifications extracted from each form should only provide options for the corresponding command; this requires that the "inherit_optargs" flag be false so that only the specifications for the options in that form be extracted and passed to OptArgs2.

Options and arguments returned by OptArgs2 are collected from a command and its parents, so are validated by the form at that command's level, which will inherit the fields from its parent forms, similar to how OptArgs2 operates.

"inherit_optargs" defaults to false so this behavior is the default.

"optargs_opts" v.s. a Plugin.

As an alternative to specifying "optargs_opts" at the top of the form definition, a Form::Tiny plugin can be used:

package My::Form::Plugin;
use parent 'Form::Tiny::Plugin';

sub plugin ( $self, $caller, $context ) {
  return {
    meta_roles  => [ 'My::Form::Plugin::Meta', ],
  };
}

package My::Form::Plugin::Meta {
  use Moo::Role;
  around inherit_optargs => sub { return !!0 };
}

package My::Form;
use Form::Tiny plugins => ['+CXC::Form::Tiny::Plugin::OptArgs2','+My::Form::Plugin'];
...

Because this plugin wraps the attribute accessor, a form using this plugin cannot override the value via the "optargs_opts" keyword.

To change an attribute's default value, wrap its builder, rather than its accessor, e.g.

package My::Form::Plugin::Meta {
  use Moo::Role;
  around _build_inherit_required => sub { return !!0 };
}

This allows a form to use the "optargs_opts" keyword and override the plugin's "inherit_required" default.

KEYWORDS

Plugin configuration

Plugin behavior for a specific form is performed via the "optargs_opts" keyword, which must appear before any "option" or "argument" keywords.

optargs_opts

This takes a list of named attributes, which may include the following:

inherit_required => Optional [Bool],

This flag determines whether options inherit the required flag from their associated form field. It defaults to true. See "Configuration Files" for a discussion of where this may not be useful.

inherit_optargs => Optional [Bool],

This flag determines whether a child form inherits the options and arguments of its parent. (This differs from nested forms, which appear as the values of form fields). If a inheritance hierarchy is used to model command / sub-command configuration options, then it is best to not inherit the options, as OptArgs2 will automatically create that hierarchy, and having this plugin do so will cause options to appear duplicated. See "Multilevel Commands".

inherit_optargs_match => Optional [ArrayRef],

This parameter provides some flexibility in specifying which parent forms to include or exclude when inheriting options and arguments.

It is takes an array of regular expressions and include/exclude flags (-, +) which are matched against the classes in the form's @ISA array. The first regular expression which matches determines whether the parent class is included or excluded.

For example, if the match list is

[ qr/Form0/, '-', qr/Form1/ ]

each class is first compared to qr/Form0/; if that succeeds the process stops and the class is included (the + is implicit). Otherwise the process continues with qr/Form1/; if that matches it is excluded (because of the immediately preceding - flag).

What happens if a class doesn't match anything in the list? If the list is composed only of included regexps, then it will be excluded. If at least one excluded regexp is in the list, it will be included.

Match lists can be nested, and each match list can be preceded by an include/exclude flag to specify the default behavior for matches. For example,

[ qr/Form1/, '-', [ qr/Form2/, qr/Form3/ ] ]

results in one included regexp and two excluded ones.

Specifying options and arguments.

Option and argument specifications use the option and argument DSL keywords. They must immediately follow the definition of their associated Form::Tiny fields (see "SYNOPSIS").

option

The option keyword takes a list of named attributes with the following accepted names and value types (refer to OptArgs2 for their meaning):

name         => Optional [NonEmptySimpleStr],
alias        => Optional [NonEmptySimpleStr],
comment      => NonEmptySimpleStr,
default      => Optional [Value|CodeRef],
required     => Optional [Bool],
hidden       => Optional [Bool],
isa          => Optional [Enum[ qw( ArrayRef Flag Bool Counter HashRef Int Num Str ) ] ],
isa_name     => Optional [NonEmptySimpleStr],
show_default => Optional [Bool],
trigger      => Optional [CodeRef],

Unlike in OptArgs2, a leading prefix of -- in the value of the isa attribute is optional.

argument

The argument keyword takes a list of named attributes with the following accepted names and value types (refer to OptArgs2 for their meaning, except for order):

name         => Optional [NonEmptySimpleStr],
comment      => NonEmptySimpleStr,
default      => Optional [Value|CodeRef],
greedy       => Optional [Bool],
fallthru     => Optional [Bool],
isa          => Optional [Enum [qw( ArrayRef HashRef Int Num Str SubCmd)]],
isa_name     => Optional [NonEmptySimpleStr],
required     => Optional [Bool],
show_default => Optional [Bool],
order        => Int,

The order attribute specifies the relative order the argument should appear on the command line. This allows the form fields to be specified in an arbitrary order.

Automatic determination of OptArgs2 attributes

name

name will be taken from the immediately preceding form_field specification if not provided. The form field and option names need not be the same.

isa

The isa option can be omitted in the list of option and argument attributes if the OptArgs2 type can be deduced from the Form::Tiny type. The types which require explicit specification are the OptArgs2 Flag, Counter, and SubCmd types.

Here are the existing mappings from Type::Tiny to OptArgs2 types:

ArrayRef Flag Bool Counter HashRef Int Num Str
                  => eponymous OptArgs2 type
BoolLike          => Bool
Any Path File Dir => Str

required

(This behavior is regulated the "inherit_required" attribute passed to the "optargs_opts" keyword.)

OptArgs2 has a simpler concept of a required element than does Form::Tiny. If the field's required attribute is false, the option or argument's value is set to false, otherwise it is set to true.

SUPPORT

Bugs

Please report any bugs or feature requests to bug-cxc-form-tiny-plugin-optargs2@rt.cpan.org or through the web interface at: https://rt.cpan.org/Public/Dist/Display.html?Name=CXC-Form-Tiny-Plugin-OptArgs2

Source

Source is available at

https://gitlab.com/djerius/cxc-form-tiny-plugin-optargs2

and may be cloned from

https://gitlab.com/djerius/cxc-form-tiny-plugin-optargs2.git

AUTHOR

Diab Jerius <djerius@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2023 by Smithsonian Astrophysical Observatory.

This is free software, licensed under:

The GNU General Public License, Version 3, June 2007