2018 twenty-four merry days of Perl Feed

Validation

Method::ParamValidator - 2018-12-22

Today I want to talk about Method::ParamValidator - yet another validator for method parameters.

Why another validator? I was looking for param validators that can be shared among various Google API interfaces that I have worked on. None of the existing solutions worked for me. So I decided to create simple, yet easily configurable, param validator. The best part of this solution is that you can add the param validation programmatically as well as in a configuration file.

I think the Christmas Elves would find this flexibility really useful when working on Santa's codebase.

Configuring Validation

There are two core methods add_field() and add_method() that can be used to setup a LMethod::ParamValidator validator.

add_field(\%params)

First step in creating a validator is to add the fields that can be shared by one or many methods in a validator. You can provide the following keys to add new field.

name unique field name (required).
format data type of the field (optional), possible values are 's' (for string) and 'd' (for digits). Default is 's'.
check code ref for custom check of the field (optional).
source lookup hashref for the acceptable values for the field (optional).
message test message (optional).

add_method(\%params)

After adding fields to the validator, it is time to add method to be validated. You can setup method by providing the following keys.

nameunique method name (required).
fieldshashref with field names.

Example

Suppose our elves want to validate the parameters for a method called add_child(). It requires parameters passed as a hashref with required keys firstname, lastname and age and it also accepts an optional key notes as well. We will first add all the fields first and then we will add the method the method that uses these fields.

use Method::ParamValidator;
my $validator = Method::ParamValidator->new;

# Add fields
$validator->add_field({ name => 'firstname', format => 's' });
$validator->add_field({ name => 'lastname', format => 's' });
$validator->add_field({ name => 'age', format => 'd' });
$validator->add_field({ name => 'notes', format => 's' });

# Add method
$validator->add_method({
    name => 'add_child',
    fields => {
        firstname => 1,
        lastname => 1,
        age => 1,
        notes => 0
    }
});

We can alternatively setup the validator using a configuration file. If we wanted the same checks as above we could create a JSON file like so:

{ "fields"  : [ { "name" : "firstname", "format" : "s" },
                { "name" : "lastname", "format" : "s" },
                { "name" : "age", "format" : "d" },
                { "name" : "notes", "format" : "s" }
              ],
  "methods" : [ { "name" : "add_child",
                  "fields": { "firstname" : "1",
                              "lastname" : "1",
                              "age" : "1",
                              "notes" : "0"
                            }
                }
              ]
}

And now when we instanciate our Method::ParamValidator instance we can simply pass in the name of the configuraton file config.json.

use Method::ParamValidator;
my $validator = Method::ParamValidator->new({ config => "config.json" });

Using Our Validator

Our validator is very simple to use; A call to validate passing in the method name and the parameters will simply throw an exception if the validation fails. We can either let these exceptions terminate our program, or we can catch them using Perl's exception handling (for example with Try::Tiny's try/catch blocks or even with the inbuilt eval keyword.)

Let's demonstrate what kind of error messages the elves are going to get with a test suite. Test::Exception expects an exception to be thrown inside a throws_ok block and conversely fails if one is thrown inside a lives_ok block.

use Test::More;
use Test::Exception;

throws_ok { $validator->validate('get_xyz') } qr/Invalid method name received/;
throws_ok { $validator->validate('add_child') } qr/Missing parameters/;
throws_ok { $validator->validate('add_child', []) } qr/Invalid parameters data structure/;
throws_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L', age => 'A' }) } qr/Parameter failed check constraint/;
throws_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L', age => 10, notes => 's' }) } qr/Parameter failed check constraint/;
throws_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L' }) } qr/Missing required parameter/;
throws_ok { $validator->validate('add_child', { firstname => 'F', lastname => undef, age => 10 }) } qr/Undefined required parameter/;
throws_ok { $validator->validate('add_child', { firstname => 'F' }) } qr/Missing required parameter/;
throws_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L', age => 40, location => 'X' }) } qr/Parameter failed check constraint/;
lives_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L', age => 40, location => 'UK' }) };
lives_ok { $validator->validate('add_child', { firstname => 'F', lastname => 'L', age => 40, location => 'uk' }) };

