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-2021 -- leonerd@leonerd.org.uk
5              
6             package Commandable::Finder::Packages 0.09;
7              
8 7     7   276944 use v5.14;
  7         67  
9 7     7   37 use warnings;
  7         15  
  7         199  
10 7     7   44 use base qw( Commandable::Finder );
  7         15  
  7         3028  
11              
12 7     7   46 use Carp;
  7         16  
  7         403  
13              
14 7     7   2365 use Commandable::Command;
  7         18  
  7         226  
15 7     7   3528 use Module::Pluggable::Object;
  7         69035  
  7         4584  
16              
17             =head1 NAME
18              
19             C - 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 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 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 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, C, and optionally C.
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, C and optionally C and C.
92              
93             =cut
94              
95             =head1 CONSTRUCTOR
96              
97             =cut
98              
99             =head2 new
100              
101             $finder = Commandable::Finder::Packages->new( %args )
102              
103             Constructs a new instance of C.
104              
105             Takes the following named arguments:
106              
107             =over 4
108              
109             =item base => STR
110              
111             The base of the package namespace to look inside for packages that implement
112             commands.
113              
114             =item name_method => STR
115              
116             Optional. Gives the name of the method inside each command package to invoke
117             to generate the name of the command. Default C.
118              
119             =item description_method => STR
120              
121             Optional. Gives the name of the method inside each command package to invoke
122             to generate the description text of the command. Default C.
123              
124             =item arguments_method => STR
125              
126             Optional. Gives the name of the method inside each command package to invoke
127             to generate a list of argument specifications. Default C.
128              
129             =item options_method => STR
130              
131             Optional. Gives the name of the method inside each command package to invoke
132             to generate a list of option specifications. Default C.
133              
134             =item code_method => STR
135              
136             Optional. Gives the name of the method inside each command package which
137             implements the actual command behaviour. Default C.
138              
139             =item named_by_package => BOOL
140              
141             Optional. If true, the name of each command will be taken from its package
142             name. with the leading C string removed. If absent or false, the
143             C will be used instead.
144              
145             =back
146              
147             If either name or description method are missing from a package, that package
148             is silently ignored.
149              
150             Any additional arguments are passed to the C method to be used as
151             configuration options.
152              
153             =cut
154              
155             sub new
156             {
157 6     6 1 651 my $class = shift;
158 6         31 my %args = @_;
159              
160 6 50       33 my $base = ( delete $args{base} ) or croak "Require 'base'";
161              
162 6   50     44 my $name_method = ( delete $args{name_method} ) // "COMMAND_NAME";
163 6   50     37 my $description_method = ( delete $args{description_method} ) // "COMMAND_DESC";
164 6   50     37 my $arguments_method = ( delete $args{arguments_method} ) // "COMMAND_ARGS";
165 6   50     32 my $options_method = ( delete $args{options_method} ) // "COMMAND_OPTS";
166 6   50     32 my $code_method = ( delete $args{code_method} ) // "run"; # App-csvtool
167              
168 6 100       33 undef $name_method if delete $args{named_by_package};
169              
170 6         61 my $mp = Module::Pluggable::Object->new(
171             search_path => $base,
172             require => 1,
173             );
174              
175 6         111 my $self = bless {
176             mp => $mp,
177             base => $base,
178             methods => {
179             name => $name_method,
180             desc => $description_method,
181             args => $arguments_method,
182             opts => $options_method,
183             code => $code_method,
184             },
185             }, $class;
186              
187 6 100       48 $self->configure( %args ) if %args;
188              
189 6         21 return $self;
190             }
191              
192             sub packages
193             {
194 6     6 0 13 my $self = shift;
195              
196 6         18 my $name_method = $self->{methods}{name};
197              
198 6   50     44 my $packages = $self->{cache_packages} //= [ $self->{mp}->plugins ];
199              
200 6         5737 return @$packages;
201             }
202              
203             sub _commands
204             {
205 23     23   57 my $self = shift;
206              
207 23         64 my $name_method = $self->{methods}{name};
208 23   66     109 return $self->{cache_commands} //= do {
209 6         13 my %commands;
210 6         24 foreach my $pkg ( $self->packages ) {
211 15 100 100     194 next if defined $name_method and not $pkg->can( $name_method );
212              
213 14 100       156 my $name = defined $name_method
214             ? $pkg->$name_method
215             : ( $pkg =~ s/\Q$self->{base}\E:://r );
216              
217 14 100       115 my $code = $pkg->can( $self->{methods}{code} ) or next;
218              
219 13   50     110 my $desc = ( $pkg->can( $self->{methods}{desc} ) or next )->( $pkg );
220              
221 13         24 my $args;
222 13 100       96 if( my $argsmeth = $pkg->can( $self->{methods}{args} ) ) {
223             $args = [
224 7         23 map { Commandable::Command::_Argument->new( %$_ ) } $pkg->$argsmeth
  7         59  
225             ];
226             }
227              
228 13         35 my $opts;
229 13 100       112 if( my $optsmeth = $pkg->can( $self->{methods}{opts} ) ) {
230             $opts = {
231 4         19 map { my $o = Commandable::Command::_Option->new( %$_ );
  7         51  
232 7         28 map { ( $_ => $o ) } $o->names
  13         49  
233             } $pkg->$optsmeth
234             };
235             }
236              
237 13         75 $commands{ $name } = Commandable::Command->new(
238             name => $name,
239             description => $desc,
240             arguments => $args,
241             options => $opts,
242              
243             package => $pkg,
244             code => $code,
245             );
246             }
247              
248 6         55 $self->add_builtin_commands( \%commands );
249              
250 6         75 \%commands;
251             };
252             }
253              
254             sub find_commands
255             {
256 3     3 1 16 my $self = shift;
257              
258 3         7 return values %{ $self->_commands };
  3         9  
259             }
260              
261             sub find_command
262             {
263 20     20 1 3428 my $self = shift;
264 20         44 my ( $cmd ) = @_;
265              
266 20         56 return $self->_commands->{$cmd};
267             }
268              
269             =head1 AUTHOR
270              
271             Paul Evans
272              
273             =cut
274              
275             0x55AA;