1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
# You may distribute under the terms of either the GNU General Public License
# or the Artistic License (the same terms as Perl itself)
#
# (C) Paul Evans, 2019-2024 -- leonerd@leonerd.org.uk
package Commandable::Finder::Packages 0.14;
use v5.26;
use warnings;
use experimental qw( signatures );
use base qw( Commandable::Finder );
use Carp;
use Commandable::Command;
use Module::Pluggable::Object;
=head1 NAME
C<Commandable::Finder::Packages> - find commands stored per package
=head1 SYNOPSIS
use Commandable::Finder::Packages;
my $finder = Commandable::Finder::Packages->new(
base => "MyApp::Command",
);
my $help_command = $finder->find_command( "help" );
foreach my $command ( $finder->find_commands ) {
...
}
=head1 DESCRIPTION
This implementation of L<Commandable::Finder> looks for implementations of
commands, where each command is implemented by a different package somewhere
in the symbol table.
This class uses L<Module::Pluggable> to load packages from the filesystem.
As commands are located per package (and not per file), the application can
provide special-purpose internal commands by implementing more packages in the
given namespace, regardless of which files they come from.
=head1 CONSTANTS
package My::App::Commands::example;
use constant COMMAND_NAME => "example";
use constant COMMAND_DESC => "an example of a command";
...
Properties about each command are stored as methods (usually constant methods)
within each package. Often the L<constant> pragma module is used to create
them.
The following constant names are used by default:
=head2 COMMAND_NAME
use constant COMMAND_NAME => "name";
Gives a string name for the command.
=head2 COMMAND_DESC
use constant COMMAND_DESC => "description";
Gives a string description for the command.
=head2 COMMAND_ARGS
use constant COMMAND_ARGS => (
{ name => "argname", description => "description" },
);
Gives a list of command argument specifications. Each specification is a HASH
reference corresponding to one positional argument, and should contain keys
named C<name>, C<description>, and optionally C<optional>.
=head2 COMMAND_OPTS
use constant COMMAND_OPTS => (
{ name => "optname", description => "description" },
);
Gives a list of command option specifications. Each specification is a HASH
reference giving one named option, in no particular order, and should contain
keys named C<name>, C<description> and optionally C<mode>, C<multi> and
C<default>.
=cut
=head1 CONSTRUCTOR
=cut
=head2 new
$finder = Commandable::Finder::Packages->new( %args )
Constructs a new instance of C<Commandable::Finder::Packages>.
Takes the following named arguments:
=over 4
=item base => STR
The base of the package namespace to look inside for packages that implement
commands.
=item name_method => STR
Optional. Gives the name of the method inside each command package to invoke
to generate the name of the command. Default C<COMMAND_NAME>.
=item description_method => STR
Optional. Gives the name of the method inside each command package to invoke
to generate the description text of the command. Default C<COMMAND_DESC>.
=item arguments_method => STR
Optional. Gives the name of the method inside each command package to invoke
to generate a list of argument specifications. Default C<COMMAND_ARGS>.
=item options_method => STR
Optional. Gives the name of the method inside each command package to invoke
to generate a list of option specifications. Default C<COMMAND_OPTS>.
=item code_method => STR
Optional. Gives the name of the method inside each command package which
implements the actual command behaviour. Default C<run>.
=item named_by_package => BOOL
Optional. If true, the name of each command will be taken from its package
name. with the leading C<base> string removed. If absent or false, the
C<name_method> will be used instead.
=back
If either name or description method are missing from a package, that package
is silently ignored.
Any additional arguments are passed to the C<configure> method to be used as
configuration options.
=cut
sub new ( $class, %args )
{
my $base = ( delete $args{base} ) or croak "Require 'base'";
my $name_method = ( delete $args{name_method} ) // "COMMAND_NAME";
my $description_method = ( delete $args{description_method} ) // "COMMAND_DESC";
my $arguments_method = ( delete $args{arguments_method} ) // "COMMAND_ARGS";
my $options_method = ( delete $args{options_method} ) // "COMMAND_OPTS";
my $code_method = ( delete $args{code_method} ) // "run"; # App-csvtool
undef $name_method if delete $args{named_by_package};
my $mp = Module::Pluggable::Object->new(
search_path => $base,
require => 1,
);
my $self = bless {
mp => $mp,
base => $base,
methods => {
name => $name_method,
desc => $description_method,
args => $arguments_method,
opts => $options_method,
code => $code_method,
},
}, $class;
$self->configure( %args ) if %args;
return $self;
}
sub packages ( $self )
{
my $name_method = $self->{methods}{name};
my $packages = $self->{cache_packages} //= [ $self->{mp}->plugins ];
return @$packages;
}
sub _commands ( $self )
{
my $name_method = $self->{methods}{name};
return $self->{cache_commands} //= do {
my %commands;
foreach my $pkg ( $self->packages ) {
next if defined $name_method and not $pkg->can( $name_method );
my $name = defined $name_method
? $pkg->$name_method
: ( $pkg =~ s/\Q$self->{base}\E:://r );
my $code = $pkg->can( $self->{methods}{code} ) or next;
my $desc = ( $pkg->can( $self->{methods}{desc} ) or next )->( $pkg );
my $args;
if( my $argsmeth = $pkg->can( $self->{methods}{args} ) ) {
$args = [
map { Commandable::Command::_Argument->new( %$_ ) } $pkg->$argsmeth
];
}
my $opts;
if( my $optsmeth = $pkg->can( $self->{methods}{opts} ) ) {
$opts = {
map { my $o = Commandable::Command::_Option->new( %$_ );
map { ( $_ => $o ) } $o->names
} $pkg->$optsmeth
};
}
$commands{ $name } = Commandable::Command->new(
name => $name,
description => $desc,
arguments => $args,
options => $opts,
package => $pkg,
code => $code,
);
}
$self->add_builtin_commands( \%commands );
\%commands;
};
}
sub find_commands ( $self )
{
return values $self->_commands->%*;
}
sub find_command ( $self, $cmd )
{
return $self->_commands->{$cmd};
}
=head1 AUTHOR
Paul Evans <leonerd@leonerd.org.uk>
=cut
0x55AA;
|