This repository has been archived by the owner on Oct 15, 2022. It is now read-only.
Support for generic templates #279
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c2ac58d
New.pm: add ia_path_lc template var
69ca301
Support for generic templates
6ec0785
Restart.pm: Use template info for monitoring
1d345b7
New.pm: modify display of created files; wording
381b532
Generic Templates: user interface changes
1af5203
New.pm: Gracefully handle --template with no param
8fcd253
Generic templates: added test cases
e5cfa27
templates: Add subdir_support to TemplateSet.
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,149 @@ | ||
package App::DuckPAN::Cmd::New; | ||
# ABSTRACT: Take a name as input and generates a new, named Goodie or Spice instant answer skeleton | ||
|
||
# For Goodies: | ||
# - <name>.pm file is created in lib/DDG/Goodie | ||
# | ||
# For Spice: | ||
# - <name>.pm file is created in lib/DDG/Spice | ||
# - directory /share/spice/<name> is created | ||
# - <name.js> is created in /share/spice/<name> | ||
# - <name.handlebars> is created in /share/spice/<name> | ||
# See the template/templates.yml file in the Goodie or Spice repository for the | ||
# list of template-sets and files generated for them | ||
|
||
use Moo; | ||
with qw( App::DuckPAN::Cmd ); | ||
|
||
use MooX::Options protect_argv => 0; | ||
use Text::Xslate qw(mark_raw); | ||
use Path::Tiny; | ||
use Try::Tiny; | ||
|
||
use App::DuckPAN::TemplateDefinitions; | ||
|
||
########################## | ||
# Command line arguments # | ||
########################## | ||
|
||
# A 'template' for the user is equivalent to a 'template-set' for the program | ||
option template => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a shortform, |
||
is => 'ro', | ||
format => 's', | ||
default => 'default', | ||
doc => 'template used to generate the instant answer skeleton (default: default)', | ||
); | ||
|
||
option list_templates => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likewise I think |
||
is => 'ro', | ||
doc => 'list the available instant answer templates and exit', | ||
); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd still like a |
||
############## | ||
# Attributes # | ||
############## | ||
|
||
has _template_defs => ( | ||
is => 'ro', | ||
init_arg => undef, | ||
lazy => 1, | ||
builder => 1, | ||
doc => 'Template definitions for the templates for the current IA type', | ||
); | ||
|
||
sub _build__template_defs { | ||
my $self = shift; | ||
my $template_defs; | ||
|
||
# Read the templates.yml file | ||
try { | ||
$template_defs = App::DuckPAN::TemplateDefinitions->new; | ||
} catch { | ||
my $error = $_; | ||
|
||
if ($error =~ /no such file/i) { | ||
# Handle the 'no such file or directory' exception | ||
# specially to show more information since it can be a | ||
# common error for users with an older IA repository | ||
my $type = $self->app->get_ia_type(); | ||
|
||
$self->app->emit_and_exit(-1, | ||
"Template definitions file not found for " . $type->{name} . | ||
" Instant Answers. You may need to pull the latest version " . | ||
"of this repository."); | ||
} else { | ||
$self->app->emit_and_exit(-1, $error); | ||
} | ||
}; | ||
|
||
return $template_defs; | ||
} | ||
|
||
has _template_set => ( | ||
is => 'ro', | ||
init_arg => undef, | ||
lazy => 1, | ||
builder => 1, | ||
doc => 'The template set chosen by the user', | ||
); | ||
|
||
sub _build__template_set { | ||
my $self = shift; | ||
my $type = $self->app->get_ia_type(); | ||
my $template_defs = $self->_template_defs; | ||
|
||
# Get the template chosen by the user | ||
my $template_set = $template_defs->get_template_set($self->template); | ||
|
||
unless ($template_set) { | ||
# We didn't find the template-set by the name. This could mean | ||
# that there was a typo in the name or the user has an older IA | ||
# repo and it not present in that version. | ||
$self->app->emit_and_exit(-1, | ||
"'" . $self->template . "' is not a valid template for a " . | ||
$type->{name} . " Instant Answer. You may need to update " . | ||
"your repository to get the latest templates.\n" . | ||
$self->_available_templates_message); | ||
} | ||
|
||
return $template_set; | ||
} | ||
|
||
########### | ||
# Methods # | ||
########### | ||
|
||
# Copy of @ARGV before MooX::Options processes it | ||
my @ORIG_ARGV; | ||
|
||
before new_with_options => sub { @ORIG_ARGV = @ARGV }; | ||
|
||
sub run { | ||
my ($self, @args) = @_; | ||
|
||
# Check which IA repo we're in... | ||
my $type = $self->app->get_ia_type(); | ||
|
||
# Process the --list-templates option: List the template-set names and exit with success | ||
$self->app->emit_and_exit(0, $self->_available_templates_message) | ||
if $self->list_templates; | ||
|
||
# Gracefully handle the case where '--template' is the last argument | ||
$self->app->emit_and_exit( | ||
1, | ||
"Please specify the template for your Instant Answer.\n" . | ||
$self->_available_templates_message | ||
) if ($ORIG_ARGV[$#ORIG_ARGV] // '') eq '--template'; | ||
|
||
# Get the template-set instance based on the command line arguments. | ||
my $template_set = $self->_template_set(); | ||
|
||
$self->app->emit_info("Creating a new " . $template_set->description . "..."); | ||
|
||
# Instant Answer name as parameter | ||
my $entered_name = (@args) ? join(' ', @args) : $self->app->get_reply('Please enter a name for your Instant Answer: '); | ||
|
||
# Validate the entered name | ||
$self->app->emit_and_exit(-1, "Must supply a name for your Instant Answer.") unless $entered_name; | ||
$self->app->emit_and_exit(-1, | ||
"'$entered_name' is not a valid name for an Instant Answer. " . | ||
"Please run the program again and provide a valid name." | ||
) unless $entered_name =~ m!^[/a-zA-Z0-9\s]+$!; | ||
$self->app->emit_and_exit(-1, | ||
"The name for this type of Instant Answer cannot contain path separators. " . | ||
"Please run the program again and provide a valid name." | ||
) if !$template_set->subdir_support && $entered_name =~ m!/!; | ||
|
||
$entered_name =~ s/\//::/g; #change "/" to "::" for easier handling | ||
|
||
my $package_name = $self->app->phrase_to_camel($entered_name); | ||
|
@@ -53,32 +170,99 @@ sub run { | |
$lc_name = $lc_path . "_" . $lc_name; | ||
} | ||
|
||
$self->app->emit_and_exit(-1, "No templates exist for this IA Type: " . $type->{name}) if (!defined $type->{templates}); | ||
my @optional_templates = $self->_ask_optional_templates; | ||
|
||
my %template_info = %{$type->{templates}}; | ||
my $tx = Text::Xslate->new(); | ||
my %files = ( | ||
test => ["$filepath.t"], | ||
code => ["$filepath.pm"], | ||
handlebars => [$lc_filepath, "$lc_name.handlebars"], | ||
js => [$lc_filepath, "$lc_name.js"]); | ||
my %vars = ( | ||
ia_package_name => $package_name, | ||
ia_name_separated => $separated_name, | ||
ia_id => $lc_name, | ||
ia_path => $filepath, | ||
ia_path_lc => $lc_filepath, | ||
); | ||
foreach my $template_type (sort keys %template_info) { | ||
my ($source, $dest) = ($template_info{$template_type}{in}, $template_info{$template_type}{out}); | ||
$self->app->emit_and_exit(-1, 'Template does not exist: ' . $source) unless ($source->exists); | ||
# Update dest based on type: | ||
$dest = $dest->child(@{$files{$template_type}}); | ||
$self->app->emit_and_exit(-1, 'File already exists: "' . $dest->basename . '" in ' . $dest->parent) if ($dest->exists); | ||
my $content = $tx->render("$source", \%vars); | ||
$dest->touchpath->append_utf8($content); #create file path and append to file | ||
$self->app->emit_info("Created file: $dest"); | ||
|
||
# Generate the instant answer files. The return value is a hash with | ||
# information about the created files and any error that was encountered. | ||
my %generate_result = $template_set->generate(\%vars, \@optional_templates); | ||
|
||
# Show the list of files that were successfully created | ||
my @created_files = @{$generate_result{created_files}}; | ||
$self->app->emit_info("Created files:"); | ||
$self->app->emit_info(" $_") for @created_files; | ||
$self->app->emit_info(" (none)") unless @created_files; # possible on error | ||
|
||
if (my $error = $generate_result{error}) { | ||
# Remove the line number information if not in verbose mode. | ||
# This error message would be seen mostly by users writing IAs | ||
# for whom the line numbers don't add much value. | ||
$error =~ s/.*\K at .* line \d+\.$// | ||
unless $self->app->verbose; | ||
|
||
$self->app->emit_and_exit(-1, $error) | ||
} | ||
$self->app->emit_info("Successfully created " . $type->{name} . ": $package_name"); | ||
|
||
$self->app->emit_info("Success!"); | ||
} | ||
|
||
# Ask the user for which optional templates they want to use and return a list | ||
# of the chosen templates | ||
sub _ask_optional_templates { | ||
my $self = shift; | ||
my $template_set = $self->_template_set; | ||
my $combinations = $template_set->optional_template_combinations; | ||
|
||
# no optional templates; nothing to do | ||
return unless @$combinations; | ||
|
||
my $show_optional_templates = $self->app->ask_yn( | ||
'Would you like to configure optional templates?', | ||
default => 0, | ||
); | ||
|
||
if ($show_optional_templates) { | ||
# The choice strings to show to the user | ||
my @choices; | ||
# Mapping from a choice string to the corresponding template combination | ||
my %choice_combinations; | ||
|
||
for my $combination (@$combinations) { | ||
# Label of every template in the combination | ||
my @labels = map { $_->label } @$combination; | ||
my $choice = join(', ', @labels); | ||
|
||
push @choices, $choice; | ||
$choice_combinations{$choice} = $combination; | ||
} | ||
|
||
my $reply = $self->app->get_reply( | ||
'Choose configuration', | ||
choices => \@choices, | ||
default => $choices[0], | ||
); | ||
|
||
return @{$choice_combinations{$reply}}; | ||
} | ||
|
||
return; | ||
} | ||
|
||
# Create a message with the list of available template-sets for this IA type | ||
sub _available_templates_message { | ||
my $self = shift; | ||
my $template_defs = $self->_template_defs; | ||
# template-sets, sorted by name | ||
my @template_sets = | ||
sort { $a->name cmp $b->name } $template_defs->get_template_sets; | ||
|
||
my $message = "Available templates:"; | ||
|
||
for my $template_set (@template_sets) { | ||
$message .= sprintf("\n %10s - %s", | ||
$template_set->name, | ||
$template_set->description, | ||
); | ||
} | ||
|
||
return $message; | ||
} | ||
|
||
1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not just use get_reply?Forget this comment!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that using
Term::UI::ask_yn
is useful since it does a couple of things automatically to save some code:But I understand your concern about defining a sub that is used only once, so I'll go with anything you're comfortable having in the code base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree @srvsh. Ignore my question!