Data::Rx::Manual::CustomTypes - overview of making new checkers
version 0.200006
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 );
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.
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.
examples/
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.
2003-02-15T13:50:05-05:00
Our parser, then, will instantiate one of these objects, and return it with guts_from_arg to be stashed away.
guts_from_arg
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:
assert_valid
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:
Data::Rx::CommonType::EasyNew
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' );
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:
open
closed
0
15
30
40
//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:
//str
{ 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:
$arg
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.
check
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!
//arr
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.
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:
contents
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!)
make_schema
Rx's type //bool is deliberately targeted at JSON like boolean objects, so we'll also accept undef and 1 as "truthy" values.
//bool
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!
Hakim Cassimally <osfameron@cpan.org>
Ricardo SIGNES <rjbs@cpan.org>
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.
To install Data::Rx, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Data::Rx
CPAN shell
perl -MCPAN -e shell install Data::Rx
For more information on module installation, please visit the detailed CPAN module installation guide.