File Coverage

blib/lib/Commandable/Finder/Packages.pm
Criterion Covered Total %
statement 74 74 100.0
branch 15 16 93.7
condition 12 20 60.0
subroutine 12 12 100.0
pod 3 4 75.0
total 116 126 92.0


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