File Coverage

blib/lib/App/DuckPAN/Cmd/New.pm
Criterion Covered Total %
statement 18 122 14.7
branch 0 52 0.0
condition 0 8 0.0
subroutine 6 15 40.0
pod 0 1 0.0
total 24 198 12.1


line stmt bran cond sub pod time code
1             package App::DuckPAN::Cmd::New;
2             our $AUTHORITY = 'cpan:DDG';
3             # ABSTRACT: Take a name as input and generates a new, named Goodie or Spice instant answer skeleton
4             $App::DuckPAN::Cmd::New::VERSION = '1018';
5             # See the template/templates.yml file in the Goodie or Spice repository for the
6             # list of template-sets and files generated for them
7              
8 1     1   943 use Moo;
  1         2  
  1         4  
9             with qw( App::DuckPAN::Cmd );
10              
11 1     1   350 use MooX::Options protect_argv => 0;
  1         5  
  1         5  
12 1     1   1362 use Try::Tiny;
  1         2  
  1         75  
13 1     1   6 use List::MoreUtils 'any';
  1         2  
  1         19  
14              
15 1     1   915 use App::DuckPAN::TemplateDefinitions;
  1         4  
  1         32  
16              
17 1     1   6 no warnings 'uninitialized';
  1         2  
  1         1336  
