The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

#pod =pod
#pod
#pod =cut

# PODNAME:  Data::Rx::Manual::CustomTypes
# ABSTRACT: overview of making new checkers

#pod =head1 OVERVIEW
#pod
#pod L<Data::Rx> ships with a variety of core validators --
#pod I<L<single|http://rx.codesimply.com/coretypes.html#single>>,
#pod I<L<collection|http://rx.codesimply.com/coretypes.html#collect>>, and
#pod I<L<combination|http://rx.codesimply.com/coretypes.html#combo>> types, which
#pod can be combined in surprisingly powerful ways.  However the core language is
#pod deliberately limited to known cross-platform features, and there are things
#pod that you simply cannot represent with it. However, you can create custom I<type
#pod plugins> in any implementation, including L<Data::Rx> in Perl.
#pod
#pod =head1 SYNOPSIS
#pod
#pod The easiest way to create a custom type plugin is to subclass
#pod L<Data::Rx::CommonType::EasyNew>.
#pod
#pod   package My::Type::Foo;
#pod   use parent 'Data::Rx::CommonType::EasyNew';
#pod
#pod   sub type_uri {
#pod     'tag:example.com,EXAMPLE:rx/foo',
#pod   }
#pod
#pod   sub guts_from_arg {
#pod     my ($class, $arg, $rx) = @_;
#pod
#pod     # get and validate arguments from $arg
#pod
#pod     return {
#pod         # the "guts" for this object
#pod         # these might be validator objects using CPAN modules
#pod         # or using $rx->make_schema() etc.
#pod       },
#pod   }
#pod
#pod   sub assert_valid {
#pod     my ($self, $value) = @_;
#pod
#pod     # check the value, and either return 1 for success
#pod     # or die on failure
#pod   }
#pod
#pod   1;
#pod
#pod and later...
#pod
#pod   use Data::Rx;
#pod   use My::Type::Foo;
#pod
#pod   my $rx = Data::Rx->new({
#pod     sort_keys => 1,
#pod     prefix => {
#pod       example => 'tag:example.com,EXAMPLE:rx/',
#pod     },
#pod     type_plugins => [qw(
#pod       My::Type::Foo
#pod     )],
#pod   });
#pod
#pod   my $schema = $rx->make_schema('/example/foo');
#pod
#pod   $schema->assert_valid( $some_value );
#pod
#pod =head1 EXAMPLES
#pod
#pod These examples are worked fully in the C<examples/> directory.  In this man
#pod page, we will just look at interesting features of each type plugin, for clarity.
#pod
#pod =head2 W3C DateTime - using Perl and CPAN in checks
#pod
#pod We might want to validate dates in the W3CDTF format, which look like
#pod C<2003-02-15T13:50:05-05:00>.  We could of course write this with a
#pod regular expression, but let's take an even better approach and dash to the
#pod CPAN, where we find an existing module, L<DateTime::Format::W3CDTF>.
#pod
#pod Our parser, then, will instantiate one of these objects, and return it
#pod with C<guts_from_arg> to be stashed away.
#pod
#pod   use DateTime::Format::W3CDTF;
#pod
#pod   sub guts_from_arg {
#pod     my ($class, $arg, $rx) = @_;
#pod
#pod     return {
#pod       dt => DateTime::Format::W3CDTF->new,
#pod     };
#pod   }
#pod
#pod We can then test this in the C<assert_valid> routine by returning true
#pod if the date format matches:
#pod
#pod   sub assert_valid {
#pod     my ($self, $value) = @_;
#pod
#pod     return 1 if $value && eval {
#pod       $self->{dt}->parse_datetime( $value  );
#pod     };
#pod
#pod If it doesn't, then we should return an error, and to make sure that we
#pod act like a good citizen in the Rx ecosystem, let's use
#pod C<Data::Rx::CommonType::EasyNew>'s provided method C<fail>:
#pod
#pod     $self->fail({
#pod       error => [ qw(type) ],
#pod       message => "found value is not a w3 datetime",
#pod       value => $value,
#pod     })
#pod   }
#pod
#pod Now we can use this checker like so:
#pod
#pod   $rx->make_schema('/example/datetime/w3')
#pod      ->assert_valid( '2003-02-15T13:50:05-05:00' );
#pod
#pod =head2 Enum - delegate to another schema
#pod
#pod You'll often want to create data-types that match a set of values like
#pod (C<open>, C<closed>) or (C<0>, C<15>, C<30>, C<40>).  L<Data::Rx> doesn't
#pod have an Enum type, but it does have C<//any>:
#pod
#pod   {
#pod     type => '//any',
#pod     of => [
#pod       { type => '//str', value => 'open' },
#pod       { type => '//str', value => 'closed' },
#pod     ]
#pod   }
#pod
#pod This is a bit clumsy though, with the repetition of the type C<//str>.  Instead
#pod we would like an Enum type which might be declared like:
#pod
#pod   {
#pod     type => '/example/enum',
#pod     contents => {
#pod       type    => '//str',
#pod       values  => [ qw/
#pod         open
#pod         closed
#pod       /],
#pod     },
#pod   }
#pod
#pod Ignoring input checking (for this example), we can get this information from the
#pod C<$arg> parameter:
#pod
#pod   sub guts_from_arg {
#pod     my ($class, $arg, $rx) = @_;
#pod
#pod     my $type = $arg->{contents}{type};
#pod     my @values = @{ $arg->{contents}{values} };
#pod
#pod We already saw how we would write the enum as an C<//any> schema.  And in fact
#pod the easiest way to implement this type plugin is to do exactly that!  Let's
#pod create a schema which is equivalent, and return it, to be stashed in the object:
#pod
#pod     my $schema = $rx->make_schema({
#pod       type => '//any',
#pod       of   => [
#pod         map {;
#pod           { type => $type, value => $_ }
#pod         } @values,
#pod       ],
#pod     });
#pod
#pod     return { schema => $schema };
#pod   }
#pod
#pod Now, checking the enum is as simple as delegating to this schema:
#pod
#pod   sub assert_valid {
#pod     my ($self, $value) = @_;
#pod
#pod     $self->{schema}->assert_valid( $value );
#pod   }
#pod
#pod As we are delegating to another schema's C<assert_valid> we know that
#pod any exceptions will be in the correct format.  However, the error will
#pod be the one that C<//any> provides:
#pod
#pod     Failed //any: matched none of the available alternatives
#pod
#pod This is probably clear enough for an enum.  But we could improve this message
#pod by calling C<check> instead of C<assert_valid> and raising our own, nicely
#pod formatted, exception using C<fail>.
#pod
#pod =head2 CSV - delegation, checking input
#pod
#pod Some APIs like to specify a list of IDs or statuses not as an array (which
#pod of course Rx handles with C<//arr> but as a comma separated list.  Curses!
#pod
#pod We would like to write a type plugin that's defined something like:
#pod
#pod   {
#pod     type => '/example/csv',
#pod     contents => '/example/status',
#pod   }
#pod
#pod Of course now that we are getting data as strings, we also have to worry
#pod about spaces: e.g. in '123, 456', is the second ID ' 456' or just '456'?
#pod So let's also accept an optional 3rd parameter C<trim>.
#pod
#pod Now that we're asking for a more complex input data structure, let's validate
#pod it using Rx itself!
#pod
#pod   sub guts_from_arg {
#pod     my ($class, $arg, $rx) = @_;
#pod
#pod     my $meta = $rx->make_schema({
#pod       type => '//rec',
#pod       required => {
#pod         # contents => '/.meta/schema', # not yet implemented
#pod         contents => '//any',
#pod       },
#pod       optional => {
#pod         trim => {
#pod           # we don't just accept //bool as this only includes 'boolean' objects,
#pod           # let's also allow undef/0/1, as this is more Perlish!
#pod           type => '//any',
#pod           of => [ '//nil', '//bool', '//int' ]
#pod         },
#pod       },
#pod     });
#pod
#pod     $meta->assert_valid( $arg );
#pod
#pod The C<contents> argument is required, and should be a valid schema.  We've had
#pod to make a few trade-offs:
#pod
#pod =over 4
#pod
#pod =item *
#pod
#pod There isn't yet a convenient way to specify a schema, so we'll just accept
#pod C<//any> for now.  As we will then pass this result to C<make_schema> shortly,
#pod we will get a further validation of that in any case! (But see
#pod L<http://rx.codesimply.com/moretypes.html> for the full definition of a schema,
#pod if you prefer!)
#pod
#pod =item *
#pod
#pod Rx's type C<//bool> is deliberately targeted at JSON like boolean objects, so
#pod we'll also accept undef and 1 as "truthy" values.
#pod
#pod =back
#pod
#pod As we are expecting a comma separated I<string>, the first check we'll want to
#pod make is that the object we receive is in fact a string.  So the guts we'll
#pod return are:
#pod
#pod     return {
#pod         trim => $arg->{trim},
#pod         str_schema => $rx->make_schema('//str'),
#pod         item_schema => $rx->make_schema( $arg->{contents} ),
#pod     };
#pod
#pod Now our C<assert_valid> routine will use all of these pieces:
#pod
#pod   use String::Trim;
#pod
#pod   sub assert_valid {
#pod     my ($self, $value) = @_;
#pod
#pod First we check that we got a string:
#pod
#pod     $self->{str_schema}->assert_valid( $value );
#pod
#pod This means we can safely split the result:
#pod
#pod     my @values = split ',' => $value;
#pod
#pod     my $item_schema = $self->{item_schema};
#pod     my $trim = $self->{trim};
#pod
#pod For each result we trim (if requested) and use the supplied checker on each
#pod element.
#pod
#pod     for my $subvalue (@values) {
#pod       trim($subvalue) if $trim;
#pod
#pod       $item_schema->assert_valid( $subvalue );
#pod     }
#pod
#pod     return 1;
#pod   }
#pod
#pod Putting together all the pieces, we can call this like so:
#pod
#pod   my $csv = $rx->make_schema({
#pod     type => '/example/csv',
#pod     contents => {
#pod       type     => '/example/enum',
#pod       contents => {
#pod         type    => '//str',
#pod         values  => [qw/ open closed /],
#pod       }
#pod     },
#pod     trim => 1,
#pod   });
#pod
#pod   $csv->assert_valid( 'open, closed' ); # OK!
#pod
#pod =head1 POD AUTHOR
#pod
#pod Hakim Cassimally <osfameron@cpan.org>
#pod
#pod =cut

