File Coverage

blib/lib/Prophet/CLI/Command.pm
Criterion Covered Total %
statement 15 122 12.3
branch 0 34 0.0
condition 0 10 0.0
subroutine 5 19 26.3
pod 9 12 75.0
total 29 197 14.7


line stmt bran cond sub pod time code
1             package Prophet::CLI::Command;
2 39     39   189 use Any::Moose;
  39         57  
  39         306  
3              
4 39     39   20598 use Prophet::CLI;
  39         69  
  39         1280  
5 39     39   192 use Params::Validate qw(validate);
  39         63  
  39         31889  
6              
7             has cli => (
8             is => 'rw',
9             isa => 'Prophet::CLI',
10             weak_ref => 1,
11             handles => [
12             qw/app_handle handle config/,
13             ],
14             );
15              
16             has context => (
17             is => 'rw',
18             isa => 'Prophet::CLIContext',
19             handles => [
20             qw/args set_arg arg has_arg delete_arg arg_names/,
21             qw/props set_prop prop has_prop delete_prop prop_names/,
22             'add_to_prop_set', 'prop_set',
23             ],
24              
25             );
26              
27             sub ARG_TRANSLATIONS {
28 0     0 0   my $self = shift;
29 0           return ( 'v' => 'verbose',
30             'a' => 'all' );
31             }
32              
33             =head2 Registering argument translations
34              
35             This is the Prophet CLI's way of supporting short forms for arguments,
36             e.g. you want to let '-v' be able to used for the same purpose as
37             '--verbose' without dirtying your code checking both or manually
38             setting them if they exist. We want it to be as easy as possible
39             to have short commands.
40              
41             To use, have your command subclass do:
42              
43             sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), f => 'file' };
44              
45             You can register as many translations at a time as you want.
46             The arguments will be translated when the command object is
47             instantiated. If an arg already exists in the arg translation
48             table, it is overwritten with the new value.
49              
50             =cut
51              
52             sub _translate_args {
53 0     0     my $self = shift;
54 0           my %translations = $self->ARG_TRANSLATIONS;
55              
56 0           for my $arg (keys %translations) {
57 0 0         $self->set_arg($translations{$arg}, $self->arg($arg))
58             if $self->has_arg($arg);
59             }
60             }
61              
62             # run arg translations on object instantiation
63             sub BUILD {
64 0     0 1   my $self = shift;
65              
66 0           $self->_translate_args();
67              
68 0           return $self;
69             }
70              
71             sub fatal_error {
72 0     0 0   my $self = shift;
73 0           my $reason = shift;
74              
75             # always skip this fatal_error function when generating a stack trace
76 0           local $Carp::CarpLevel = $Carp::CarpLevel + 1;
77              
78 0           die $reason . "\n";
79             }
80              
81             =head2 require_uuid
82              
83             Checks to make sure the uuid attribute is set. Prints an error and dies
84             with the command's usage string if it is not set.
85              
86             =cut
87              
88             sub require_uuid {
89 0     0 1   my $self = shift;
90              
91 0 0         if (!$self->has_uuid) {
92 0           my $type = $self->type;
93 0           my $name = (split /::/, $self->meta->name)[-1];
94 0           warn "No UUID or LUID given!\n";
95 0           $self->print_usage;
96             }
97             }
98              
99             =head2 edit_text [text] -> text
100              
101             Filters the given text through the user's C<$EDITOR> using
102             L.
103              
104             =cut
105              
106             sub edit_text {
107 0     0 1   my $self = shift;
108 0           my $text = shift;
109              
110             # don't invoke the editor in a script, the test will appear to hang
111             #die "Tried to invoke an editor in a test script!" if $ENV{IN_PROPHET_TEST_COMMAND};
112              
113 0           require Proc::InvokeEditor;
114 0           return scalar Proc::InvokeEditor->edit($text);
115             }
116              
117              
118              
119              
120             =head2 edit_hash hash => hashref, ordering => arrayref
121              
122             Filters the hash through the user's C<$EDITOR> using L.
123              
124             No validation is done on the input or output.
125              
126             If the optional ordering argument is specified, hash keys will be presented
127             in that order (with unspecified elements following) for edit.
128              
129             If the record class for the current type defines a C
130             routine, those props will not be presented for editing.
131              
132             False values are not returned unless a prop is removed from the output.
133              
134             =cut
135              
136             sub edit_hash {
137 0     0 1   my $self = shift;
138 0           validate( @_, { hash => 1, ordering => 0 } );
139 0           my %args = @_;
140 0           my $hash = $args{'hash'};
141 0 0         my @ordering = @{ $args{'ordering'} || [] };
  0            
142 0           my $record = $self->_get_record_object;
143 0 0         my @do_not_edit = $record->can('immutable_props') ? $record->immutable_props : ();
144              
145 0 0         if (@ordering) {
146             # add any keys not in @ordering to the end of it
147 0           my %keys_in_ordering;
148 0 0         map { $keys_in_ordering{$_} = 1 if exists($hash->{$_}) } @ordering;
  0            
149 0 0         map { push @ordering, $_ if !exists($keys_in_ordering{$_}) } keys %$hash;
  0            
150             } else {
151 0           @ordering = sort keys %$hash;
152             }
153              
154             # filter out props we don't want to present for editing
155 0           my %do_not_edit = map { $_ => 1 } @do_not_edit;
  0            
156 0           @ordering = grep { !$do_not_edit{$_} } @ordering;
  0            
157              
158 0           my $input = join "\n", map { "$_: $hash->{$_}" } @ordering;
  0            
159              
160 0           my $output = $self->edit_text($input);
161              
162 0 0         die "Aborted.\n" if $input eq $output;
163              
164             # parse the output
165 0           my $filtered = {};
166 0           for my $line (split "\n", $output) {
167 0 0         if ($line =~ m/^([^:]+):\s*(.*)$/) {
168 0           my $prop = $1;
169 0           my $val = $2;
170             # don't return empty values
171 0 0         $filtered->{$prop} = $val unless !($val);
172             }
173             }
174 39     39   257 no warnings 'uninitialized';
  39         67  
  39         31417  
175              
176             # if a key is deleted intentionally, set its value to ''
177 0           for my $prop (keys %$hash) {
178 0 0 0       if (!exists $filtered->{$prop} and ! exists $do_not_edit{$prop}) {
179 0           $filtered->{$prop} = '';
180             }
181             }
182              
183             # filter out unchanged keys as they clutter changesets if they're set again
184 0 0         map { delete $filtered->{$_} if $hash->{$_} eq $filtered->{$_} } keys %$filtered;
  0            
185              
186 0           return $filtered;
187             }
188              
189             =head2 edit_props arg => str, defaults => hashref, ordering => arrayref
190              
191             Returns a hashref of the command's props mixed in with any default props.
192             If the "arg" argument is specified, (default "edit", use C if you only
193             want default arguments), then L is invoked on the property list.
194              
195             If the C argument is specified, properties will be presented in that
196             order (with unspecified props following) if filtered through L.
197              
198             =cut
199              
200             sub edit_props {
201 0     0 1   my $self = shift;
202 0           my %args = @_;
203 0   0       my $arg = $args{'arg'} || 'edit';
204 0           my $defaults = $args{'defaults'};
205              
206 0           my %props;
207 0 0         if ($defaults) {
208 0           %props = (%{ $defaults }, %{ $self->props });
  0            
  0            
209             } else {
210 0           %props = %{$self->props};
  0            
211             }
212              
213 0 0         if ($self->has_arg($arg)) {
214 0           return $self->edit_hash(hash => \%props, ordering => $args{'ordering'});
215             }
216              
217 0           return \%props;
218             }
219              
220             =head2 prompt_choices question
221              
222             Asks user the question and returns 0 if answer was the second choice,
223             1 otherwise. (First choice is the default.)
224              
225             =cut
226              
227             sub prompt_choices {
228 0     0 1   my $self = shift;
229 0           my ($choice1, $choice2, $question) = @_;
230              
231 0           $choice1 = uc $choice1; # default is capsed
232 0           $choice2 = lc $choice2; # non-default is lowercased
233              
234 0           Prophet::CLI->end_pager();
235 0           print "$question [$choice1/$choice2]: ";
236              
237 0           chomp( my $answer = );
238              
239 0           Prophet::CLI->start_pager();
240              
241 0           return $answer !~ /^$choice2$/i;
242             }
243              
244             =head2 prompt_Yn question
245              
246             Asks user the question and returns true if answer was positive or false
247             otherwise. Default answer is 'Yes' (returns true).
248              
249             =cut
250              
251             sub prompt_Yn {
252 0     0 1   my $self = shift;
253 0           my $msg = shift;
254              
255 0           return $self->prompt_choices( 'y', 'n', $msg );
256             }
257              
258             # Create a new [replica] config file section for the given replica if
259             # it hasn't been seen before (config section doesn't already exist)
260             sub record_replica_in_config {
261 0     0 0   my $self = shift;
262 0           my $replica_url = shift;
263 0           my $replica_uuid = shift;
264 0   0       my $url_variable = shift || 'url';
265              
266 0           my %previous_sources_by_uuid
267             = $self->app_handle->config->sources(
268             by_variable => 1,
269             variable => 'uuid',
270             );
271              
272 0           my $found_prev_replica = $previous_sources_by_uuid{$replica_uuid};
273              
274 0 0         if ( !$found_prev_replica ) {
    0          
275             # replica section doesn't exist at all; create a new one
276 0           my $url = $replica_url;
277 0           $self->app_handle->config->group_set(
278             $self->app_handle->config->replica_config_file,
279             [
280             {
281             key => "replica.$url.$url_variable",
282             value => $replica_url,
283             },
284             {
285             key => "replica.$url.uuid",
286             value => $replica_uuid,
287             },
288             ],
289             );
290             }
291             elsif ( $found_prev_replica ne $replica_url ) {
292             # We're publishing to a different place than where it was published
293             # to previously--we don't want to end up with a multivalue in the
294             # config file, so just replace the old value.
295 0           my $name = $self->app_handle->display_name_for_replica($replica_uuid);
296 0           $self->app_handle->config->set(
297             filename => $self->app_handle->config->replica_config_file,
298             key => "replica.$name.$url_variable",
299             value => $replica_url,
300             );
301             }
302             }
303              
304             =head2 print_usage
305              
306             Print the command's usage message to STDERR and die. Commands should
307             implement C, which returns the usage message.
308              
309             If the usage message method needs arguments passed in, use a closure.
310              
311             =cut
312              
313             sub print_usage {
314 0     0 1   my $self = shift;
315             my %args = (
316 0     0     usage_method => sub { $self->usage_msg },
317 0           @_,
318             );
319              
320 0           die $args{usage_method}();
321             }
322              
323             =head2 get_cmd_and_subcmd_names [no_type => 1]
324              
325             Gets the name of the script that was run and the primary commands that were
326             specified on the command-line. If a true boolean is passed in as C,
327             won't add '' to the subcmd if no type was passed in via the
328             primary commands.
329              
330             =cut
331              
332             sub get_cmd_and_subcmd_names {
333 0     0 1   my $self = shift;
334 0           my %args = @_;
335              
336 0           my $cmd = $self->cli->get_script_name;
337 0           my @primary_commands = @{ $self->context->primary_commands };
  0            
338              
339             # if primary commands was only length 1, the type was not specified
340             # and we should indicate that a type is expected
341             push @primary_commands, ''
342 0 0 0       if @primary_commands <= 1 && !$args{no_type};
343              
344 0           my $type_and_subcmd = join( q{ }, @primary_commands );
345              
346 0           return ($cmd, $type_and_subcmd);
347             }
348              
349             __PACKAGE__->meta->make_immutable;
350 39     39   249 no Any::Moose;
  39         67  
  39         195  
351              
352             1;
353