diff options
author | Russ Allbery <rra@cpan.org> | 2019-03-16 15:53:54 -0700 |
---|---|---|
committer | Russ Allbery <rra@cpan.org> | 2019-03-16 15:53:54 -0700 |
commit | 6030c61d9a418d85a789bcc8bdfd751e8afd2a93 (patch) | |
tree | 4d04e7001d4df549e1cc8c7c8c5ee8311eebc117 | |
parent | 1c1515b5d74f26125f61ffb0dd70ac0c7d28b278 (diff) |
Separate App::DocKnot::Command, add base App::DocKnot
Move the entry point for command-line commands from App::DocKnot to
App::DocKnot::Command. The App::DocKnot module now only provides some
helper methods to load application data, used by both
App::DocKnot::Config and App::DocKnot::Generate. It's no longer
necessary to explicitly load App::DocKnot before using one of the
submodules.
-rw-r--r-- | Changes | 9 | ||||
-rw-r--r-- | MANIFEST | 2 | ||||
-rwxr-xr-x | bin/docknot | 4 | ||||
-rw-r--r-- | lib/App/DocKnot.pm | 279 | ||||
-rw-r--r-- | lib/App/DocKnot/Command.pm | 312 | ||||
-rw-r--r-- | lib/App/DocKnot/Config.pm | 51 | ||||
-rw-r--r-- | lib/App/DocKnot/Generate.pm | 32 | ||||
-rwxr-xr-x | t/cli/errors.t | 8 | ||||
-rwxr-xr-x | t/cli/generate.t | 8 | ||||
-rw-r--r-- | t/config/basic.t | 7 | ||||
-rwxr-xr-x | t/generate/basic.t | 9 | ||||
-rwxr-xr-x | t/generate/output.t | 9 | ||||
-rwxr-xr-x | t/generate/self.t | 9 |
13 files changed, 417 insertions, 322 deletions
@@ -1,10 +1,17 @@ User-Visible DocKnot Changes -DocKnot 2.01 (unreleased) +DocKnot 3.00 (unreleased) Separate configuration parsing into a new App::DocKnot::Config module, used by App::DocKnot::Generate. + Move the entry point for command-line commands from App::DocKnot to + App::DocKnot::Command. The App::DocKnot module now only provides some + helper methods to load application data, used by both + App::DocKnot::Config and App::DocKnot::Generate. It's no longer + necessary to explicitly load App::DocKnot before using one of the + submodules. + Support orphaned warnings in the README and README.md output as well as thread output. @@ -10,6 +10,7 @@ docs/metadata/README docs/metadata/requirements docs/metadata/test/suffix lib/App/DocKnot.pm +lib/App/DocKnot/Command.pm lib/App/DocKnot/Config.pm lib/App/DocKnot/Generate.pm LICENSE @@ -26,6 +27,7 @@ share/templates/readme.tmpl share/templates/thread.tmpl t/cli/errors.t t/cli/generate.t +t/config/basic.t t/data/ansicolor/metadata/blurb t/data/ansicolor/metadata/description t/data/ansicolor/metadata/metadata.json diff --git a/bin/docknot b/bin/docknot index 3f13163..36ba39a 100755 --- a/bin/docknot +++ b/bin/docknot @@ -15,7 +15,7 @@ use warnings; use App::DocKnot; # Dispatch everything to the module. -my $docknot = App::DocKnot->new(); +my $docknot = App::DocKnot::Command->new(); $docknot->run(); __END__ @@ -154,7 +154,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2016, 2018 Russ Allbery <rra@cpan.org> +Copyright 2016, 2018-2019 Russ Allbery <rra@cpan.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot.pm b/lib/App/DocKnot.pm index f1dfb98..b4ed7f1 100644 --- a/lib/App/DocKnot.pm +++ b/lib/App/DocKnot.pm @@ -1,8 +1,9 @@ -# Dispatch code for the DocKnot application. +# Parent module for DocKnot. # # DocKnot provides various commands for generating documentation, web pages, -# and software releases. This module provides command-line parsing and -# dispatch of commands to the various App::DocKnot modules. +# and software releases. This parent module provides some internal helper +# functions used to load configuration and metadata. The normal entry point +# are the various submodules, or App::DocKnot::Command via docknot. # # SPDX-License-Identifier: MIT @@ -16,220 +17,74 @@ use 5.024; use autodie; use warnings; -use App::DocKnot::Generate; -use Getopt::Long; - -# Defines the subcommands, their options, and the module and method that -# implements them. The keys are the names of the commands. Each value is a -# hash with one or more of the following keys: -# -# code -# A reference to a function to call to implement this command. If set, -# overrides method and module. The function will be passed a reference to -# the hash resulting from option parsing as its first argument and any -# other command-line arguments as its remaining arguments. -# -# maximum -# The maximum number of positional arguments this command takes. -# -# method -# The name of the method to run to implement this command. It is passed -# as arguments any remaining command-line arguments after option parsing. -# -# minimum -# The minimum number of positional arguments this command takes. -# -# module -# The name of the module that implements this command. Its constructor -# (which must be named new) will be passed as its sole argument a -# reference to the hash containing the results of parsing any options. -# -# options -# A reference to an array of Getopt::Long option specifications defining -# the arguments that can be passed to this subcommand. -# -# required -# A reference to an array of required option names (the part before any | -# in the option specification for that option). If any of these options -# are not set, an error will be thrown. -our %COMMANDS = ( - generate => { - method => 'generate_output', - module => 'App::DocKnot::Generate', - options => ['metadata|m=s', 'width|w=i'], - maximum => 2, - minimum => 1, - }, - 'generate-all' => { - method => 'generate_all', - module => 'App::DocKnot::Generate', - options => ['metadata|m=s', 'width|w=i'], - maximum => 0, - }, -); +use File::BaseDir qw(config_files); +use File::ShareDir qw(module_file); +use File::Spec; +use JSON; +use Perl6::Slurp; ############################################################################## -# Option parsing +# Helper methods ############################################################################## -# Parse command-line options and do any required error handling. -# -# $self - The App::DocKnot object -# $command - The command being run or undef for top-level options -# $options_ref - A reference to the options specification -# @args - The arguments to the command +# Helper routine to return the path of a file from the application data. +# These data files are installed with App::DocKnot, but each file can be +# overridden by the user via files in $HOME/.config/docknot or +# /etc/xdg/docknot (or whatever $XDG_CONFIG_DIRS is set to). # -# Returns: A list composed of a reference to a hash of options and values, -# followed by a reference to the remaining arguments after options -# have been extracted -# Throws: A text error message if the options are invalid -sub _parse_options { - my ($self, $command, $options_ref, @args) = @_; - - # Use the object-oriented syntax to isolate configuration options from the - # rest of the program. - my $parser = Getopt::Long::Parser->new; - $parser->configure(qw(bundling no_ignore_case require_order)); - - # Parse the options and capture any errors, turning them into exceptions. - # The first letter of the Getopt::Long warning message will be capitalized - # but we want it to be lowercase to follow our error message standard. - my %opts; - { - my $error = 'option parsing failed'; - local $SIG{__WARN__} = sub { ($error) = @_ }; - if (!$parser->getoptionsfromarray(\@args, \%opts, $options_ref->@*)) { - $error =~ s{ \n+ \z }{}xms; - $error =~ s{ \A (\w) }{ lc($1) }xmse; - if ($command) { - die "$0 $command: $error\n"; - } else { - die "$0: $error\n"; - } - } - } - - # Success. Return the options and the remaining arguments. - return (\%opts, \@args); -} - -# Parse command-line options for a given command. +# We therefore try File::BaseDir first (which handles the XDG paths) and fall +# back on using File::ShareDir to locate the data. # -# $self - The App::DocKnot object -# $command - The command being run -# @args - The arguments to the command +# $self - The App::DocKnot object +# @path - The relative path of the file as a list of components # -# Returns: A list composed of a reference to a hash of options and values, -# followed by a reference to the remaining arguments after options -# have been extracted -# Throws: A text error message if the options are invalid -sub _parse_command { - my ($self, $command, @args) = @_; - my $options_ref = $COMMANDS{$command}{options}; - return $self->_parse_options($command, $options_ref, @args); -} +# Returns: The absolute path to the application data +# Throws: Text exception on failure to locate the desired file +sub appdata_path { + my ($self, @path) = @_; -############################################################################## -# Public interface -############################################################################## + # Try XDG paths first. + my $path = config_files('docknot', @path); -# Create a new App::DocKnot object. -# -# $class - Class of object to create -# -# Returns: Newly created object -sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + # If that doesn't work, use the data that came with the module. + if (!defined($path)) { + $path = module_file('App::DocKnot', File::Spec->catfile(@path)); + } + return $path; } -# Parse command-line options to determine which command to run, and then -# dispatch that command. +# Helper routine that locates an application data file, interprets it as JSON, +# and returns the resulting decoded contents. This uses the relaxed parsing +# mode, so comments and commas after data elements are supported. # # $self - The App::DocKnot object -# @args - Command-line arguments (optional, default: @ARGV) -# -# Returns: undef -# Throws: A text error message for invalid arguments -sub run { - my ($self, @args) = @_; - if (!@args) { - @args = @ARGV; - } - - # Parse the initial options and extract the subcommand to run, preserving - # any options after the subcommand. - my $spec = ['help|h']; - my ($opts_ref, $args_ref) = $self->_parse_options(undef, $spec, @args); - if ($opts_ref->{help}) { - pod2usage(0); - } - if (!$args_ref->@*) { - die "$0: no subcommand given\n"; - } - my $command = shift($args_ref->@*); - if (!$COMMANDS{$command}) { - die "$0: unknown command $command\n"; - } - - # Parse the arguments for the command and check for required arguments. - ($opts_ref, $args_ref) = $self->_parse_command($command, $args_ref->@*); - if (exists($COMMANDS{$command}{required})) { - for my $required ($COMMANDS{$command}{required}->@*) { - if (!exists($opts_ref->{$required})) { - die "$0 $command: missing required option --$required\n"; - } - } - } - - # Check that we have the correct number of remaining arguments. - if (exists($COMMANDS{$command}{maximum})) { - if (scalar($args_ref->@*) > $COMMANDS{$command}{maximum}) { - die "$0 $command: too many arguments\n"; - } - } - if (exists($COMMANDS{$command}{minimum})) { - if (scalar($args_ref->@*) < $COMMANDS{$command}{minimum}) { - die "$0 $command: too few arguments\n"; - } - } - - # Dispatch the command and turn exceptions into error messages. - eval { - if ($COMMANDS{$command}{code}) { - $COMMANDS{$command}{code}->($opts_ref, $args_ref->@*); - } else { - my $object = $COMMANDS{$command}{module}->new($opts_ref); - my $method = $COMMANDS{$command}{method}; - $object->$method($args_ref->@*); - } - }; - if ($@) { - my $error = $@; - chomp($error); - $error =~ s{ \s+ at \s+ \S+ \s+ line \s+ \d+ [.]? \z }{}xms; - die "$0 $command: $error\n"; - } - return; +# @path - The path of the file to load, as a list of components +# +# Returns: Anonymous hash or array resulting from decoding the JSON object +# Throws: slurp or JSON exception on failure to load or decode the object +sub load_appdata_json { + my ($self, @path) = @_; + my $path = $self->appdata_path(@path); + my $json = JSON->new; + $json->relaxed; + return $json->decode(scalar(slurp($path))); } +############################################################################## +# Module return value and documentation +############################################################################## + 1; __END__ =for stopwords -Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense +Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense JSON +submodules =head1 NAME App::DocKnot - Documentation and software release management -=head1 SYNOPSIS - - my $docknot = App::DocKnot->new(); - $docknot->run(); - =head1 REQUIREMENTS Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON, @@ -238,31 +93,35 @@ available from CPAN. =head1 DESCRIPTION -The App::DocKnot module implements the B<docknot> command-line interface to -all of the functions of DocKnot. It is an implementation detail of the -B<docknot> command-line tool and is normally only called by that program. +DocKnot is a system for documentation and software release management. Its +functionality is provided by various submodules, often invoked via the +B<docknot> command-line program. For more information, see L<docknot(1)>. -For full documentation, see L<docknot(1)>. +This module only provides helper functions to load configuration and metadata +that are used by its various submodules. -=head1 CLASS METHODS +=head1 INSTANCE METHODS =over 4 -=item new() +=item appdata_path(PATH[, ...]) -Create a new App::DocKnot object. +Return the path of a file from the application data. The file is specified as +one or more path components. -=back +These data files are installed with App::DocKnot, but each file can be +overridden by the user via files in F<$HOME/.config/docknot> or +F</etc/xdg/docknot> (or whatever $XDG_CONFIG_DIRS is set to). Raises a text +exception if the desired file could not be located. -=head1 INSTANCE METHODS - -=over 4 +=item load_appdata_json(PATH[, ...]) -=item run([ARGS]) +Locate an application data file using the same algorithm as appdata_path(), +interpret it as JSON, and returns the resulting decoded contents. This uses +the relaxed JSON parsing mode, so comments and commas after data elements are +supported. The path is specified as one or more path components. -Run the DocKnot action specified by ARGS, which are parsed as command-line -arguments to B<docknot>. If ARGS is not given or is empty, C<@ARGV> will be -parsed instead. +Raises a slurp or JSON exception on failure to load or decode the data file. =back @@ -272,7 +131,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2018 Russ Allbery <rra@cpan.org> +Copyright 2013-2019 Russ Allbery <rra@cpan.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -294,7 +153,7 @@ SOFTWARE. =head1 SEE ALSO -L<App::DocKnot::Generate>, L<docknot(1)> +L<docknot(1)> This module is part of the App-DocKnot distribution. The current version of App::DocKnot is available from CPAN, or directly from its web site at diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm new file mode 100644 index 0000000..f7fcf3e --- /dev/null +++ b/lib/App/DocKnot/Command.pm @@ -0,0 +1,312 @@ +# Dispatch code for the DocKnot application. +# +# DocKnot provides various commands for generating documentation, web pages, +# and software releases. This module provides command-line parsing and +# dispatch of commands to the various App::DocKnot modules. +# +# SPDX-License-Identifier: MIT + +############################################################################## +# Modules and declarations +############################################################################## + +package App::DocKnot::Command 2.00; + +use 5.024; +use autodie; +use warnings; + +use App::DocKnot::Generate; +use Getopt::Long; + +# Defines the subcommands, their options, and the module and method that +# implements them. The keys are the names of the commands. Each value is a +# hash with one or more of the following keys: +# +# code +# A reference to a function to call to implement this command. If set, +# overrides method and module. The function will be passed a reference to +# the hash resulting from option parsing as its first argument and any +# other command-line arguments as its remaining arguments. +# +# maximum +# The maximum number of positional arguments this command takes. +# +# method +# The name of the method to run to implement this command. It is passed +# as arguments any remaining command-line arguments after option parsing. +# +# minimum +# The minimum number of positional arguments this command takes. +# +# module +# The name of the module that implements this command. Its constructor +# (which must be named new) will be passed as its sole argument a +# reference to the hash containing the results of parsing any options. +# +# options +# A reference to an array of Getopt::Long option specifications defining +# the arguments that can be passed to this subcommand. +# +# required +# A reference to an array of required option names (the part before any | +# in the option specification for that option). If any of these options +# are not set, an error will be thrown. +our %COMMANDS = ( + generate => { + method => 'generate_output', + module => 'App::DocKnot::Generate', + options => ['metadata|m=s', 'width|w=i'], + maximum => 2, + minimum => 1, + }, + 'generate-all' => { + method => 'generate_all', + module => 'App::DocKnot::Generate', + options => ['metadata|m=s', 'width|w=i'], + maximum => 0, + }, +); + +############################################################################## +# Option parsing +############################################################################## + +# Parse command-line options and do any required error handling. +# +# $self - The App::DocKnot::Command object +# $command - The command being run or undef for top-level options +# $options_ref - A reference to the options specification +# @args - The arguments to the command +# +# Returns: A list composed of a reference to a hash of options and values, +# followed by a reference to the remaining arguments after options +# have been extracted +# Throws: A text error message if the options are invalid +sub _parse_options { + my ($self, $command, $options_ref, @args) = @_; + + # Use the object-oriented syntax to isolate configuration options from the + # rest of the program. + my $parser = Getopt::Long::Parser->new; + $parser->configure(qw(bundling no_ignore_case require_order)); + + # Parse the options and capture any errors, turning them into exceptions. + # The first letter of the Getopt::Long warning message will be capitalized + # but we want it to be lowercase to follow our error message standard. + my %opts; + { + my $error = 'option parsing failed'; + local $SIG{__WARN__} = sub { ($error) = @_ }; + if (!$parser->getoptionsfromarray(\@args, \%opts, $options_ref->@*)) { + $error =~ s{ \n+ \z }{}xms; + $error =~ s{ \A (\w) }{ lc($1) }xmse; + if ($command) { + die "$0 $command: $error\n"; + } else { + die "$0: $error\n"; + } + } + } + + # Success. Return the options and the remaining arguments. + return (\%opts, \@args); +} + +# Parse command-line options for a given command. +# +# $self - The App::DocKnot::Command object +# $command - The command being run +# @args - The arguments to the command +# +# Returns: A list composed of a reference to a hash of options and values, +# followed by a reference to the remaining arguments after options +# have been extracted +# Throws: A text error message if the options are invalid +sub _parse_command { + my ($self, $command, @args) = @_; + my $options_ref = $COMMANDS{$command}{options}; + return $self->_parse_options($command, $options_ref, @args); +} + +############################################################################## +# Public interface +############################################################################## + +# Create a new App::DocKnot::Command object. +# +# $class - Class of object to create +# +# Returns: Newly created object +sub new { + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; +} + +# Parse command-line options to determine which command to run, and then +# dispatch that command. +# +# $self - The App::DocKnot::Command object +# @args - Command-line arguments (optional, default: @ARGV) +# +# Returns: undef +# Throws: A text error message for invalid arguments +sub run { + my ($self, @args) = @_; + if (!@args) { + @args = @ARGV; + } + + # Parse the initial options and extract the subcommand to run, preserving + # any options after the subcommand. + my $spec = ['help|h']; + my ($opts_ref, $args_ref) = $self->_parse_options(undef, $spec, @args); + if ($opts_ref->{help}) { + pod2usage(0); + } + if (!$args_ref->@*) { + die "$0: no subcommand given\n"; + } + my $command = shift($args_ref->@*); + if (!$COMMANDS{$command}) { + die "$0: unknown command $command\n"; + } + + # Parse the arguments for the command and check for required arguments. + ($opts_ref, $args_ref) = $self->_parse_command($command, $args_ref->@*); + if (exists($COMMANDS{$command}{required})) { + for my $required ($COMMANDS{$command}{required}->@*) { + if (!exists($opts_ref->{$required})) { + die "$0 $command: missing required option --$required\n"; + } + } + } + + # Check that we have the correct number of remaining arguments. + if (exists($COMMANDS{$command}{maximum})) { + if (scalar($args_ref->@*) > $COMMANDS{$command}{maximum}) { + die "$0 $command: too many arguments\n"; + } + } + if (exists($COMMANDS{$command}{minimum})) { + if (scalar($args_ref->@*) < $COMMANDS{$command}{minimum}) { + die "$0 $command: too few arguments\n"; + } + } + + # Dispatch the command and turn exceptions into error messages. + eval { + if ($COMMANDS{$command}{code}) { + $COMMANDS{$command}{code}->($opts_ref, $args_ref->@*); + } else { + my $object = $COMMANDS{$command}{module}->new($opts_ref); + my $method = $COMMANDS{$command}{method}; + $object->$method($args_ref->@*); + } + }; + if ($@) { + my $error = $@; + chomp($error); + $error =~ s{ \s+ at \s+ \S+ \s+ line \s+ \d+ [.]? \z }{}xms; + die "$0 $command: $error\n"; + } + return; +} + +############################################################################## +# Module return value and documentation +############################################################################## + +1; +__END__ + +=for stopwords +Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense + +=head1 NAME + +App::DocKnot::Command - Run DocKnot commands + +=head1 SYNOPSIS + + my $docknot = App::DocKnot::Command->new(); + $docknot->run(); + +=head1 REQUIREMENTS + +Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON, +Perl6::Slurp, and Template (part of Template Toolkit), all of which are +available from CPAN. + +=head1 DESCRIPTION + +The App::DocKnot::Command module implements the B<docknot> command-line +interface to all of the functions of DocKnot. It is an implementation detail +of the B<docknot> command-line tool and is normally only called by that +program. + +For full documentation, see L<docknot(1)>. + +=head1 CLASS METHODS + +=over 4 + +=item new() + +Create a new App::DocKnot::Command object. + +=back + +=head1 INSTANCE METHODS + +=over 4 + +=item run([ARGS]) + +Run the DocKnot action specified by ARGS, which are parsed as command-line +arguments to B<docknot>. If ARGS is not given or is empty, C<@ARGV> will be +parsed instead. + +=back + +=head1 AUTHOR + +Russ Allbery <rra@cpan.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2018-2019 Russ Allbery <rra@cpan.org> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=head1 SEE ALSO + +L<App::DocKnot::Generate>, L<docknot(1)> + +This module is part of the App-DocKnot distribution. The current version of +App::DocKnot is available from CPAN, or directly from its web site at +L<https://www.eyrie.org/~eagle/software/docknot/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm index de8d8b9..9273719 100644 --- a/lib/App/DocKnot/Config.pm +++ b/lib/App/DocKnot/Config.pm @@ -13,11 +13,10 @@ package App::DocKnot::Config 2.00; use 5.024; use autodie; +use parent qw(App::DocKnot); use warnings; use Carp qw(croak); -use File::BaseDir qw(config_files); -use File::ShareDir qw(module_file); use File::Spec; use JSON; use Perl6::Slurp; @@ -41,49 +40,6 @@ our @METADATA_FILES = qw( # Helper methods ############################################################################## -# Internal helper routine to return the path of a file from the application -# data. These data files are installed with App::DocKnot, but each file can -# be overridden by the user via files in $HOME/.config/docknot or -# /etc/xdg/docknot (or whatever $XDG_CONFIG_DIRS is set to). -# -# We therefore try File::BaseDir first (which handles the XDG paths) and fall -# back on using File::ShareDir to locate the data. -# -# $self - The App::DocKnot::Generate object -# @path - The relative path of the file as a list of components -# -# Returns: The absolute path to the application data -# Throws: Text exception on failure to locate the desired file -sub _appdata_path { - my ($self, @path) = @_; - - # Try XDG paths first. - my $path = config_files('docknot', @path); - - # If that doesn't work, use the data that came with the module. - if (!defined($path)) { - $path = module_file('App::DocKnot', File::Spec->catfile(@path)); - } - return $path; -} - -# Internal helper routine that locates an application data file, interprets it -# as JSON, and returns the resulting decoded contents. This uses the relaxed -# parsing mode, so comments and commas after data elements are supported. -# -# $self - The App::DocKnot::Generate object -# @path - The path of the file to load, as a list of components -# -# Returns: Anonymous hash or array resulting from decoding the JSON object -# Throws: slurp or JSON exception on failure to load or decode the object -sub _load_appdata_json { - my ($self, @path) = @_; - my $path = $self->_appdata_path(@path); - my $json = JSON->new; - $json->relaxed; - return $json->decode(scalar(slurp($path))); -} - # Internal helper routine to return the path of a file or directory from the # package metadata directory. The resulting file or directory path is not # checked for existence. @@ -209,11 +165,11 @@ sub config { # Expand the package license into license text. my $license = $data_ref->{license}; - my $licenses_ref = $self->_load_appdata_json('licenses.json'); + my $licenses_ref = $self->load_appdata_json('licenses.json'); if (!exists($licenses_ref->{$license})) { die "Unknown license $license\n"; } - my $license_text = slurp($self->_appdata_path('licenses', $license)); + my $license_text = slurp($self->appdata_path('licenses', $license)); $data_ref->{license} = { $licenses_ref->{$license}->%* }; $data_ref->{license}{full} = $license_text; @@ -258,7 +214,6 @@ App::DocKnot::Config - Read and return DocKnot package configuration =head1 SYNOPSIS - use App::DocKnot; use App::DocKnot::Config; my $reader = App::DocKnot::Config->new({ metadata => 'docs/metadata' }); my $config = $reader->config(); diff --git a/lib/App/DocKnot/Generate.pm b/lib/App/DocKnot/Generate.pm index 0eca5ef..de8426b 100644 --- a/lib/App/DocKnot/Generate.pm +++ b/lib/App/DocKnot/Generate.pm @@ -14,12 +14,11 @@ package App::DocKnot::Generate 2.00; use 5.024; use autodie; +use parent qw(App::DocKnot); use warnings; use App::DocKnot::Config; use Carp qw(croak); -use File::BaseDir qw(config_files); -use File::ShareDir qw(module_file); use Template; use Text::Wrap qw(wrap); @@ -304,32 +303,6 @@ sub _code_for_to_thread { # Helper methods ############################################################################## -# Internal helper routine to return the path of a file from the application -# data. These data files are installed with App::DocKnot, but each file can -# be overridden by the user via files in $HOME/.config/docknot or -# /etc/xdg/docknot (or whatever $XDG_CONFIG_DIRS is set to). -# -# We therefore try File::BaseDir first (which handles the XDG paths) and fall -# back on using File::ShareDir to locate the data. -# -# $self - The App::DocKnot::Generate object -# @path - The relative path of the file as a list of components -# -# Returns: The absolute path to the application data -# Throws: Text exception on failure to locate the desired file -sub _appdata_path { - my ($self, @path) = @_; - - # Try XDG paths first. - my $path = config_files('docknot', @path); - - # If that doesn't work, use the data that came with the module. - if (!defined($path)) { - $path = module_file('App::DocKnot', File::Spec->catfile(@path)); - } - return $path; -} - # Word-wrap a paragraph of text. This is a helper function for _wrap, mostly # so that it can be invoked recursively to wrap bulleted paragraphs. # @@ -496,7 +469,7 @@ sub generate { $vars{to_thread} = $self->_code_for_to_thread; # Ensure we were given a valid template. - $template = $self->_appdata_path('templates', "${template}.tmpl"); + $template = $self->appdata_path('templates', "${template}.tmpl"); # Run Template Toolkit processing. my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error()); @@ -568,7 +541,6 @@ App::DocKnot::Generate - Generate documentation from package metadata =head1 SYNOPSIS - use App::DocKnot; use App::DocKnot::Generate; my $docknot = App::DocKnot::Generate->new({ metadata => 'docs/metadata' }); my $readme = $docknot->generate('readme'); diff --git a/t/cli/errors.t b/t/cli/errors.t index bd27bfd..fc0561e 100755 --- a/t/cli/errors.t +++ b/t/cli/errors.t @@ -2,7 +2,7 @@ # # Tests for the App::DocKnot command dispatch error handling. # -# Copyright 2018 Russ Allbery <rra@cpan.org> +# Copyright 2018-2019 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT @@ -13,7 +13,7 @@ use warnings; use Test::More tests => 10; # Load the module. -BEGIN { use_ok('App::DocKnot') } +BEGIN { use_ok('App::DocKnot::Command') } # Check an error against the expected message, removing the trailing newline # and stripping off the leading $0 that's prepended and the colon and space @@ -33,8 +33,8 @@ sub is_error { } # Create the command-line parser. -my $docknot = App::DocKnot->new(); -isa_ok($docknot, 'App::DocKnot'); +my $docknot = App::DocKnot::Command->new(); +isa_ok($docknot, 'App::DocKnot::Command'); # Test various errors. eval { $docknot->run('foo') }; diff --git a/t/cli/generate.t b/t/cli/generate.t index 21ab2f0..05e096d 100755 --- a/t/cli/generate.t +++ b/t/cli/generate.t @@ -2,7 +2,7 @@ # # Tests for the App::DocKnot command dispatch for generate. # -# Copyright 2018 Russ Allbery <rra@cpan.org> +# Copyright 2018-2019 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT @@ -21,11 +21,11 @@ use Test::RRA qw(is_file_contents); use Test::More tests => 7; # Load the module. -BEGIN { use_ok('App::DocKnot') } +BEGIN { use_ok('App::DocKnot::Command') } # Create the command-line parser. -my $docknot = App::DocKnot->new(); -isa_ok($docknot, 'App::DocKnot'); +my $docknot = App::DocKnot::Command->new(); +isa_ok($docknot, 'App::DocKnot::Command'); # Generate the package README file to a temporary file, read it into memory, # and compare it to the actual README file. This duplicates part of the diff --git a/t/config/basic.t b/t/config/basic.t index 1dd9409..6b719bb 100644 --- a/t/config/basic.t +++ b/t/config/basic.t @@ -15,13 +15,10 @@ use File::Spec; use JSON; use Perl6::Slurp; -use Test::More tests => 8; +use Test::More tests => 7; # Load the modules. -BEGIN { - use_ok('App::DocKnot'); - use_ok('App::DocKnot::Config'); -} +BEGIN { use_ok('App::DocKnot::Config') } # Load a test configuration and check a few inobvious pieces of it. my $metadata_path = File::Spec->catfile('t', 'data', 'ansicolor', 'metadata'); diff --git a/t/generate/basic.t b/t/generate/basic.t index c5ead59..52f5c83 100755 --- a/t/generate/basic.t +++ b/t/generate/basic.t @@ -2,7 +2,7 @@ # # Tests for the App::DocKnot::Generate module API. # -# Copyright 2013, 2016-2018 Russ Allbery <rra@cpan.org> +# Copyright 2013, 2016-2019 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT @@ -18,10 +18,7 @@ use Test::RRA qw(is_file_contents); use Test::More; # Load the modules. -BEGIN { - use_ok('App::DocKnot'); - use_ok('App::DocKnot::Generate'); -} +BEGIN { use_ok('App::DocKnot::Generate') } # We have a set of test cases in the data directory. Each of them contains # metadata and output directories. @@ -48,4 +45,4 @@ for my $test (@tests) { } # Check that we ran the correct number of tests. -done_testing(2 + scalar(@tests) * 4); +done_testing(1 + scalar(@tests) * 4); diff --git a/t/generate/output.t b/t/generate/output.t index 8478cf1..11e8e3d 100755 --- a/t/generate/output.t +++ b/t/generate/output.t @@ -3,7 +3,7 @@ # Test the generate_output method. This doubles as a test for whether the # package metadata is consistent with the files currently in the distribution. # -# Copyright 2016, 2018 Russ Allbery <rra@cpan.org> +# Copyright 2016, 2018-2019 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT @@ -19,13 +19,10 @@ use File::Temp; use Perl6::Slurp; use Test::RRA qw(is_file_contents); -use Test::More tests => 8; +use Test::More tests => 7; # Load the module. -BEGIN { - use_ok('App::DocKnot'); - use_ok('App::DocKnot::Generate'); -} +BEGIN { use_ok('App::DocKnot::Generate') } # Initialize the App::DocKnot object using the default metadata path. my $metadata_path = File::Spec->catfile(getcwd(), 'docs', 'metadata'); diff --git a/t/generate/self.t b/t/generate/self.t index b4ac0ea..352d0c2 100755 --- a/t/generate/self.t +++ b/t/generate/self.t @@ -2,7 +2,7 @@ # # Test generated files against the files included in the package. # -# Copyright 2016, 2018 Russ Allbery <rra@cpan.org> +# Copyright 2016, 2018-2019 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT @@ -15,13 +15,10 @@ use lib 't/lib'; use File::Spec; use Test::RRA qw(is_file_contents); -use Test::More tests => 6; +use Test::More tests => 5; # Load the module. -BEGIN { - use_ok('App::DocKnot'); - use_ok('App::DocKnot::Generate'); -} +BEGIN { use_ok('App::DocKnot::Generate') } # Initialize the App::DocKnot object using the default metadata path. my $docknot = App::DocKnot::Generate->new(); |