18             ##########################
19             # Command line arguments #
20             ##########################
21              
22             # A 'template' for the user is equivalent to a 'template-set' for the program
23             option template => (
24             is => 'rwp',
25             format => 's',
26             default => 'default',
27             short => 't',
28             doc => 'template used to generate the instant answer skeleton (default: default)',
29             );
30              
31             option list_templates => (
32             is => 'ro',
33             short => 'l',
34             doc => 'list the available instant answer templates and exit',
35             );
36              
37             option cheatsheet => (
38             is => 'ro',
39             short => 'c',
40             doc => "create a Cheat Sheet (short for `--template cheatsheet'; valid only for Goodies)",
41             );
42              
43             option no_optionals => (
44             is => 'ro',
45             short => 'N',
46             doc => 'do not create any optional files from the chosen template',
47             );
48              
49             ##############
50             # Attributes #
51             ##############
52              
53             has _template_defs => (
54             is => 'ro',
55             init_arg => undef,
56             lazy => 1,
57             builder => 1,
58             doc => 'Template definitions for the templates for the current IA type',
59             );
60              
61             sub _build__template_defs {
62 0     0     my $self = shift;
63 0           my $template_defs;
64              
65             # Read the templates.yml file
66             try {
67 0     0     $template_defs = App::DuckPAN::TemplateDefinitions->new;
68             } catch {
69 0     0     my $error = $_;
70              
71 0 0         if ($error =~ /no such file/i) {
72             # Handle the 'no such file or directory' exception
73             # specially to show more information since it can be a
74             # common error for users with an older IA repository
75 0           my $type = $self->app->get_ia_type();
76              
77             $self->app->emit_and_exit(1,
78             "Template definitions file not found for " . $type->{name} .
79 0           " Instant Answers. You may need to pull the latest version " .
80             "of this repository.");
81             }
82             else {
83 0           $self->app->emit_and_exit(1, $error);
84             }
85 0           };
86              
87 0           return $template_defs;
88             }
89              
90             has _template_set => (
91             is => 'ro',
92             init_arg => undef,
93             lazy => 1,
94             builder => 1,
95             doc => 'The template set chosen by the user',
96             );
97              
98             sub _build__template_set {
99 0     0     my $self = shift;
100 0           my $type = $self->app->get_ia_type();
101 0           my $template_defs = $self->_template_defs;
102              
103             # Get the template chosen by the user
104 0           my $template_set = $template_defs->get_template_set($self->template);
105              
106 0 0         unless ($template_set) {
107             # We didn't find the template-set by the name. This could mean
108             # that there was a typo in the name or the user has an older IA
109             # repo and it not present in that version.
110             $self->app->emit_and_exit(1,
111             "'" . $self->template . "' is not a valid template for a " .
112 0           $type->{name} . " Instant Answer. You may need to update " .
113             "your repository to get the latest templates.\n" .
114             $self->_available_templates_message);
115             }
116              
117 0           return $template_set;
118             }
119              
120             ###########
121             # Methods #
122             ###########
123              
124             # Copy of @ARGV before MooX::Options processes it
125             my @ORIG_ARGV;
126              
127             before new_with_options => sub { @ORIG_ARGV = @ARGV };
128              
129             sub run {
130 0     0 0   my ($self, @args) = @_;
131              
132             # Check which IA repo we're in...
133 0           my $type = $self->app->get_ia_type();
134              
135 0           my $no_handler = 0;
136             # Process the --cheatsheet option
137 0 0 0       if ($self->cheatsheet || $self->template eq 'cheatsheet') {
138 0 0         if ($type->{name} ne 'Goodie') {
139 0           $self->app->emit_and_exit(1,
140             "Cheat Sheets can be created only in the Goodie " .
141             "Instant Answer repository.");
142             }
143              
144 0           $self->_set_template('cheatsheet');
145 0           $no_handler = 1;
146             }
147              
148             # Process the --list-templates option: List the template-set names and exit with success
149 0 0         $self->app->emit_and_exit(0, $self->_available_templates_message)
150             if $self->list_templates;
151              
152             # Gracefully handle the case where '--template' is the last argument
153 0 0 0       $self->app->emit_and_exit(
154             1,
155             "Please specify the template for your Instant Answer.\n" .
156             $self->_available_templates_message
157             ) if ($ORIG_ARGV[$#ORIG_ARGV] // '') eq '--template';
158              
159             # Get the template-set instance based on the command line arguments.
160 0           my $template_set = $self->_template_set();
161              
162 0           $self->app->emit_info('Creating a new ' . $template_set->description . '...');
163              
164             # Instant Answer name as parameter
165 0 0         my $entered_name = (@args) ? join(' ', @args) : $self->app->get_reply('Please enter a name for your Instant Answer: ');
166              
167             # Validate the entered name
168 0 0         $self->app->emit_and_exit(1, 'Must supply a name for your Instant Answer.') unless $entered_name;
169 0 0         $self->app->emit_and_exit(1,
170             "'$entered_name' is not a valid name for an Instant Answer. " .
171             'Please run the program again and provide a valid name.'
172             ) unless $entered_name =~ m@^( [a-zA-Z0-9\s] | (?<![:/])(::|/)(?![:/]) )+$@x;
173 0 0 0       $self->app->emit_and_exit(1,
174             'The name for this type of Instant Answer cannot contain package or path separators. ' .
175             'Please run the program again and provide a valid name.'
176             ) if !$template_set->subdir_support && $entered_name =~ m![/:]!;
177              
178 0           $entered_name =~ s/\//::/g; #change "/" to "::" for easier handling
179              
180 0           my $package_name = $self->app->phrase_to_camel($entered_name);
181 0           my ($name, $separated_name, $path, $lc_path) = ($package_name, $package_name, '', '');
182              
183 0           $separated_name =~ s/::/ /g;
184              
185 0 0         if ($package_name =~ m/::/) {
186 0           my @path_parts = split(/::/, $package_name);
187 0 0         if (scalar @path_parts > 1) {
188 0           $name = pop @path_parts;
189 0           $path = join("/", @path_parts);
190 0           $lc_path = join("/", map { $self->app->camel_to_underscore($_) } @path_parts);
  0            
191             }
192             else {
193 0           $self->app->emit_and_exit(1, "Malformed input. Please provide a properly formatted package name for your Instant Answer.");
194             }
195             }
196              
197 0           my $lc_name = $self->app->camel_to_underscore($name);
198 0 0         my $filepath = $path ? "$path/$name" : $name;
199 0 0         my $lc_filepath = $lc_path ? "$lc_path/$lc_name" : $lc_name;
200 0 0         if (scalar $lc_path) {
201 0           $lc_path =~ s/\//_/g; #safe to modify, we already used this in $lc_filepath
202 0           $lc_name = $lc_path . '_' . $lc_name;
203             }
204              
205 0 0         my @optional_templates = $self->_ask_optional_templates
206             unless $self->no_optionals;
207              
208 0           my %vars = (
209             ia_package_name => $package_name,
210             ia_name_separated => $separated_name,
211             ia_id => $lc_name,
212             ia_path => $filepath,
213             ia_path_lc => $lc_filepath,
214             );
215              
216             # Cheat sheets use hyphenated file names.
217 0 0         if ($self->template eq 'cheatsheet') {
218 0           my $underscored = $vars{ia_id} =~ s/_cheat_sheet//r;
219 0           $vars{cheat_sheet_hyphenated} = $underscored =~ s/_/-/gr;
220             }
221              
222             # If the Perl module every becomes optional, this should only run if the user
223             # requests one
224 0 0         unless($no_handler){
225 0           %vars = (%vars, %{$self->_config_handler});
  0            
226             }
227              
228             # Generate the instant answer files. The return value is a hash with
229             # information about the created files and any error that was encountered.
230 0           my %generate_result = $template_set->generate(\%vars, \@optional_templates);
231              
232             # Show the list of files that were successfully created
233 0           my @created_files = @{$generate_result{created_files}};
  0            
234 0           $self->app->emit_info('Created files:');
235 0           $self->app->emit_info(" $_") for @created_files;
236 0 0         $self->app->emit_info(' (none)') unless @created_files; # possible on error
237              
238 0 0         if (my $error = $generate_result{error}) {
239             # Remove the line number information if not in verbose mode.
240             # This error message would be seen mostly by users writing IAs
241             # for whom the line numbers don't add much value.
242 0 0         $error =~ s/.*\K at .* line \d+\.$//
243             unless $self->app->verbose;
244              
245 0           $self->app->emit_and_exit(1, $error)
246             }
247              
248 0           $self->app->emit_info('Success!');
249             }
250              
251             # Allow user to choose a handler
252             sub _config_handler {
253 0     0     my $self = shift;
254              
255 0           my @handlers = (
256             # Scalar-based
257             'remainder: (default) The query without the trigger words, spacing and case are preserved.',
258             'query_raw: Like remainder but with trigger words intact',
259             'query: Full query normalized with a single space between terms',
260             'query_lc: Like query but in lowercase',
261             'query_clean: Like query_lc but with non-alphanumeric characters removed',
262             'query_nowhitespace: All whitespace removed',
263             'query_nowhitespace_nodash: All whitespace and hyphens removed',
264             # Array-based
265             'matches: Returns an array of captured expression from a regular expression trigger',
266             'words: Like query_clean but returns an array of the terms split on whitespace',
267             'query_parts: Like query but returns an array of the terms split on whitespace',
268             'query_raw_parts: Like query_parts but array contains original whitespace elements'
269             );
270              
271 0           my $res = $self->app->get_reply(
272             'Which handler would you like to use to process the query?',
273             choices => \@handlers,
274             default => $handlers[0]
275             );
276              
277 0 0         unless($res =~ /^([^:]+)/){
278 0           $self->app->emit_and_exit(1, "Failed to extract handler from response: $res");
279             }
280 0           my $handler = $1;
281 0 0   0     my $var = (any {$handler eq $_} qw(words query_parts query_raw_parts matches)) ? '@' : '$';
  0            
282 0 0         my $trigger = $handler eq 'matches'
283             ? q{query => qr/trigger regex/}
284             : q{any => 'triggerword', 'trigger phrase'};
285              
286             return {
287 0           ia_handler => $handler,
288             ia_handler_var => $var,
289             ia_trigger => $trigger
290             };
291             }
292              
293             # Ask the user for which optional templates they want to use and return a list
294             # of the chosen templates
295             sub _ask_optional_templates {
296 0     0     my $self = shift;
297 0           my $template_set = $self->_template_set;
298 0           my $combinations = $template_set->optional_template_combinations;
299              
300             # no optional templates; nothing to do
301 0 0         return unless @$combinations;
302              
303 0           my $show_optional_templates = $self->app->ask_yn(
304             'Would you like to configure optional templates?',
305             default => 0,
306             );
307              
308 0 0         if ($show_optional_templates) {
309             # The choice strings to show to the user
310 0           my @choices;
311             # Mapping from a choice string to the corresponding template combination
312             my %choice_combinations;
313              
314 0           for my $combination (@$combinations) {
315             # Label of every template in the combination
316 0           my @labels = map { $_->label } @$combination;
  0            
317 0           my $choice = join(', ', @labels);
318              
319 0           push @choices, $choice;
320 0           $choice_combinations{$choice} = $combination;
321             }
322              
323 0           my $reply = $self->app->get_reply(
324             'Choose configuration',
325             choices => \@choices,
326             default => $choices[0],
327             );
328              
329 0           return @{$choice_combinations{$reply}};
  0            
330             }
331              
332 0           return;
333             }
334              
335             # Create a message with the list of available template-sets for this IA type
336             sub _available_templates_message {
337 0     0     my $self = shift;
338 0           my $template_defs = $self->_template_defs;
339             # template-sets, sorted by name
340             my @template_sets =
341 0           sort { $a->name cmp $b->name } $template_defs->get_template_sets;
  0            
342              
343 0           my $message = "Available templates:";
344              
345 0           for my $template_set (@template_sets) {
346 0           $message .= sprintf("\n %10s - %s",
347             $template_set->name,
348             $template_set->description,
349             );
350             }
351              
352 0           return $message;
353             }
354              
355             1;
356              
357             __END__
358              
359             =pod
360              
361             =head1 NAME
362              
363             App::DuckPAN::Cmd::New - Take a name as input and generates a new, named Goodie or Spice instant answer skeleton
364              
365             =head1 VERSION
366              
367             version 1018
368              
369             =head1 AUTHOR
370              
371             DuckDuckGo <open@duckduckgo.com>, Zach Thompson <zach@duckduckgo.com>, Zaahir Moolla <moollaza@duckduckgo.com>, Torsten Raudssus <torsten@raudss.us> L<https://raudss.us/>
372              
373             =head1 COPYRIGHT AND LICENSE
374              
375             This software is Copyright (c) 2013 by DuckDuckGo, Inc. L<https://duckduckgo.com/>.
376              
377             This is free software, licensed under:
378              
379             The Apache License, Version 2.0, January 2004
380              
381             =cut