![]() |
![]()
| ![]() |
![]()
NAMEData::Rx::Manual::CustomTypes - overview of making new checkers VERSIONversion 0.200008 SYNOPSISThe 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 ); OVERVIEWData::Rx ships with a variety of core validators -- single <http://rx.codesimply.com/coretypes.html#single>, collection <http://rx.codesimply.com/coretypes.html#collect>, and 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 type plugins in any implementation, including Data::Rx in Perl. PERL VERSIONThis 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. EXAMPLESThese 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 checksWe 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 schemaYou'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 inputSome 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:
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 AUTHORHakim Cassimally <osfameron@cpan.org> AUTHORRicardo SIGNES <cpan@semiotic.systems> COPYRIGHT AND LICENSEThis 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.
|