From c2ac58d0bac53efb30cf7f832cbadb14341c3a61 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Thu, 10 Dec 2015 03:16:43 +0530 Subject: [PATCH 1/8] New.pm: add ia_path_lc template var --- lib/App/DuckPAN/Cmd/New.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index f11d258..02e0df0 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -67,6 +67,7 @@ sub run { 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}); From 69ca30161f2e3e7bb8fa3c32e3c6c2ce4245a6f3 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Thu, 26 Nov 2015 18:33:36 +0530 Subject: [PATCH 2/8] Support for generic templates Template information is now stored in respective IA packages for easier extensibility. There's also support for optional files that users can decide whether to generate. --- lib/App/DuckPAN.pm | 7 ++ lib/App/DuckPAN/Cmd/New.pm | 160 ++++++++++++++++++++----- lib/App/DuckPAN/Template.pm | 95 +++++++++++++++ lib/App/DuckPAN/TemplateDefinitions.pm | 129 ++++++++++++++++++++ lib/App/DuckPAN/TemplateSet.pm | 77 ++++++++++++ 5 files changed, 440 insertions(+), 28 deletions(-) create mode 100644 lib/App/DuckPAN/Template.pm create mode 100644 lib/App/DuckPAN/TemplateDefinitions.pm create mode 100644 lib/App/DuckPAN/TemplateSet.pm diff --git a/lib/App/DuckPAN.pm b/lib/App/DuckPAN.pm index 9d85f6b..c561958 100644 --- a/lib/App/DuckPAN.pm +++ b/lib/App/DuckPAN.pm @@ -207,6 +207,13 @@ sub get_reply { return $return; } +sub get_reply_yes_no { + my ( $self, $prompt, %params ) = @_; + my $reply = $self->get_reply($prompt . " [y/n]", %params); + + return $reply =~ /^y/i; +} + has http => ( is => 'ro', builder => '_build_http', diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index 02e0df0..ccd1263 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -1,22 +1,65 @@ package App::DuckPAN::Cmd::New; # ABSTRACT: Take a name as input and generates a new, named Goodie or Spice instant answer skeleton -# For Goodies: -# - .pm file is created in lib/DDG/Goodie -# -# For Spice: -# - .pm file is created in lib/DDG/Spice -# - directory /share/spice/ is created -# - is created in /share/spice/ -# - is created in /share/spice/ +# See the template/templates.yml file in the Goodie or Spice repository for the +# list of sub-types 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; + +# A 'subtype' for the user is equivalent to a 'template set' for the program +option subtype => ( + is => 'ro', + format => 's', + default => 'default', + doc => 'sub-type of instant answer (default: default)', +); + +option list_subtypes => ( + is => 'ro', + doc => 'list the available instant answer sub-types and exit', +); + +has _template_defs => ( + is => 'ro', + init_arg => undef, + lazy => 1, + builder => 1, + doc => 'template definitions for the subtypes of this 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; +} sub run { my ($self, @args) = @_; @@ -24,6 +67,15 @@ sub run { # Check which IA repo we're in... my $type = $self->app->get_ia_type(); + # Process the --list-subtypes option: List the sub-type names and exit with success + $self->app->emit_and_exit(0, $self->_available_subtypes_message) + if $self->list_subtypes; + + # Get the template set instance based on the command line arguments. + my $template_set = $self->_get_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: '); $self->app->emit_and_exit(-1, "Must supply a name for your Instant Answer.") unless $entered_name; @@ -53,15 +105,6 @@ 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 %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, @@ -69,17 +112,78 @@ sub run { 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"); + + # Ask which optional templates to create + my @optional_templates; + + for my $template (@{$template_set->optional_templates}) { + if ($self->app->get_reply_yes_no('Create ' . $template->description . '?')) { + push @optional_templates, $template; + } } + + my %generate_result; + + # Generate the instant answer files. The return value is a hash with + # information about the created files and any error that was encountered. + %generate_result = $template_set->generate(\%vars, \@optional_templates); + + # Show the list of files that were successfully created + $self->app->emit_info("Created file: $_") for @{$generate_result{created_files}}; + + if (my $error = $generate_result{error}) { + # Remove the line number information if not in verbose mode + $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"); } +# Get the template set from the '--subtype' command line argument +sub _get_template_set { + my $self = shift; + my $type = $self->app->get_ia_type(); + my $template_defs = $self->_template_defs; + + # Get the template set for the chosen sub-type + my $template_set = $template_defs->get_template_set($self->subtype); + + unless ($template_set) { + # We didn't find the template set for the chosen sub-type. This + # could mean that there was a typo in the sub-type name or the + # user has an older IA repo and this sub-type was not present + # in that version. + $self->app->emit_and_exit(-1, + "'" . $self->subtype . "' is not a valid sub-type of a " . + $type->{name} . " Instant Answer. You may need to update " . + "your repository to get the latest sub-type definitions.\n" . + $self->_available_subtypes_message); + } + + return $template_set; +} + +# Create a message with the list of available types (aka template sets) for this IA type +sub _available_subtypes_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 $available_subtypes_msg = "Available sub-types:"; + + for my $template_set (@template_sets) { + $available_subtypes_msg .= sprintf("\n %10s - %s", + $template_set->name, + $template_set->description, + ); + } + + return $available_subtypes_msg; +} + 1; diff --git a/lib/App/DuckPAN/Template.pm b/lib/App/DuckPAN/Template.pm new file mode 100644 index 0000000..ca20116 --- /dev/null +++ b/lib/App/DuckPAN/Template.pm @@ -0,0 +1,95 @@ +package App::DuckPAN::Template; +# ABSTRACT: Template to generate one file of an Instant Answer + +# An Instant Answer has multiple templates, each of which can be used +# to generate one output file. + +use Moo; + +use Try::Tiny; +use Text::Xslate; +use Path::Tiny qw(path); + +use namespace::clean; + +has name => ( + is => 'ro', + required => 1, + doc => 'Name of the template', +); + +has description => ( + is => 'ro', + required => 1, + doc => 'Description of the template', +); + +has input_file => ( + is => 'ro', + required => 1, + doc => 'Path of the input file for the template', +); + +has output_file => ( + is => 'ro', + required => 1, + doc => 'Path of the output file for the template. ' . + 'This string is rendered through Text::Xslate to get the final path.', +); + +has output_directory => ( + is => 'ro', + init_arg => undef, + lazy => 1, + builder => 1, + doc => 'Directory known to contain all of the generated template output files and subdirectories', +); + +sub _build_output_directory { + my ($self) = @_; + my $out_dir = path($self->output_file); + + # Get the directory that is certain to be the closest ancestor of the + # output file i.e., keep removing directory parts from the right till the + # path does not contain any Text::Xslate syntax. + $out_dir = $out_dir->parent while $out_dir =~ /<:/; + + return $out_dir; +} + +has needs_restart => ( + is => 'ro', + required => 1, + doc => 'Does the server need to be restarted when the output file is modified? (boolean)', +); + +# Create the output file from the input file +sub generate { + my ($self, $vars) = @_; + + # Increased verbosity to help while writing templates + my $tx = Text::Xslate->new(type => 'text', verbose => 2); + my $input_file = path($self->input_file); + + # (should not occur for users) + die "Template input file '$input_file' not found" unless $input_file->exists; + + # The output file path is a Text::Xslate template, so we generate the + # actual path here + my $output_file = path($tx->render_string($self->output_file, $vars)); + + die "Template output file '" . $output_file . "' already exists" if $output_file->exists; + + my $content = $tx->render($input_file, $vars); + + try { + path($output_file)->touchpath->spew_utf8($content); + } catch { + die "Error creating output file '$output_file' from template: $_"; + }; + + return $output_file; +} + +1; + diff --git a/lib/App/DuckPAN/TemplateDefinitions.pm b/lib/App/DuckPAN/TemplateDefinitions.pm new file mode 100644 index 0000000..00b9403 --- /dev/null +++ b/lib/App/DuckPAN/TemplateDefinitions.pm @@ -0,0 +1,129 @@ +package App::DuckPAN::TemplateDefinitions; +# ABSTRACT: Parse the template definitions file to create templates and template sets + +use Moo; + +use Try::Tiny; +use Path::Tiny; +use YAML::XS qw(LoadFile); + +use App::DuckPAN::Template; +use App::DuckPAN::TemplateSet; + +use namespace::clean; + +has templates_yml => ( + is => 'ro', + required => 1, + default => sub { path('template', 'templates.yml') }, + doc => 'Path to the YAML file with template definitions', +); + +has _templates_data => ( + is => 'rwp', + init_arg => undef, + doc => 'Raw template definitions read from the template definitions file', +); + +has _template_map => ( + is => 'ro', + lazy => 1, + builder => 1, + init_arg => undef, + doc => 'Hashref of tempate name => App::DuckPAN::Template instances ' . + 'built from the templates_yml file', +); + +sub _build__template_map { + my $self = shift; + my $template_root = path($self->templates_yml)->parent; + my $data = $self->_templates_data->{templates}; + my %template_map; + + for my $name (keys %$data) { + my $template_data = $data->{$name}; + + my $template = App::DuckPAN::Template->new( + name => $name, + description => $template_data->{description}, + input_file => path($template_root, $template_data->{input}), + output_file => path($template_data->{output}), + needs_restart => $template_data->{needs_restart}, + ); + + $template_map{$name} = $template; + } + + return \%template_map; +} + +has _template_sets => ( + is => 'ro', + builder => 1, + lazy => 1, + doc => 'hashref of template set name to App::DuckPAN::TemplateSet instances ' . + 'built from the templates_yml file', +); + +sub _build__template_sets { + my $self = shift; + my $sets_data = $self->_templates_data->{template_sets}; + my %template_sets; + + for my $name (keys %$sets_data) { + my $data = $sets_data->{$name}; + my @required = @{$data->{required} // []}; + my @optional = @{$data->{optional} // []}; + + # check if all templates in this set are defined + for my $template_name (@required, @optional) { + die "Template '$template_name' not defined in " . $self->templates_yml + unless $self->_template_map->{$template_name}; + } + + my $template_set = App::DuckPAN::TemplateSet->new( + name => $name, + description => $data->{description}, + required_templates => [ @{$self->_template_map}{@required} ], + optional_templates => [ @{$self->_template_map}{@optional} ], + ); + + $template_sets{$name} = $template_set; + } + + return \%template_sets; +} + +# Get a template set by name +sub get_template_set { + my ($self, $name) = @_; + + return $self->_template_sets->{$name}; +} + +# Get all available template set names +sub get_template_sets { + my ($self) = @_; + + return values %{$self->_template_sets}; +} + +# Get a list of all templates +sub get_templates { + my ($self) = @_; + + return values %{$self->_template_map}; +} + +sub BUILD { + my $self = shift; + + try { + $self->_set__templates_data(LoadFile($self->templates_yml)); + } catch { + die "Error loading template definitions file " . $self->templates_yml . ": $_"; + }; +} + +1; + diff --git a/lib/App/DuckPAN/TemplateSet.pm b/lib/App/DuckPAN/TemplateSet.pm new file mode 100644 index 0000000..bebe622 --- /dev/null +++ b/lib/App/DuckPAN/TemplateSet.pm @@ -0,0 +1,77 @@ +package App::DuckPAN::TemplateSet; +# ABSTRACT: Set of templates an Instant Answer + +# A group of templates is a template set. Conceptually this represents a +# sub-type of an instant answer type. For example, a Goodie can be a standard +# Goodie or a Cheat Sheet goodie, each of which corresponds to a template set. +# +# Each template set can have required and optional templates. 'required' templates +# are always used to generate output files, while the user's confirmation is +# needed before each optional template is processed. + +use Moo; + +use Try::Tiny; +use List::Util qw(all); + +use namespace::clean; + +has name => ( + is => 'ro', + required => 1, + doc => 'Name of the template set', +); + +has description => ( + is => 'ro', + required => 1, + doc => 'Description of the template set', +); + +has required_templates => ( + is => 'ro', + required => 1, + doc => 'Arrayref of App::DuckPAN::Template instances that represent mandatory templates', +); + +has optional_templates => ( + is => 'ro', + required => 1, + doc => 'Arrayref of App::DuckPAN::Template instances that represent optional templates', +); + +# check if all templates in the array @templates are optional +sub _are_templates_optional { + my ($self, @templates) = @_; + my %optional_templates = map { ($_ => 1) } @{$self->optional_templates}; + + return all { $optional_templates{$_} } @templates; +} + +# Use the template to generate necessary files. Takes 2 parameters: +# $vars: variables to substitute in the templates +# $optional_templates: arrayref of template instances taken from $self->optional_templates +sub generate { + my ($self, $vars, $optional_templates) = @_; + my @created_files; + my $error; + + # Verify that $optional_templates has templates from within $self->optional_templates + $self->_are_templates_optional(@$optional_templates) + or die "Unknown template(s) in \$optional_templates"; + + for my $template (@{$self->required_templates}, @$optional_templates) { + try { + push @created_files, $template->generate($vars); + } catch { + $error = $_; + }; + + last if $error; + } + + return (created_files => [ @created_files ], error => $error); +} + +1; + From 6ec078528bb38606856d1b498e32f2579a803910 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Thu, 26 Nov 2015 18:34:03 +0530 Subject: [PATCH 3/8] Restart.pm: Use template info for monitoring of directories which need the server to be restarted --- lib/App/DuckPAN/Restart.pm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/App/DuckPAN/Restart.pm b/lib/App/DuckPAN/Restart.pm index 65daaf2..17b1f9e 100644 --- a/lib/App/DuckPAN/Restart.pm +++ b/lib/App/DuckPAN/Restart.pm @@ -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; @@ -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; } From 1d345b72784954e8cf3d4d800de415a675135cbb Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Sun, 29 Nov 2015 02:36:46 +0530 Subject: [PATCH 4/8] New.pm: modify display of created files; wording - Use the word 'template' instead of 'sub-type' - Renamed get_reply_yes_no() to ask_yn() for consitency with Term::UI. - Use tabs instead of spaces (introduced in an earlier commit) in New.pm for consitency - Other minor changes to names and comments --- lib/App/DuckPAN.pm | 5 +-- lib/App/DuckPAN/Cmd/New.pm | 80 ++++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/lib/App/DuckPAN.pm b/lib/App/DuckPAN.pm index c561958..ae82144 100644 --- a/lib/App/DuckPAN.pm +++ b/lib/App/DuckPAN.pm @@ -207,11 +207,10 @@ sub get_reply { return $return; } -sub get_reply_yes_no { +sub ask_yn { my ( $self, $prompt, %params ) = @_; - my $reply = $self->get_reply($prompt . " [y/n]", %params); - return $reply =~ /^y/i; + return $self->term->ask_yn( prompt => $prompt, %params ); } has http => ( diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index ccd1263..5e2051a 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -2,7 +2,7 @@ package App::DuckPAN::Cmd::New; # ABSTRACT: Take a name as input and generates a new, named Goodie or Spice instant answer skeleton # See the template/templates.yml file in the Goodie or Spice repository for the -# list of sub-types and files generated for them +# list of template-sets and files generated for them use Moo; with qw( App::DuckPAN::Cmd ); @@ -12,17 +12,17 @@ use Try::Tiny; use App::DuckPAN::TemplateDefinitions; -# A 'subtype' for the user is equivalent to a 'template set' for the program -option subtype => ( +# A 'template' for the user is equivalent to a 'template-set' for the program +option template => ( is => 'ro', format => 's', default => 'default', - doc => 'sub-type of instant answer (default: default)', + doc => 'template used to generate the instant answer skeleton (default: default)', ); -option list_subtypes => ( +option list_templates => ( is => 'ro', - doc => 'list the available instant answer sub-types and exit', + doc => 'list the available instant answer templates and exit', ); has _template_defs => ( @@ -30,7 +30,7 @@ has _template_defs => ( init_arg => undef, lazy => 1, builder => 1, - doc => 'template definitions for the subtypes of this IA type', + doc => 'template definitions for the templates for the current IA type', ); sub _build__template_defs { @@ -67,11 +67,11 @@ sub run { # Check which IA repo we're in... my $type = $self->app->get_ia_type(); - # Process the --list-subtypes option: List the sub-type names and exit with success - $self->app->emit_and_exit(0, $self->_available_subtypes_message) - if $self->list_subtypes; + # 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; - # Get the template set instance based on the command line arguments. + # Get the template-set instance based on the command line arguments. my $template_set = $self->_get_template_set(); $self->app->emit_info("Creating a new " . $template_set->description . "..."); @@ -117,24 +117,29 @@ sub run { my @optional_templates; for my $template (@{$template_set->optional_templates}) { - if ($self->app->get_reply_yes_no('Create ' . $template->description . '?')) { - push @optional_templates, $template; - } + if ($self->app->ask_yn('Create ' . $template->description . '?', + default => 0)) { + push @optional_templates, $template; + } + print "\n"; } - my %generate_result; - # Generate the instant answer files. The return value is a hash with # information about the created files and any error that was encountered. - %generate_result = $template_set->generate(\%vars, \@optional_templates); + my %generate_result = $template_set->generate(\%vars, \@optional_templates); # Show the list of files that were successfully created - $self->app->emit_info("Created file: $_") for @{$generate_result{created_files}}; + 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 - $error =~ s/.*\K at .* line \d+\.$// - unless $self->app->verbose; + # 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) } @@ -142,48 +147,47 @@ sub run { $self->app->emit_info("Successfully created " . $type->{name} . ": $package_name"); } -# Get the template set from the '--subtype' command line argument +# Get the template-set from the '--template' command line argument sub _get_template_set { my $self = shift; my $type = $self->app->get_ia_type(); my $template_defs = $self->_template_defs; - # Get the template set for the chosen sub-type - my $template_set = $template_defs->get_template_set($self->subtype); + # 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 for the chosen sub-type. This - # could mean that there was a typo in the sub-type name or the - # user has an older IA repo and this sub-type was not present - # in that version. + # 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->subtype . "' is not a valid sub-type of a " . + "'" . $self->template . "' is not a valid template for a " . $type->{name} . " Instant Answer. You may need to update " . - "your repository to get the latest sub-type definitions.\n" . - $self->_available_subtypes_message); + "your repository to get the latest templates.\n" . + $self->_available_templates_message); } return $template_set; } -# Create a message with the list of available types (aka template sets) for this IA type -sub _available_subtypes_message { +# 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 + # template-sets, sorted by name my @template_sets = - sort { $a->name cmp $b->name} $template_defs->get_template_sets; + sort { $a->name cmp $b->name } $template_defs->get_template_sets; - my $available_subtypes_msg = "Available sub-types:"; + my $message = "Available templates:"; for my $template_set (@template_sets) { - $available_subtypes_msg .= sprintf("\n %10s - %s", + $message .= sprintf("\n %10s - %s", $template_set->name, $template_set->description, ); } - return $available_subtypes_msg; + return $message; } 1; From 381b532de50bc14b49e5a95ec3485ddc8710c3c9 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Tue, 1 Dec 2015 23:45:28 +0530 Subject: [PATCH 5/8] Generic Templates: user interface changes 1. Ask user if they want to see optional templates 2. Show all possible combinations of optional templates in a menu Also, made '_template_set' as an attribute of App::DuckPAN::New instead of using a passing it around to methods. --- lib/App/DuckPAN/Cmd/New.pm | 114 ++++++++++++++++++------- lib/App/DuckPAN/Template.pm | 4 +- lib/App/DuckPAN/TemplateDefinitions.pm | 2 +- lib/App/DuckPAN/TemplateSet.pm | 70 +++++++++++++-- 4 files changed, 148 insertions(+), 42 deletions(-) diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index 5e2051a..03c2175 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -12,6 +12,10 @@ 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 => ( is => 'ro', @@ -25,12 +29,16 @@ option list_templates => ( doc => 'list the available instant answer templates and exit', ); +############## +# Attributes # +############## + has _template_defs => ( is => 'ro', init_arg => undef, lazy => 1, builder => 1, - doc => 'template definitions for the templates for the current IA type', + doc => 'Template definitions for the templates for the current IA type', ); sub _build__template_defs { @@ -61,6 +69,40 @@ sub _build__template_defs { 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 # +########### + sub run { my ($self, @args) = @_; @@ -72,7 +114,7 @@ sub run { if $self->list_templates; # Get the template-set instance based on the command line arguments. - my $template_set = $self->_get_template_set(); + my $template_set = $self->_template_set(); $self->app->emit_info("Creating a new " . $template_set->description . "..."); @@ -105,6 +147,8 @@ sub run { $lc_name = $lc_path . "_" . $lc_name; } + my @optional_templates = $self->_ask_optional_templates; + my %vars = ( ia_package_name => $package_name, ia_name_separated => $separated_name, @@ -113,17 +157,6 @@ sub run { ia_path_lc => $lc_filepath, ); - # Ask which optional templates to create - my @optional_templates; - - for my $template (@{$template_set->optional_templates}) { - if ($self->app->ask_yn('Create ' . $template->description . '?', - default => 0)) { - push @optional_templates, $template; - } - print "\n"; - } - # 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); @@ -139,7 +172,7 @@ sub run { # 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; + unless $self->app->verbose; $self->app->emit_and_exit(-1, $error) } @@ -147,27 +180,46 @@ sub run { $self->app->emit_info("Successfully created " . $type->{name} . ": $package_name"); } -# Get the template-set from the '--template' command line argument -sub _get_template_set { +# 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 $type = $self->app->get_ia_type(); - my $template_defs = $self->_template_defs; + my $template_set = $self->_template_set; + my $combinations = $template_set->optional_template_combinations; - # Get the template chosen by the user - my $template_set = $template_defs->get_template_set($self->template); + # no optional templates; nothing to do + return unless @$combinations; - 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); + 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 $template_set; + return; } # Create a message with the list of available template-sets for this IA type @@ -176,7 +228,7 @@ sub _available_templates_message { 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; + sort { $a->name cmp $b->name } $template_defs->get_template_sets; my $message = "Available templates:"; diff --git a/lib/App/DuckPAN/Template.pm b/lib/App/DuckPAN/Template.pm index ca20116..39ad569 100644 --- a/lib/App/DuckPAN/Template.pm +++ b/lib/App/DuckPAN/Template.pm @@ -18,10 +18,10 @@ has name => ( doc => 'Name of the template', ); -has description => ( +has label => ( is => 'ro', required => 1, - doc => 'Description of the template', + doc => 'Label of the template', ); has input_file => ( diff --git a/lib/App/DuckPAN/TemplateDefinitions.pm b/lib/App/DuckPAN/TemplateDefinitions.pm index 00b9403..5b238bf 100644 --- a/lib/App/DuckPAN/TemplateDefinitions.pm +++ b/lib/App/DuckPAN/TemplateDefinitions.pm @@ -45,7 +45,7 @@ sub _build__template_map { my $template = App::DuckPAN::Template->new( name => $name, - description => $template_data->{description}, + label => $template_data->{label}, input_file => path($template_root, $template_data->{input}), output_file => path($template_data->{output}), needs_restart => $template_data->{needs_restart}, diff --git a/lib/App/DuckPAN/TemplateSet.pm b/lib/App/DuckPAN/TemplateSet.pm index bebe622..3d6cefd 100644 --- a/lib/App/DuckPAN/TemplateSet.pm +++ b/lib/App/DuckPAN/TemplateSet.pm @@ -13,6 +13,7 @@ use Moo; use Try::Tiny; use List::Util qw(all); +use Algorithm::Combinatorics qw(combinations); use namespace::clean; @@ -40,6 +41,59 @@ has optional_templates => ( doc => 'Arrayref of App::DuckPAN::Template instances that represent optional templates', ); +has optional_template_combinations => ( + is => 'ro', + lazy => 1, + builder => 1, + init_arg => undef, + doc => 'Arrayref of possible optional template combinations, ' . + 'which themselves are arrayrefs of templates.', +); + +# All possible template combinations are generated from the list in the +# 'optional_templates' attribute. They are sorted by the following rules: +# +# 1. Combinations are sorted by length (ascending) +# 2. In each combination, the templates are sorted in the same order that +# they appear in the 'optional_templates' attribute +# 3. Combinations of the same length are then sorted based on the same rule +sub _build_optional_template_combinations { + my $self = shift; + my @templates = @{$self->optional_templates}; + + # Map of template -> position in template list + my %template_pos = map { ($templates[$_] => $_) } 0..$#templates; + + # All combinations of all lengths; sorted + my @template_combinations; + + for my $length (1..@templates) { + # Generate all combinations of length $length + my @combinations = combinations(\@templates, $length); + + # Sort the tempates in each combination + for my $combination (@combinations) { + @$combination = sort { $template_pos{$a} <=> $template_pos{$b} } @$combination; + } + + # Sort the array of combinations + @combinations = sort { + # The comparison function compares two arrayrefs of templates. + # Templates from both the arrayrefs are compared one by one and the + # function returns the value when it finds a difference. + for my $i (0..$length-1) { + my $cmp = $template_pos{$a->[$i]} <=> $template_pos{$b->[$i]}; + + return $cmp; + } + } @combinations; + + push @template_combinations, @combinations; + } + + return \@template_combinations; +} + # check if all templates in the array @templates are optional sub _are_templates_optional { my ($self, @templates) = @_; @@ -58,16 +112,16 @@ sub generate { # Verify that $optional_templates has templates from within $self->optional_templates $self->_are_templates_optional(@$optional_templates) - or die "Unknown template(s) in \$optional_templates"; - + or die "Unknown template(s) in \$optional_templates"; + for my $template (@{$self->required_templates}, @$optional_templates) { - try { - push @created_files, $template->generate($vars); - } catch { - $error = $_; - }; + try { + push @created_files, $template->generate($vars); + } catch { + $error = $_; + }; - last if $error; + last if $error; } return (created_files => [ @created_files ], error => $error); From 1af52038ea13f3cef57ebef305ec87f247249bb1 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Mon, 7 Dec 2015 10:34:01 +0530 Subject: [PATCH 6/8] New.pm: Gracefully handle --template with no param --- lib/App/DuckPAN/Cmd/New.pm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index 03c2175..c9c9b5d 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -103,6 +103,11 @@ sub _build__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) = @_; @@ -113,6 +118,13 @@ sub run { $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(); From 8fcd253f31d27f986e84e70b991d5dd377116464 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Tue, 8 Dec 2015 13:46:30 +0530 Subject: [PATCH 7/8] Generic templates: added test cases --- t/template/lib/DDG/Default.pm | 2 + t/template/share/css/default.css | 0 t/template/share/javascript/default.js | 0 t/template/share/text/README | 0 t/template/t/Example.test | 0 t/template/templates.yml | 56 ++++++ t/templates.t | 243 +++++++++++++++++++++++++ 7 files changed, 301 insertions(+) create mode 100644 t/template/lib/DDG/Default.pm create mode 100644 t/template/share/css/default.css create mode 100644 t/template/share/javascript/default.js create mode 100644 t/template/share/text/README create mode 100644 t/template/t/Example.test create mode 100644 t/template/templates.yml create mode 100644 t/templates.t diff --git a/t/template/lib/DDG/Default.pm b/t/template/lib/DDG/Default.pm new file mode 100644 index 0000000..3103c16 --- /dev/null +++ b/t/template/lib/DDG/Default.pm @@ -0,0 +1,2 @@ +package <:$package_name:>; +# <:$lia_name:> diff --git a/t/template/share/css/default.css b/t/template/share/css/default.css new file mode 100644 index 0000000..e69de29 diff --git a/t/template/share/javascript/default.js b/t/template/share/javascript/default.js new file mode 100644 index 0000000..e69de29 diff --git a/t/template/share/text/README b/t/template/share/text/README new file mode 100644 index 0000000..e69de29 diff --git a/t/template/t/Example.test b/t/template/t/Example.test new file mode 100644 index 0000000..e69de29 diff --git a/t/template/templates.yml b/t/template/templates.yml new file mode 100644 index 0000000..863f2cb --- /dev/null +++ b/t/template/templates.yml @@ -0,0 +1,56 @@ +--- +templates: + pm: + label: Perl Module + input: lib/DDG/Default.pm + output: t/out/lib/DDG/<:$package_name:>.pm + needs_restart: true + + test: + label: Perl Module Test + # don't use a '.t' extension so as not to interfere with actual tests + # in the distribution + input: t/Example.test + output: t/out/t/<:$package_name:>.test + + js: + label: Javascript + input: share/javascript/default.js + output: t/out/share/javascript/<:$lia_name:>.js + + css: + label: Javascript + input: share/css/default.css + output: t/out/share/css/<:$lia_name:>.css + + complex_out_dir: + label: README file + input: share/text/README + output: t/out/share/text/<:$lia_name:>/docs/README + + no_input: + label: File with non-existent input + input: share/text/does_not_exist.txt + output: t/out/share/text/<:$lia_name:>.txt + + no_write_perm: + label: Output directory is not writeable + input: share/text/README + output: t/out/readonly/<:$lia_name:>.txt + +template_sets: + all_optional: + description: Template set with all optional templates + optional: [ js, pm, test ] + + required_and_optional: + description: Template set with required and optional templates + required: [ pm, test ] + optional: [ js, css ] + + errors: + description: Template set with templates that can produce errors + required: [ pm ] + optional: [ no_input ] +... + diff --git a/t/templates.t b/t/templates.t new file mode 100644 index 0000000..4955f01 --- /dev/null +++ b/t/templates.t @@ -0,0 +1,243 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Test::More; +use Test::Exception; +use File::Path qw(remove_tree); +use Path::Tiny; + +my $TEMPLATE_OUT = 't/out'; + +# clear the test template output directory +sub clear_output_directory { + remove_tree($TEMPLATE_OUT, { keep_root => 1 }); +} + +BEGIN { + use_ok 'App::DuckPAN::TemplateDefinitions'; +} + +################################ +# Parsing template definitions # +################################ + +my $template_defs = + new_ok 'App::DuckPAN::TemplateDefinitions', + [ templates_yml => 't/template/templates.yml' ]; + +# check templates +my @templates = $template_defs->get_templates; +my %template_map = map { $_->name => $_ } @templates; + +# check if templates are instantiated +isa_ok $_, 'App::DuckPAN::Template' + for @templates; + +# check if all the templates have been read +is_deeply [ sort map { $_->name } @templates ], + [qw(complex_out_dir css js no_input no_write_perm pm test)], + 'template defs: read all templates'; + +# check template sets +my @template_sets = $template_defs->get_template_sets; +my %template_set_map = map { $_->name => $_ } @template_sets; + +# check if template sets are instantiated +isa_ok $_, 'App::DuckPAN::TemplateSet' + for @template_sets; + +# check if all the template sets have been read +is_deeply [ sort map { $_->name } @template_sets ], + [qw(all_optional errors required_and_optional)], + 'template defs: read all template sets'; + +############################### +# Template definitions errors # +############################### + +throws_ok { + my $templates_defs = App::DuckPAN::TemplateDefinitions->new( + templates_yml => 't/template/templates-nonexistent.yml' + ); + } qr/Error loading/, + 'template defs: non-existent template definitions file throws error'; + +######################## +# Individual Templates # +######################## + +# check if all fields of a template are read +is $template_map{pm}->label, 'Perl Module', + 'template defs: set template label field'; +is $template_map{pm}->input_file, 't/template/lib/DDG/Default.pm', + 'template defs: set template input field'; +is $template_map{pm}->output_file, 't/out/lib/DDG/<:$package_name:>.pm', + 'template defs: set template output field'; +ok $template_map{pm}->needs_restart, + 'template defs: set template needs_restart field'; + +# output directory computation +is $template_map{pm}->output_directory, 't/out/lib/DDG', + 'template: output directory generated'; +is $template_map{complex_out_dir}->output_directory, 't/out/share/text', + 'template: output directory generated (complex)'; + +################# +# Template Sets # +################# + +# check if all fields of a template set are read +my $template_set_ro = $template_set_map{required_and_optional}; + +is $template_set_ro->description, 'Template set with required and optional templates', + 'template defs: set template_set description'; +is_deeply $template_set_ro->required_templates, [@template_map{qw(pm test)}], + 'template defs: set template_set required templates'; +is_deeply $template_set_ro->optional_templates, [@template_map{qw(js css)}], + 'template defs: set template_set optional templates'; + +{ + # check template combinations that we should show to the user + my $combinations = $template_set_map{all_optional}->optional_template_combinations; + + # for neater code + my %tm = %template_map; + + # combinations must be in a particular order + is_deeply $combinations, + [ + [ $tm{js} ], + [ $tm{pm} ], + [ $tm{test} ], + [ $tm{js}, $tm{pm} ], + [ $tm{js}, $tm{test} ], + [ $tm{pm}, $tm{test} ], + [ $tm{js}, $tm{pm}, $tm{test} ], + ], + 'template set: all optional template combinations generated correctly'; +} + +################################## +# File generation from templates # +################################## + +my $package_name = 'MyInstantAnswer'; +my $lia_name = 'my_instant_answer'; +my %vars = ( + package_name => $package_name, + lia_name => $lia_name, +); +my $pm_out_file = "$TEMPLATE_OUT/lib/DDG/$package_name.pm"; +my $test_out_file = "$TEMPLATE_OUT/t/$package_name.test"; +my $js_out_file = "$TEMPLATE_OUT/share/javascript/$lia_name.js"; +my $css_out_file = "$TEMPLATE_OUT/share/css/$lia_name.css"; + +clear_output_directory(); + +$template_map{pm}->generate(\%vars); + +# check if output file is correct +ok -f "$pm_out_file", 'template: file generated from template'; + +my $pm_file_content = path($pm_out_file)->slurp; + +# check the content of the output file using all variables +is $pm_file_content, <generate(\%vars); + } qr/already exists/, + 'template: overwriting generated file throws error'; + +# input file not present +throws_ok { $template_map{no_input}->generate(\%vars) } + qr/not found/, + 'template: non-existent template file throws error'; + + +# create a directory with no write access +mkdir "$TEMPLATE_OUT/readonly", 0500; +throws_ok { + $template_map{no_write_perm}->generate(\%vars) + } + qr/Error creating output/, + 'template: failure creating template output file throws error'; + +{ + my $got_warning; + local $SIG{__WARN__} = sub { $got_warning = 1 }; + + clear_output_directory(); + + # all variables were passed to template; no warnings should be shown + $template_map{pm}->generate(\%vars); + ok !$got_warning, 'template: no warning when all variables are passed to template'; + + clear_output_directory(); + + # some or all variables were missing; show warnings + $template_map{pm}->generate({}); + ok $got_warning, 'template: show warning when not all variables are passed to template'; +} + +###################################### +# File generation from template sets # +###################################### +my %generate_res; + +clear_output_directory(); +%generate_res = $template_set_map{required_and_optional}->generate(\%vars, [ $template_map{js} ]); + +is_deeply [ sort @{$generate_res{created_files}} ], + [ $pm_out_file, $js_out_file, $test_out_file ], + 'template set: return value has all the created files'; + +ok !$generate_res{error}, 'template set: no errors when succesfully generating files'; + +# verify all required files have been generated +ok -f $_, "template set: required file '$_' generated from template set" + for ($pm_out_file, $test_out_file); + +ok -f $js_out_file, "template set: selected optional file '$js_out_file' generated from template set"; +ok !-f $css_out_file, "template set: unselected optional file '$css_out_file' not generated from template set"; + +############################################# +# File generation errors from template sets # +############################################# + +clear_output_directory(); +throws_ok { + $template_set_map{required_and_optional}->generate(\%vars, [ $template_map{pm} ]) + } + qr/Unknown template/, + 'template set: die when invalid templates passed in optional templates list'; + +# individual template errors while generating output +%generate_res = $template_set_map{errors}->generate(\%vars, [ $template_map{no_input} ]); + +# required files are generated before optional files, thus we get the .pm file +# in the created files list. Feel free to change this behaviour and update the +# test if necessary. +is_deeply $generate_res{created_files}, [ $pm_out_file ], + 'template set: successfully generated file added to "created_files" list'; + +# the optional template failed to generate the output +ok scalar($generate_res{error} =~ /Template input file.*not found/), + 'template set: error message set for failed template'; + +############################################# + +clear_output_directory(); + +done_testing; + From e5cfa27c52985d2682b70390b6b18a2592e70981 Mon Sep 17 00:00:00 2001 From: Sarvesh D Date: Fri, 11 Dec 2015 02:50:51 +0530 Subject: [PATCH 8/8] templates: Add subdir_support to TemplateSet. This is a general addition, but it specifically allows us to error out when the user provides a name containing path separators for Cheat Sheets. --- lib/App/DuckPAN/Cmd/New.pm | 13 ++++++++++++- lib/App/DuckPAN/TemplateDefinitions.pm | 2 ++ lib/App/DuckPAN/TemplateSet.pm | 8 ++++++++ t/template/templates.yml | 9 +++++++++ t/templates.t | 5 ++++- 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/App/DuckPAN/Cmd/New.pm b/lib/App/DuckPAN/Cmd/New.pm index c9c9b5d..8fae5c3 100644 --- a/lib/App/DuckPAN/Cmd/New.pm +++ b/lib/App/DuckPAN/Cmd/New.pm @@ -132,7 +132,18 @@ sub run { # 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); @@ -189,7 +200,7 @@ sub run { $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 diff --git a/lib/App/DuckPAN/TemplateDefinitions.pm b/lib/App/DuckPAN/TemplateDefinitions.pm index 5b238bf..14b543d 100644 --- a/lib/App/DuckPAN/TemplateDefinitions.pm +++ b/lib/App/DuckPAN/TemplateDefinitions.pm @@ -74,6 +74,7 @@ sub _build__template_sets { my $data = $sets_data->{$name}; my @required = @{$data->{required} // []}; my @optional = @{$data->{optional} // []}; + my $subdir_support = $data->{subdir_support}; # check if all templates in this set are defined for my $template_name (@required, @optional) { @@ -86,6 +87,7 @@ sub _build__template_sets { description => $data->{description}, required_templates => [ @{$self->_template_map}{@required} ], optional_templates => [ @{$self->_template_map}{@optional} ], + defined($subdir_support) ? (subdir_support => $subdir_support) : (), ); $template_sets{$name} = $template_set; diff --git a/lib/App/DuckPAN/TemplateSet.pm b/lib/App/DuckPAN/TemplateSet.pm index 3d6cefd..ef24f52 100644 --- a/lib/App/DuckPAN/TemplateSet.pm +++ b/lib/App/DuckPAN/TemplateSet.pm @@ -29,6 +29,14 @@ has description => ( doc => 'Description of the template set', ); +has subdir_support => ( + is => 'ro', + default => 1, + doc => 'Does this template set support creation of Instant Answers inside sub-directories? ' . + 'For example, Cheat Sheet Instant Answers do not support it, while Standard Goodie ' . + 'and Spice ones do have support.', +); + has required_templates => ( is => 'ro', required => 1, diff --git a/t/template/templates.yml b/t/template/templates.yml index 863f2cb..97e044e 100644 --- a/t/template/templates.yml +++ b/t/template/templates.yml @@ -52,5 +52,14 @@ template_sets: description: Template set with templates that can produce errors required: [ pm ] optional: [ no_input ] + + subdir_support_specified: + description: Template which will not work if IAs are created inside a subdirectory + required: [ pm ] + subdir_support: false + + subdir_support_not_defined: + description: Template which will work if IAs are created inside a subdirectory + required: [ pm ] ... diff --git a/t/templates.t b/t/templates.t index 4955f01..2ae8400 100644 --- a/t/templates.t +++ b/t/templates.t @@ -50,7 +50,7 @@ isa_ok $_, 'App::DuckPAN::TemplateSet' # check if all the template sets have been read is_deeply [ sort map { $_->name } @template_sets ], - [qw(all_optional errors required_and_optional)], + [qw(all_optional errors required_and_optional subdir_support_not_defined subdir_support_specified)], 'template defs: read all template sets'; ############################### @@ -98,6 +98,9 @@ is_deeply $template_set_ro->required_templates, [@template_map{qw(pm test)}], is_deeply $template_set_ro->optional_templates, [@template_map{qw(js css)}], 'template defs: set template_set optional templates'; +ok !$template_set_map{subdir_support_specified}->subdir_support, 'Template set with subdir support explicitly disabled'; +ok $template_set_map{subdir_support_not_defined}->subdir_support, 'Template set with subdir support implicitly enabled'; + { # check template combinations that we should show to the user my $combinations = $template_set_map{all_optional}->optional_template_combinations;