File Coverage

File:blib/lib/App/FileSync/Args.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package App::FileSync::Args;
2
3# One place that turns a routine's @_ into a validated hashref, so every public
4# entry point reads its arguments the same way and reports a bad one with a
5# catalogued message instead of the validator's own text.
6
7
324
324
324
744
255
4362
use App::FileSync::Platform;
8
9
324
324
324
535
246
2602
use strict;
10
324
324
324
444
293
5920
use warnings;
11
324
324
324
497
243
650
use autodie qw(:all);
12
13
324
324
324
689674
266
4563
use Exporter qw(import);
14
324
324
324
771
5249
6608
use Params::Get qw(get_params);
15
324
324
324
906
12672
5827
use Params::Validate::Strict qw(validate_strict);
16
324
324
324
749
12153
1040
use Sub::Private { mode => 'enforce' };
17
18
324
324
324
33621
270
6512
use App::FileSync::I18N qw(throw strip_location);
19
20
324
324
324
547
225
676
use version; our $VERSION = version->declare('0.001.1');
21
22our @EXPORT_OK = qw(named positional);
23
24# Errors should point at the caller of the routine whose arguments were bad.
25$Carp::Internal{ (__PACKAGE__) }++;
26
27=encoding utf8
28
29 - 100
=head1 NAME

App::FileSync::Args - validated arguments for App::FileSync's public routines

=head1 SYNOPSIS

        use App::FileSync::Args qw(named positional);

        sub repo_create {
                my $self = shift;
                my $args = named(\@_, {
                        name       => { type => 'string', min => 1 },
                        trash_days => { type => 'integer', min => 0, optional => 1 },
                }, 'repo_create');
                ...
        }

        sub load_repo {
                my $args = positional(\@_, [qw(root repo)], {
                        root => { type => 'string', min => 1 },
                        repo => { type => 'string', min => 1 },
                }, 'load_repo');
        }

=head1 FUNCTIONS

=head2 named

Purpose: validate C<key =E<gt> value> arguments (or a single hashref).

Args: C<\@args>, C<\%schema> (L<Params::Validate::Strict> format), C<$routine>
(the name used in an error).

Returns: a hashref of validated (and coerced) arguments.  An empty argument
list is an empty hash.

Side Effects: raises C<args.invalid> naming the routine and the problem.

Usage:

        my $args = named(\@_, $SCHEMA, 'verify');

=head3 EXAMPLE

        my $args = named([ deep => 1 ], { deep => { type => 'boolean', optional => 1 } }, 'verify');

=head3 API SPECIFICATION

=head4 INPUT

        [
                { name => 'args',    type => 'arrayref' },
                { name => 'schema',  type => 'hashref' },
                { name => 'routine', type => 'string', min => 1 },
        ]

=head4 OUTPUT

        { type => 'hashref' }

=head3 MESSAGES

        Key            Meaning                                     Resolution
        args.invalid   an argument is missing, unknown or invalid  fix the call

=head3 FORMAL SPECIFICATION

        named : seq VALUE × SCHEMA × NAME ↛ (KEY ⇸ VALUE)
        pre  pairs(args) satisfies schema
        post result = coerce(pairs(args))

=cut
101
102sub named {
103
5780
9898
        my ($args, $schema, $routine) = @_;
104
105
5780
5780
4317
11553
        my $input = @{$args} ? get_params(undef, $args) : {};
106
5780
73574
        return _validate($input, $schema, $routine);
107}
108
109 - 154
=head2 positional

Purpose: validate positional arguments by naming them.

Args: C<\@args>, C<\@names> (one per position), C<\%schema>, C<$routine>.

Returns: a hashref keyed by C<@names>; missing trailing arguments are
C<undef> and must be C<optional> in the schema.

Side Effects: raises C<args.invalid>.

Usage:

        my $args = positional(\@_, [qw(root repo)], $SCHEMA, 'load_repo');

=head3 EXAMPLE

        my $args = positional([ '/srv', 'docs' ], [qw(root repo)],
                { root => { type => 'string' }, repo => { type => 'string' } }, 'load_repo');

=head3 API SPECIFICATION

=head4 INPUT

        [
                { name => 'args',    type => 'arrayref' },
                { name => 'names',   type => 'arrayref', min => 1 },
                { name => 'schema',  type => 'hashref' },
                { name => 'routine', type => 'string', min => 1 },
        ]

=head4 OUTPUT

        { type => 'hashref' }

=head3 MESSAGES

        Key            Meaning                                     Resolution
        args.invalid   an argument is missing or invalid           fix the call

=head3 FORMAL SPECIFICATION

        positional(a, n, s, r) = named(zip(n, a), s, r)
        pre #a ≤ #n

=cut
155
156sub positional {
157
1007
1593
        my ($args, $names, $schema, $routine) = @_;
158
159        # Map by position directly: get_params treats a lone hashref argument as
160        # named pairs, which is wrong for a positional routine that takes one.
161
1007
932
        my %input;
162
1007
1007
1007
851
1477
1166
        @input{ @{$names} } = @{$args};
163
1007
1810
1568
2288
        delete @input{ grep { !defined $input{$_} } keys %input };
164
165
1007
2077
        return _validate(\%input, $schema, $routine);
166}
167
168# _validate
169# Purpose:     run Params::Validate::Strict and translate its failure.
170# Entry:       $input is a hashref; $schema a Params::Validate::Strict schema.
171# Exit:        returns the validated hashref, or raises args.invalid.
172# Side effects: none beyond the raise.
173sub _validate :Private {
174        my ($input, $schema, $routine) = @_;
175
176        my $valid = eval { validate_strict(schema => $schema, input => $input) };
177        unless ($valid) {
178                # Drop the validator's own prefix and location; the routine name says
179                # where, and the rest says what.
180                my $reason = strip_location($@);
181                $reason =~ s/\AParams::Validate::Strict line \d+: (?:validate_strict: )?//;
182                $reason =~ s/ at \S+ line \d+\.?\z//;
183                throw('args.invalid', { routine => $routine, reason => $reason });
184        }
185
186        return $valid;
187
324
324
324
72386
286
4230
}
188
1891;
190