App-Easer

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

   - Release after 84 PASSes on CPAN Testers (thanks!)

2.005  2023-01-02 00:36:49 CET
   - Enhance automatic generation of help for options
   - Fix bugs in automatic generation of help for options

2.004  2022-07-16 12:28:13 CEST
   - Release after 90 PASSes on CPAN Testers (thanks!)

2.003  2022-07-15 19:44:31 CEST
   - Accept sub reference for validate
   - Test validation, both with Params::Validate and sub ref

2.002  2022-06-18 18:07:12 CEST
   - Release after 194 PASSes on CPAN Testers (thanks!)

2.001001  2022-06-13 21:06:03 CEST
   - Try to (blindly) address an issue with tests in Strawberry Perl

2.001  2022-06-12 21:39:55 CEST
   - Split into legacy V1 version and new V2 version

MANIFEST  view on Meta::CPAN

t/V2/20.collect-options.t
t/V2/21.options-ordering.json
t/V2/21.options-ordering.t
t/V2/22.custom-sources.t
t/V2/23.options-default.t
t/V2/24.hash-sources.1.json
t/V2/24.hash-sources.t
t/V2/25.inheritance.t
t/V2/26.options-matrix.1.json
t/V2/26.options-matrix.t
t/V2/30.params-validate.t
t/V2/31.custom-validate.t
t/V2/40.help-booleans.t
t/V2/41.missing-help-no-warning.t
t/V2/42.help-help.t
t/V2/50.lone-dash.t
t/V2/51.params-validate-before.t
t/V2/52.name-in-option.t
t/V2/60.final_commit.t
t/V2/LocalTester.pm
t/author-pod-syntax.t
t/author-pod-version.t
t/release-pod-coverage.t

docs/docs/02-cookbook.md  view on Meta::CPAN


```
Callback name | Searched function
--------------+------------------
collect       | collect   
commit        | commit    
dispatch      | dispatch  
execute       | execute   
fallback      | fallback  
merge         | merge     
validate      | validate  
```

It is possible to set a different name like this:

