NAME

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

VERSION

version 0.200008

SYNOPSIS

The easiest way to create a custom type plugin is to subclass 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 );

OVERVIEW

Data::Rx ships with a variety of core validators -- single, collection, and combination 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 type plugins in any implementation, including Data::Rx in Perl.

PERL VERSION

This library should run on perls released even a long time ago. It should work on any version of perl released in the last five years.

Although it may work on older versions of perl, no guarantee is made that the minimum required version will not be increased. The version may be increased for any reason, and there is no promise that patches will be accepted to lower the minimum required perl.

EXAMPLES

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

W3C DateTime - using Perl and CPAN in checks

We might want to validate dates in the W3CDTF format, which look like 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, DateTime::Format::W3CDTF.

Our parser, then, will instantiate one of these objects, and return it with 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 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 Data::Rx::CommonType::EasyNew's provided method 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' );

Enum - delegate to another schema

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

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

This is a bit clumsy though, with the repetition of the type //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 $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 //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 assert_valid we know that any exceptions will be in the correct format. However, the error will be the one that //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 check instead of assert_valid and raising our own, nicely formatted, exception using fail.

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 //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 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 contents argument is required, and should be a valid schema. We've had to make a few trade-offs:

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

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

As we are expecting a comma separated 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 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!

POD AUTHOR

Hakim Cassimally <osfameron@cpan.org>

AUTHOR

Ricardo SIGNES <cpan@semiotic.systems>

COPYRIGHT AND LICENSE

This software is copyright (c) 2023 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.