done_testing();

Custom Checks

Up until this point we've used very simple inbuilt checks: Is this a string? Is this digits? Are the required parameters passed? But what if we want to define something more complicated?

When adding field to a validator, you can hookup your own checks. Below we are adding new field location with a custom check

my $LOCATION = { 'USA' => 1, 'UK' => 1 };

# Add field with custom check
$validator->add_field({
    name => 'location',
    format => 's',
    check => sub {
        exists $LOCATION->{ uc($_[0]) }
    },
});

# Add method using the new field with custom check
$validator->add_method({ name => 'check_location', fields => { location => 1 }});

While we can't create custom Perl code in our configuration file we can create a limited custom check as above:

{
    "fields" : [{ "name" : "location", "format" : "s", "source": [ "USA", "UK" ] } ],
    "methods" : [{ "name" : "check_location", "fields": { "location" : "1" } } ]
}

The array under the source key lists all the values that the location field can have whenever the value is uppercased.

We can demonstrate what kind of error messages our elves are going to see with another test script:

use Test::More;
use Test::Exception;

throws_ok { $validator->validate('check_location', { location => 'X' }) } qr/Parameter failed check constraint/;
done_testing();

Extend Moo Package

So we've seen how we can call the validation up manually, but is there an easy way to add it to multiple methods in a class without having to change the method code?

If you want to plug the validator into an existing Moo package it's easy. For an example let's create package Calculator.

package Calculator;

use Moo;

sub calc {
    my ($self, $param) = @_;

    if ($param->{op} eq 'add') {
        return ($param->{a} + $param->{b});
    }
    elsif ($param->{op} eq 'sub') {
        return ($param->{a} - $param->{b});
    }
    elsif ($param->{op} eq 'mul') {
        return ($param->{a} * $param->{b});
    }
}

Now it is time to create configuration file calc.json for validator.

{ "fields"  : [ { "name" : "op", "format" : "s", "source": [ "add", "sub", "mul" ] },
                { "name" : "a", "format" : "d" },
                { "name" : "b", "format" : "d" }
              ],
  "methods" : [ { "name" : "calc",
                  "fields": { "op" : "1",
                              "a" : "1",
                              "b" : "1"
                            }
                }
              ]
}

Add the following lines to plug the validator.

use Method::ParamValidator;

has 'validator' => (
    is => 'ro',
    default => sub { Method::ParamValidator->new(config => "calc.json") }
);

foreach my $method (qw/calc/) {
    before $method => sub {
        my ($self, $param) = @_;
        $self->validator->validate($method, $param);
    };
}

For the love of TDD, lets define the unit test.

use Test::More;
use Test::Exception;
use Calculator;

my $calc = Calculator->new;

is($calc->calc({ op => 'add', a => 4, b => 2 }), 6);
is($calc->calc({ op => 'sub', a => 4, b => 2 }), 2);
is($calc->calc({ op => 'mul', a => 4, b => 2 }), 8);

throws_ok { $calc->calc({ op => 'add' }) } qr/Missing required parameter. \(a\)/;
throws_ok { $calc->calc({ op => 'add', a => 1 }) } qr/Missing required parameter. \(b\)/;
throws_ok { $calc->calc({ op => 'x', a => 1, b => 2 }) } qr/Parameter failed check constraint. \(op\)/;
throws_ok { $calc->calc({ op => 'add', a => 'x', b => 2 }) } qr/Parameter failed check constraint. \(a\)/;
throws_ok { $calc->calc({ op => 'add', a => 1, b => 'x' }) } qr/Parameter failed check constraint. \(b\)/;

done_testing();

Conclusion

I have used Method::ParamValidator to one of my Google API interface WWW::Google::Places. Any help to extend the validator would be highly appreciated. Or if you have any suggestions please raise them at GitHub.

Gravatar Image This article contributed by: Mohammad S Anwar <mohammad.anwar@yahoo.com>