Skip to content
This repository has been archived by the owner on Oct 15, 2022. It is now read-only.

Support for generic templates #279

Merged
merged 8 commits into from Dec 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/App/DuckPAN.pm
Expand Up @@ -207,6 +207,12 @@ sub get_reply {
return $return;
}

sub ask_yn {
Copy link
Contributor

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!

Copy link
Contributor Author

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:

  1. It adds a '[y/n]' at the end of the prompt with the default option capitalized and
  2. Returns a boolean value after parsing the reply

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.

Copy link
Contributor

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!

my ( $self, $prompt, %params ) = @_;

return $self->term->ask_yn( prompt => $prompt, %params );
}

has http => (
is => 'ro',
builder => '_build_http',
Expand Down
240 changes: 212 additions & 28 deletions lib/App/DuckPAN/Cmd/New.pm
@@ -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 => (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a shortform, -t would be nice --- I'll try and compile of features that can be added later

is => 'ro',
format => 's',
default => 'default',
doc => 'template used to generate the instant answer skeleton (default: default)',
);

option list_templates => (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise I think --list would be greatand/or-L`

is => 'ro',
doc => 'list the available instant answer templates and exit',
);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still like a --cheatsheet and/or -c option to go straight to a cheat sheet

##############
# 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);
Expand All @@ -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;
12 changes: 9 additions & 3 deletions lib/App/DuckPAN/Restart.pm
Expand Up @@ -4,6 +4,8 @@ package App::DuckPAN::Restart;
use File::Find::Rule;
use Filesys::Notify::Simple;

use App::DuckPAN::TemplateDefinitions;

use strict;

use Moo::Role;
Expand Down Expand Up @@ -66,11 +68,15 @@ sub _monitor_directories {
# Note: Could potentially be functionality added to App::DuckPAN
# which would return the directories involved in an IA
# (see https://github.com/duckduckgo/p5-app-duckpan/issues/200)
my $template_defs = App::DuckPAN::TemplateDefinitions->new;
my @templates = $template_defs->get_templates;
my %distinct_dirs;
while(my ($type, $io) = each %{$self->app->get_ia_type()->{templates}}){
next if $type eq 'test'; # skip the test dir?

for my $template (@templates) {
next unless $template->needs_restart;

# Get any subdirectories
my @d = File::Find::Rule->directory()->in($io->{out});
my @d = File::Find::Rule->directory()->in($template->output_directory);
# We don't know what templates will contain, e.g. subdiretories
++$distinct_dirs{$_} for @d;
}
Expand Down