NAME
Data::Rx::Manual::CustomTypes - overview of making new checkers
VERSION
version 0.200007
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.
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 tomake_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 <rjbs@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2015 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.