File Coverage

blib/lib/Commandable/Finder/Packages.pm
Criterion Covered Total %
statement 62 62 100.0
branch 15 16 93.7
condition 12 20 60.0
subroutine 11 11 100.0
pod 3 4 75.0
total 103 113 91.1


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