__END__

=pod

=encoding UTF-8

=head1 NAME

Data::Rx::Manual::CustomTypes - overview of making new checkers

=head1 VERSION

version 0.200006

=head1 SYNOPSIS

The easiest way to create a custom type plugin is to subclass
L<Data::Rx::CommonType::EasyNew>.

  package My::Type::Foo;
  use parent 'Data::Rx::CommonType::EasyNew';

  sub type_uri {
    'tag:example.com,EXAMPLE:rx/foo',
  }

  sub guts_from_arg {
    my ($class, $arg, $rx) = @_;

    # get and validate arguments from $arg

    return {
        # the "guts" for this object
        # these might be validator objects using CPAN modules
        # or using $rx->make_schema() etc.
      },
  }

  sub assert_valid {
    my ($self, $value) = @_;

    # check the value, and either return 1 for success
    # or die on failure
  }

  1;

and later...

  use Data::Rx;
  use My::Type::Foo;

  my $rx = Data::Rx->new({
    sort_keys => 1,
    prefix => {
      example => 'tag:example.com,EXAMPLE:rx/',
    },
    type_plugins => [qw(
      My::Type::Foo
    )],
  });

  my $schema = $rx->make_schema('/example/foo');

  $schema->assert_valid( $some_value );

=head1 OVERVIEW

