The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
=head1 NAME

Params::Validate::Dependencies::Extending

=head1 DESCRIPTION

How to extend Params::Validate::Dependencies, and a discussion of
its internals.

=head1 PRE-REQUISITES

Before even thinking about extending this module you should understand
references, in particular code-references and closures, and also
understand objects in perl.

=head1 WHAT THE *_of FUNCTIONS REALLY DO

If you've read the documentation for Params::Validate::Dependencies
and Data::Domain::Dependencies, then you'd think that they return
a code-ref which is a closure over the original arguments, and that
they're implemented something like this:

  sub any_of {
    my @options = @_;
    return sub {
      my $hashref = shift;
      ... do stuff with @options and $hashref ...
    }
  }

In version 1, that was exactly what was happening.  But then I wanted
to make them self-documenting.

Now, they still return a code-ref, but that code-ref is blessed into
a class - Params::Validate::Dependencies::any_of in the case of the
closures generated by C<any_of> - so that I can attach a little bit of
extra metadata.  They now look something like this:

  sub any_of {
    my @options = @_;
    bless sub {
      ... some extra stuff ...
      my $hashref = shift;
      ... do stuff with @options and $hashref ...
    }, 'Params::Validate::Dependencies::any_of';
  }

There are four such classes, one for each of the *_of functions.

=head1 HOW THE AUTO-DOCUMENTATION WORKS

Each of those four classes is a sub-class of
Params::Validate::Dependencies::Documenter.  When you call
Params::Validate::Dependencies::document() and pass it one of
those objects, it calls the object's C<_document> method.  That
puts P::V::D into "documentation mode" by setting a global variable
to the object itself, and then executes the object's underlying
code-ref.

In the example above, where it says "... some extra stuff ...", we
actually have this:

  if($Params::Validate::Dependencies::DOC) {
    return $Params::Validate::Dependencies::DOC->_doc_me(list => \@options);
  }

so, now that <$DOC> has a value, the code-ref doesn't bother
validating anything, it instead calls its C<_doc_me> method,
and passes it the options that the closure was originally created
with.

Finally, C<_doc_me>, which now has both the data that was originally
closed over, and the object itself, it can construct a useful string
of documentation.  To do this it gets the name of the original
factory function by calling the object's C<name> method - in the
case of a Params::Validate::Dependencies::any_of object this returns
'any_of' - and, so that it can use 'and' or 'or' when constructing
lists, it gets that by calling the C<join_with> method.

It then iterates over the original list of options, scalars first
followed by code-reffy objects, recursing into objects and getting
them to document themselves.

The end result of that, given a code-ref created thus:

  any_of(
    qw(alpha beta),
    all_of(
      qw(foo bar),
      none_of('barf')
    ),
    one_of(qw(quux garbleflux))
  )

You will get back documentation like this:

  any of ('alpha', 'beta', all of ('foo', 'bar' and none of ('barf')) or one of ('quux' or 'garbleflux'))

which is admittedly not perfect but is better than having to write the
blasted stuff yourself.

=head1 ADDING YOUR OWN VALIDATORS WITHOUT DOCUMENTATION

If you can't be bothered with making your validators self-documenting,
then you can just have them return a closure and be done with it:

  sub two_of {
    my @options = @_;
    return sub {
      my $hashref = shift;
      my $count = 0;
      foreach my $option (@options) {
        $count++ if(
          (!ref($option) && exists($hashref->{$option})) ||
          (ref($option) && $option->($hashref))
        );
      }
      return ($count == 2);
    }
  }

There is an example of this in t/05-extra-validator-without-doco.t.  In
fact it's this very example.

=head1 ADDING YOUR OWN VALIDATORS WITH AUTO-DOCUMENTATION

If you want to do the full-fat implementation complete with auto-doc,
then you will need to implement one teeny-tiny class, as well as modify
your factory function to return an object of that class and to look
out for "documentation mode" being turned on.  The example
above would be modifed thus:

  sub two_of {
    my @options = @_;
    return bless sub {
      if($Params::Validate::Dependencies::DOC) {
        return $Params::Validate::Dependencies::DOC->_doc_me(list => \@options);
      }
      ...
    }, 'Params::Validate::Dependencies::two_of'
  }

and the corresponding class would look like:

  package Params::Validate::Dependencies::two_of;
  use base qw(Params::Validate::Dependencies::Documenter);
  sub join_with { return 'or'; }
  sub name { return 'two_of'; }

It is suggested that you combine class and function together, and
have it export the function, as can be seen in the example
in t/lib/Params/Validate/Dependencies/two_of.pm, which is used in
t/06-extra-validator-with-doco.t.

=head1 CUSTOM AUTO-DOCUMENTATION

If the C<_doc_me> method described above can't cope with your new validation
function then your class need not bother implementing the C<name> or C<join_with>
methods, but your function must, when <$Params::Validate::Dependencies::DOC> is
set, return a string. Anything that you want the C<exclusively()> validator to
pay attention to needs to be single quoted, with any embedded single quotes
preceded by a back-slash.

Best practice is to return something that looks very similar to how your
function was invoked in the first place.

For an example see how the C<exclusively()> function documents itself.

=head1 RESERVED CLASSES

You may not write your own validators or classes for the following, as they are implemented
by Params::Validate::Dependencies itself. If you try implementing them, strange
things may happen:

=over

=item none_of

=item one_of

=item any_of

=item all_of

=item exclusively

=back

=head1 SEE ALSO

L<Params::Validate::Dependencies>

L<Data::Domain::Dependencies>

=head1 SOURCE CODE REPOSITORY

L<git://github.com/DrHyde/perl-modules-Params-Validate-Dependencies.git>

L<https://github.com/DrHyde/perl-modules-Params-Validate-Dependencies/>

=head1 COPYRIGHT and LICENCE

Copyright 2016 David Cantrell E<lt>F<david@cantrell.org.uk>E<gt>

This documentation is free-as-in-speech software.  It may be used, distributed, and modified under the terms of the Creative Commons Attribution-Share Alike 2.0 UK: England & Wales License, whose text you may read at L<http://creativecommons.org/licenses/by-sa/2.0/uk/>.

=head1 CONSPIRACY

This documentation is also free-as-in-mason.

=cut