```perl
my $app = {
    commands => {
        foo => {
            execute => 'MyApp::Foo#execute_this',
        }

docs/docs/40-command-options.md  view on Meta::CPAN

- last, the `bar` child uses the defaults set in the main application
  configuration.

## Option values validation

[Getopt::Long][] enables a first coarse validation, by providing input
types for integers, strings and booleans. Sometimes, though, this might
not be sufficient, e.g. if some options are mutually exclusive and
incompatible.

To address this, it is possible to set key `validate` for performing
validation upon the collected options. This can happen at the top
`configuration` level or inside each command's specification.

In the first case, `validate` must point to an *executable* (i.e.
something that can be resolved by [App::Easer][] into a sub) with the
following signature:

```perl
sub validator ($global, $spec, $args) { die 'sorry!' if error(); }
```

The passed parameters are:

- `$global` the global application object;
- `$spec` the specification for the command under analysis;
- `$args` any command-line arguments residual up to this point.

The *latest* configuration to be checked is available in
`$global->{configs}[-1]` (not exactly an easy place...):

```perl
sub validator ($global, $spec, $args) {
   require Params::Validate;
   Params::Validate::validate(
      $self->{configs}[-1]->%*,  # configuration to validate
      {...}                      # hash for Params::Validate
   );
}
```

The interface above is not officially documented and is subject to
change, e.g. to make it easier to do the validation without having to
fiddle with the internals of `$global`.

When the `validate` key is set in a command's specification, it can
either be an *executable* like described above, or a hash reference. In
this latter case, [Params::Validate][] is used to validate the
configuration against that hash reference. This particular approach can
be considered stable and not subject to changes in the future.


## Configuration file from another option

It's possible to expand the options values gathering with loading them
from a JSON file, whose path is provided among the available options.
This allows e.g. to add a command-line option `-c|--config` to point to
a file with further values.

lib/App/Easer/V1.pm  view on Meta::CPAN

} ## end sub name_for_option ($o)

sub name_for_option ($o) {
   return $o->{name} if defined $o->{name};
   return $1 if defined $o->{getopt} && $o->{getopt} =~ m{\A(\w+)}mxs;
   return lc $o->{environment}
      if defined $o->{environment} && $o->{environment} ne '1';
   return '~~~';
} ## end sub name_for_option ($o)

sub params_validate ($self, $spec, $args) {
   my $validator = $spec->{validate}
     // $self->{application}{configuration}{validate} // return;
   require Params::Validate;
   Params::Validate::validate($self->{configs}[-1]->%*, $validator);
} ## end sub params_validate

sub print_commands ($self, $target) {
   my $command = fetch_spec_for($self, $target);
   my $fh =
     $self->{application}{configuration}{'help-on-stderr'}
     ? \*STDERR
     : \*STDOUT;
   if (my @children = get_children($self, $command)) {
      print {$fh} list_commands($self, \@children);
   }

lib/App/Easer/V1.pm  view on Meta::CPAN

      },
      trail       => [['MAIN', $application->{commands}{MAIN}{name}]],
   };

   while ('necessary') {
      my $command = $self->{trail}[-1][0];
      my $spec    = fetch_spec_for($self, $command)
        or die "no definition for '$command'\n";

      $args = collect_options($self, $spec, $args);
      validate_configuration($self, $spec, $args);
      commit_configuration($self, $spec, $args);

      my ($subc, $alias) = fetch_subcommand($self, $spec, $args) or last;
      push $self->{trail}->@*, [$subc, $alias];
   } ## end while ('necessary')

   return execute($self, $args) // 0;
} ## end sub run

sub slurp ($file, $mode = '<:encoding(UTF-8)') {

lib/App/Easer/V1.pm  view on Meta::CPAN

sub stock_DefaultSources { [qw< +Default +CmdLine +Environment +Parent >] }

sub stock_SourcesWithFiles {
   [
      qw< +Default +CmdLine +Environment +Parent
         +JsonFileFromConfig +JsonFiles
        >
   ]
} ## end sub stock_SourcesWithFiles

sub validate_configuration ($self, $spec, $args) {
   my $from_spec = $spec->{validate};
   my $from_self = $self->{application}{configuration}{validate};
   my $validator;
   if (defined $from_spec && 'HASH' ne ref $from_spec) {
      $validator = $self->{factory}->($from_spec, 'validate');
   }
   elsif (defined $from_self && 'HASH' ne ref $from_self) {
      $validator = $self->{factory}->($from_self, 'validate');
   }
   else {    # use stock one
      $validator = \&params_validate;
   }
   $validator->($self, $spec, $args);
} ## end sub validate_configuration

exit run(
   $ENV{APPEASER} // {
      commands => {
         MAIN => {
            name        => 'main app',
            help        => 'this is the main app',
            description => 'Yes, this really is the main app',
            options     => [
               {

lib/App/Easer/V1.pod  view on Meta::CPAN

The following YAML representation gives a view of the structure of an
application managed by C<App::Easer::V1>:

   factory:
      create: «executable»
      prefixes: «hash or array of hashes»
   configuration:
      collect:   «executable»
      merge:     «executable»
      specfetch: «executable»
      validate:  «executable»
      sources:   «array»
      'auto-children':    «false or array»
      'help-on-stderr':   «boolean»
      'auto-leaves':      «boolean»
      'auto-environment': «boolean»
   commands:
      cmd-1:
         «command definition»
      cmd-2:
         «command definition»

lib/App/Easer/V1.pod  view on Meta::CPAN

the specification of the command is fetched, either from a configuration
hash or by some other method, according to the I<specfetch> hook;

=item *

option values for that command are gathered, I<consuming> part of the
command-line arguments;

=item *

the configuration is optionally validated;

=item *

a I<commit> hook is optionally called, allowing an intermediate command
to perform some actions before a sub-command is run;

=item *

a sub-command is searched and, if present, the process restarts from the
first step above

lib/App/Easer/V1.pod  view on Meta::CPAN

the normal working of C<App::Easer::V1>.

=head2 Configuration Parsing Customization

   configuration:
      name:      «string»
      collect:   «executable»
      merge:     «executable»
      namenv:    «executable»
      specfetch: «executable»
      validate:  «executable»
      sources:   «array»
      'auto-children':    «false or array»
      'help-on-stderr':   «boolean»
      'auto-leaves':      «boolean»
      'auto-environment': «boolean»

The C<name> configuration allows setting a name for the application,
which can e.g. be used to generate automatic names for environment
variables to be associated to command options.

lib/App/Easer/V1.pod  view on Meta::CPAN


The C<specfetch> executable allows setting a function to perform
resolution of a command identifier (as e.g. stored in the C<children>)
or an upper command) into a specification. By default the internal
function corresponding to the executable specification string
C<+SpecFromHash> is used, insisting that the whole application is
entirely pre-assembled in the specification hash/object; it's also
possible to use C<+SpecFromHashOrModule> for allowing searching through
modules too.

The C<validate> executable allows setting a validator. By default the
validation is performed using L<Params::Validate> (if available, it is
anyway loaded only when needed).

It is possible to set several I<sources> for gathering options values,
setting them using the C<sources> array. By default it is set to the
ordered list with C<+Default>, C<+CmdLine>, C<+Environment>, and
C<+Parent>, , meaning that options from the command line will have the
highest precedence, then the environment, then whatever comes from the
parent command configuration, then default values if present. This can
be set explicitly with C<+DefaultSources>.

lib/App/Easer/V1.pod  view on Meta::CPAN


=back

As anticipated, the C<help> and C<commands> sub-commands are
automatically generated and associated to each command by default (more
or less). If this is not the desired behaviour, it is possible to either
disable the addition of the C<auto-children> completely (by setting a
false value), or provide an array of children names that will be added
automatically to each command (again, more or less).

It should be noted that both C<validate> and C<sources> are also part of
the specific setup for each command. As such, they will be rarely set at
the higher C<configuration> level and the whole C<configuration> section
can normally be left out of an application's definition.

Option C<help-on-stderr> allows printing the two stock helper
commands C<help> and C<commands> on standard error instead of standard
output (which is the default).

Option C<auto-leaves> allows setting any command that has no I<explicit>
sub-command as a leaf, which prevents it from getting a C<help> and a

lib/App/Easer/V1.pod  view on Meta::CPAN

       getopt: 'whip|w=s'
       environment: FOO_WHIP
       default: gargle
       help: 'beware of the whip'
   auto-environment: 0
   allow-residual-options: 0
   sources: ['+CmdLine', '+Environment', '+Parent', '+Default']

   collect:  «executable»
   merge:    «executable»
   validate: ... «executable» or data structure...
   commit:   «executable»
   execute:  «executable»

   children: ['foo.bar', 'baz', {...}]
   default-child: 'foo.bar'
   dispatch: «executable»
   fallback: «executable»
   fallback-to: 'baz'
   fallback-to-default: 1
   leaf: 0

lib/App/Easer/V1.pod  view on Meta::CPAN

Items are executables, i.e. sub references or names that will be
I<resolved> into sub references through the I<factory>.

=item C<collect>

=item C<merge>

These allow overriding the internal default behaviour of
C<App::Easer::V1>

=item C<validate>

This can be a sub reference called to perform the validation, or a hash
that, when the default validator is in effect, will be used to call
C<Params::Validate>.

=item C<commit>

This optional callback is invoked just after the parsing of the
configuration and its optional validation. It shouldn't be normally
needed, but it allows a "former" command to perform actions before the

lib/App/Easer/V1.pod  view on Meta::CPAN

=head2 hash_merge

=head2 list_commands

=head2 load_application

=head2 merger

=head2 name_for_option

=head2 params_validate

=head2 print_commands

=head2 print_help

=head2 slurp

=head2 sources

=head2 stock_ChildrenByPrefix

lib/App/Easer/V1.pod  view on Meta::CPAN


=head2 stock_SpecFromHashOrModule

Used as a C<specfetch> for accessing the specification first in the
hash, then looking into modules (and caching the search in the hash).

=head2 stock_commands

=head2 stock_help

=head2 validate_configuration

=end hidden

=head1 BUGS AND LIMITATIONS

Minimum perl version 5.24.

Report bugs through GitHub (patches welcome) at
L<https://github.com/polettix/App-Easer>.

lib/App/Easer/V2.pm  view on Meta::CPAN

sub environment_prefix ($self, @r) { $self->_rw(@r) }
sub execution_reason ($self, @r) { $self->_rw(@r) }
sub fallback_to ($self, @r) { $self->_rw(@r) }
sub final_commit_stack ($self, @r) { $self->_rwa(@r) }
sub force_auto_children ($self, @r) { $self->_rw(@r) }
sub hashy_class ($self, @r) { $self->_rw(@r) }
sub help ($self, @r) { $self->_rw(@r) }
sub help_channel ($slf, @r) { $slf->_rw(@r) }
sub name ($s, @r) { $s->_rw(@r) // ($s->aliases)[0] // '**no name**' }
sub options_help ($s, @r) { $s->_rw(@r) }
sub params_validate ($self, @r) { $self->_rw(@r) }
sub parent ($self, @r) { $self->_rw(@r) }
sub pre_execute ($self, @r) { $self->_rwa(@r) }
sub residual_args ($self, @r) { $self->_rwa(@r) }
sub _last_cmdline ($self, @r) { $self->_rw(@r) }
sub _sources ($self, @r) { $self->_rwn(sources => @r) }
sub usage ($self, @r) { $self->_rw(@r) }

sub config_hash_key ($self, @r) { $self->_rw_prd(@r) }

sub is_root ($self) { ! defined($self->parent) }

lib/App/Easer/V2.pm  view on Meta::CPAN

      children_prefixes      => [$pkg . '::Cmd'],
      config_hash_key        => \'merged',
      default_child          => 'help',
      environment_prefix     => '',
      fallback_to            => undef,
      final_commit_stack     => [],
      force_auto_children    => undef,
      hashy_class            => __PACKAGE__,
      help_channel           => '-STDOUT:encoding(UTF-8)',
      options                => [],
      params_validate        => undef,
      pre_execute            => [],
      residual_args          => [],
      sources                => 'default-array',   # 2024-08-24 defer
      ($pkg_spec // {})->%*,
      (@args && ref $args[0] ? $args[0]->%* : @args),
   };
   my $self = bless {$pkg => $slot}, $pkg;
   return $self;
} ## end sub new

lib/App/Easer/V2.pm  view on Meta::CPAN

   }

   # here we try to propagate to the parent... if it exists
   my $parent = $self->parent;
   return unless $parent;  # we're root, no parent, no propagation up

   $parent->final_commit_stack([$stack->@*]);
   return $parent->final_commit;
} ## end sub commit

# validate collected options values, called after commit ends.
sub validate ($self, @n) {

   # Support the "accessor" interface for using a validation sub
   my $validator = $self->_rw(@n);
   return $validator if @n;

   # If set, it MUST be a validation sub reference. Otherwise, try the
   # params_validate/Params::Validate path.
   if ($validator) {
      die "validator can only be a CODE reference\n"
         unless ref $validator eq 'CODE';
      $validator->($self);
   }
   elsif (my $params_validate = $self->params_validate) {
      require Params::Validate;
      if (my $config_validator = $params_validate->{config} // undef) {
         my @array = $self->config_hash;
         &Params::Validate::validate(\@array, $config_validator);
      }
      if (my $args_validator = $params_validate->{args} // undef) {
         my @array = $self->residual_args;
         &Params::Validate::validate_pos(\@array, $args_validator->@*);
      }
   }
   else {} # no validation needed

   return $self;
} ## end sub validate ($self)

sub find_matching_child ($self, $command) {
   return unless defined $command;
   for my $candidate ($self->list_children) {
      my ($child) = $self->inflate_children($candidate);
      return $child if $child->supports($command);
   }
   return;
} ## end sub find_matching_child

lib/App/Easer/V2.pm  view on Meta::CPAN

      my $sub = $self->ref_to_sub($spec) or die "nothing to pre-execute\n";
      $sub->($self);
   }
   return $self;
}

sub run ($self, $name, @args) {
   $self->call_name($name);
   $self->collect(@args);
   $self->commit;
   $self->validate;
   my ($child, @child_args) = $self->find_child;
   return $child->run(@child_args) if defined $child;

   # we're the executors
   $self->execution_reason($child_args[0]);
   $self->final_collect;  # no @args passed in this collection
   $self->final_commit;
   $self->pre_execute_run;
   return $self->execute;
} ## end sub run

lib/App/Easer/V2.pod  view on Meta::CPAN


Name of the command. If absent, the first item in the C<alias> array is
used.

=item C<options>

Array of items.

See L</OPTIONS>.

=item C<params_validate>

Hash reference or C<undef>. Ignored if L</validate> is set.

If passed as a hash reference, two keys are supported:

=over

=item C<args>

call C<Params::Validate::validate_pos> on the C<residual_args> (see
L</OPTIONS>).

=item C<config>

call C<Params::Validate::validate> on the collected I<merged>
configuration (see L</OPTIONS>).

=back

=item C<sources>

Array of items.

See L</OPTIONS>.

=item C<validate>

Sub reference for performing validation. Will be called during the
validation phase and passed the command object instance:

   $validation_sub->($self);

If set, L</params_validate> is ignored.

=back

The following YAML representation gives an overview of the elements that
define an application managed by C<App::Easer::V2>, highlighting the
necessary or I<strongly suggested> ones at the beginning:

  aliases: «array of strings»
  execute: «executable»
  help: «string»

lib/App/Easer/V2.pod  view on Meta::CPAN

  children_prefixes: «array of strings»
  commit: «executable»
  default_child: «string»
  description: «string»
  environment_prefix: «string»
  fallback_to: «string»
  force_auto_children: «boolean»
  hashy_class: «string»
  help_channel: «string»
  name: «string»
  params_validate: «hash»
  sources: «array of items»
  validate: «executable»

As anticipated, it's entirely up to the user to decide what style is
best, i.e. define applications through metadata only, through
object-oriented derivation, or through a mix of the two. The following
examples are aimed at producing the same application:

   # metadata (mostly)
   my $app_as_metadata = {
      aliases => [qw< this that >],
      help => 'this is the application, but also that',

lib/App/Easer/V2.pod  view on Meta::CPAN


Set text to be printed for the options.

If set to a plain string, that string is all that will be printed, i.e.
no automatic help text from C<options> will be generated.

If set to a hash reference, two keys are supported: C<preamble> and
C<postamble>. When present, they will be printed respectively before and
after the help text generated automatically from C<options>.

=item C<params_validate>

   my $href_or_undef = $self->params_validate;
   $self->params_validate($new_pv_conf);

See L</Application High Level View>.

=item C<parent>

   my $parent_command = $self->parent;

Get the parent command.

=item C<ref_to_sub>

lib/App/Easer/V2.pod  view on Meta::CPAN


Use of this method is discouraged and not future-proof.

=item C<sources>

   my @sources = $self->sources;
   $self->sources(\@new_sources_list);

See L</Application High Level View> and L</OPTIONS>.

=item C<validate>

   $self->validate(\&validation_sub);
   $self->validate;

Performs validation.

When a validation sub is set (either calling with a parameter, or
setting it via C<validate> in L</new>) it will be called, receiving
C<$self> as the only parameter:

   $sub->($self);

The validator is supposed to throw an exception upon validation failure.

If no validation sub is set, L</params_validate> is looked for. If
present, validation is applied according to it, using
L<Params::Validate>.

=back

=head1 OPTIONS

The main capability provided by C<App::Easer> is the flexibility to
handle options, collecting them from several sources and merging them
together according to priorities.

t/V2/30.params-validate.t  view on Meta::CPAN

      {
         getopt      => 'foo|f=s',
         environment => 'GALOOK_FOO',
      },
      {
         getopt      => 'bar|b=s',
         environment => 'GALOOK_BAR',
         default     => 'buzz',
      },
   ],
   params_validate => {
      config => { foo => 1, bar => { regex => qr{(?imxs:\A b)} } },
      args   => [1, 0],
   },
   execute => sub ($self) { print {*STDOUT} 'FOO'; return 42 },
};

subtest '--foo hello all' => sub {
   test_run($app, [qw< --foo hello all >], {}, 'baz')
     ->no_exceptions->result_is(42)->stdout_like(qr{FOO});
};

t/V2/31.custom-validate.t  view on Meta::CPAN

      {
         getopt      => 'foo|f=s',
         environment => 'GALOOK_FOO',
      },
      {
         getopt      => 'bar|b=s',
         environment => 'GALOOK_BAR',
         default     => 'buzz',
      },
   ],
   validate => sub ($self) {
      die "Mandatory parameter 'foo'\n" unless $self->config('foo');
      my $n_args = $self->residual_args;
      die "0 parameters but 1-2 needed\n" unless $n_args;
      die "$n_args parameters but 1-2 needed\n" if $n_args > 2;
      return;
   },
   execute => sub ($self) { print {*STDOUT} 'FOO'; return 42 },
};

subtest '--foo hello all' => sub {

t/V2/51.params-validate-before.t  view on Meta::CPAN

      {
         getopt      => 'foo|f=s',
         environment => 'GALOOK_FOO',
      },
      {
         getopt      => 'bar|b=s',
         environment => 'GALOOK_BAR',
         default     => 'buzz',
      },
   ],
   params_validate => {
      config => { foo => 1, bar => { regex => qr{(?imxs:\A b)} } },
      args   => [1, 0],
   },
   execute => sub ($self) { print {*STDOUT} 'FOO'; return 42 },
};

subtest '--foo hello all' => sub {
   test_run($app, [qw< --foo hello all >], {}, 'baz')
     ->no_exceptions->result_is(42)->stdout_like(qr{FOO});
};



( run in 0.566 second using v1.01-cache-2.11-cpan-a5abf4f5562 )