L<Data::Rx> ships with a variety of core validators --
I<L<single|http://rx.codesimply.com/coretypes.html#single>>,
I<L<collection|http://rx.codesimply.com/coretypes.html#collect>>, and
I<L<combination|http://rx.codesimply.com/coretypes.html#combo>> types, which
can be combined in surprisingly powerful ways.  However the core language is
deliberately limited to known cross-platform features, and there are things
that you simply cannot represent with it. However, you can create custom I<type
plugins> in any implementation, including L<Data::Rx> in Perl.

=head1 EXAMPLES

These examples are worked fully in the C<examples/> directory.  In this man
page, we will just look at interesting features of each type plugin, for clarity.

=head2 W3C DateTime - using Perl and CPAN in checks

We might want to validate dates in the W3CDTF format, which look like
C<2003-02-15T13:50:05-05:00>.  We could of course write this with a
regular expression, but let's take an even better approach and dash to the
CPAN, where we find an existing module, L<DateTime::Format::W3CDTF>.

Our parser, then, will instantiate one of these objects, and return it
with C<guts_from_arg> to be stashed away.

  use DateTime::Format::W3CDTF;

  sub guts_from_arg {
    my ($class, $arg, $rx) = @_;

    return {
      dt => DateTime::Format::W3CDTF->new,
    };
  }

We can then test this in the C<assert_valid> routine by returning true
if the date format matches:

  sub assert_valid {
    my ($self, $value) = @_;

    return 1 if $value && eval {
      $self->{dt}->parse_datetime( $value  );
    };

If it doesn't, then we should return an error, and to make sure that we
act like a good citizen in the Rx ecosystem, let's use
C<Data::Rx::CommonType::EasyNew>'s provided method C<fail>:

    $self->fail({
      error => [ qw(type) ],
      message => "found value is not a w3 datetime",
      value => $value,
    })
  }

Now we can use this checker like so:

  $rx->make_schema('/example/datetime/w3')
     ->assert_valid( '2003-02-15T13:50:05-05:00' );

=head2 Enum - delegate to another schema

You'll often want to create data-types that match a set of values like
(C<open>, C<closed>) or (C<0>, C<15>, C<30>, C<40>).  L<Data::Rx> doesn't
have an Enum type, but it does have C<//any>:

  {
    type => '//any',
    of => [
      { type => '//str', value => 'open' },
      { type => '//str', value => 'closed' },
    ]
  }

This is a bit clumsy though, with the repetition of the type C<//str>.  Instead
we would like an Enum type which might be declared like:

  {
    type => '/example/enum',
    contents => {
      type    => '//str',
      values  => [ qw/
        open
        closed
      /],
    },
  }

Ignoring input checking (for this example), we can get this information from the
C<$arg> parameter:

  sub guts_from_arg {
    my ($class, $arg, $rx) = @_;

    my $type = $arg->{contents}{type};
    my @values = @{ $arg->{contents}{values} };

We already saw how we would write the enum as an C<//any> schema.  And in fact
the easiest way to implement this type plugin is to do exactly that!  Let's
create a schema which is equivalent, and return it, to be stashed in the object:

    my $schema = $rx->make_schema({
      type => '//any',
      of   => [
        map {;
          { type => $type, value => $_ }
        } @values,
      ],
    });

    return { schema => $schema };
  }

Now, checking the enum is as simple as delegating to this schema:

  sub assert_valid {
    my ($self, $value) = @_;

    $self->{schema}->assert_valid( $value );
  }

As we are delegating to another schema's C<assert_valid> we know that
any exceptions will be in the correct format.  However, the error will
be the one that C<//any> provides:

    Failed //any: matched none of the available alternatives

This is probably clear enough for an enum.  But we could improve this message
by calling C<check> instead of C<assert_valid> and raising our own, nicely
formatted, exception using C<fail>.

=head2 CSV - delegation, checking input

Some APIs like to specify a list of IDs or statuses not as an array (which
of course Rx handles with C<//arr> but as a comma separated list.  Curses!

We would like to write a type plugin that's defined something like:

  {
    type => '/example/csv',
    contents => '/example/status',
  }

Of course now that we are getting data as strings, we also have to worry
about spaces: e.g. in '123, 456', is the second ID ' 456' or just '456'?
So let's also accept an optional 3rd parameter C<trim>.

Now that we're asking for a more complex input data structure, let's validate
it using Rx itself!

  sub guts_from_arg {
    my ($class, $arg, $rx) = @_;

    my $meta = $rx->make_schema({
      type => '//rec',
      required => {
        # contents => '/.meta/schema', # not yet implemented
        contents => '//any',
      },
      optional => {
        trim => {
          # we don't just accept //bool as this only includes 'boolean' objects,
          # let's also allow undef/0/1, as this is more Perlish!
          type => '//any',
          of => [ '//nil', '//bool', '//int' ]
        },
      },
    });

    $meta->assert_valid( $arg );

The C<contents> argument is required, and should be a valid schema.  We've had
to make a few trade-offs:

=over 4

=item *

There isn't yet a convenient way to specify a schema, so we'll just accept
C<//any> for now.  As we will then pass this result to C<make_schema> shortly,
we will get a further validation of that in any case! (But see
L<http://rx.codesimply.com/moretypes.html> for the full definition of a schema,
if you prefer!)

=item *

Rx's type C<//bool> is deliberately targeted at JSON like boolean objects, so
we'll also accept undef and 1 as "truthy" values.

=back

As we are expecting a comma separated I<string>, the first check we'll want to
make is that the object we receive is in fact a string.  So the guts we'll
return are:

    return {
        trim => $arg->{trim},
        str_schema => $rx->make_schema('//str'),
        item_schema => $rx->make_schema( $arg->{contents} ),
    };

Now our C<assert_valid> routine will use all of these pieces:

  use String::Trim;

  sub assert_valid {
    my ($self, $value) = @_;

First we check that we got a string:

    $self->{str_schema}->assert_valid( $value );

This means we can safely split the result:

    my @values = split ',' => $value;

    my $item_schema = $self->{item_schema};
    my $trim = $self->{trim};

For each result we trim (if requested) and use the supplied checker on each
element.

    for my $subvalue (@values) {
      trim($subvalue) if $trim;

      $item_schema->assert_valid( $subvalue );
    }

    return 1;
  }

Putting together all the pieces, we can call this like so:

  my $csv = $rx->make_schema({
    type => '/example/csv',
    contents => {
      type     => '/example/enum',
      contents => {
        type    => '//str',
        values  => [qw/ open closed /],
      }
    },
    trim => 1,
  });

  $csv->assert_valid( 'open, closed' ); # OK!

=head1 POD AUTHOR

Hakim Cassimally <osfameron@cpan.org>

=head1 AUTHOR

Ricardo SIGNES <rjbs@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Ricardo SIGNES.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut