beberlei / assert

Thin assertion library for use in libraries and business-model
Other
2.41k stars 188 forks source link

Assert that it verifies a list of assertion #257

Closed clement-michelet closed 6 years ago

clement-michelet commented 6 years ago

Use case :

I have multiple configurations which define the list of constraints on a password (length, chars, etc.). I want to generate the list of assertions to check on a service depending the configuration. The generated list will be added to an assertion which contains other static assertions.

e.g (quick & dirty):

$configuration = ['minLength' => 8, 'blacklist' => ['password', 'azerty']];

function convertToAssertions(array $configuration): array {
   if(isset($configuration['minLength'])) {
     $assertions[] = Assert::lazy()->minLength($configuration['minLength'], 'Assertion error message');
   }

   if(isset($configuration['blacklist'])) {
     $assertions[] = Assert::lazy()->notInArray($configuration['blacklist'], 'Other error message');
   }

   return $assertions;
}

$assertions = convertToAssertions($configuration);

$object = new stdClass();
$object->foo = '';
$object->password = 'azerty';

$assert = Assert::lazy()->tryAll();
$assert->that($object->foo, 'foo)->notEmpty('Empty foo');
$assert->that($object->password, 'password')->verifyAllAssertions($assertions);

$assert->verifyNow();

// LazyAssertionException with : ['foo' => ['Empty foo'], 'password' => ['Assertion error message', 'Other error message']]

How to do it ?

I could pass the assertion as an argument of my configuration converter and add directly my password assertions to the main assertion. But I would prefer to separate the configuration conversion and the attachment to assertion part.

Is there a way to do that ?

rquadling commented 6 years ago

Not without some complicated mechanism that I can't think of at the moment.

Injecting the form values into the assertion builder is the only way I think you can do this.

Lazy assertion isn't that lazy. It is lazy in as far as you don't get the failures immediately and have to call verifyNow() to get result.

That's the answer.

Below is some idle thoughts/ideas/suggestions. Take them or leave them as you see fit.

I'd like to suggest an option against the way you've defined things.

The code that builds a list of assertions has to evaluate the configuration upon every request. You COULD cache that expanded configuration, but injecting the values into that is going to be complex.

Rather than evaluating the configuration upon every request, write a code generator that converts the configuration into actual PHP code that would perform the exact task required. Think of it like a validation compiler. You've defined the rules, now build the code that does it.

Using generators like this are quite common. You can start adding more complex rules to the configuration and then support them in the generator.

The main idea is to think about the redundant code and offload that somehow. Building the list of assertions is the redundancy. The execution of the exact assertions is the only thing you really want to be doing in the request.

Sure, the code COULD look ugly, but only if your generator creates ugly code!

The generator should generate the exact code that is required to be executed as if you had written it all out long had. No branching, no ifs. This makes your testing simpler as there is only 1 execution path, regardless of any data you are providing.

clement-michelet commented 6 years ago

Thanks for the feedback.

I see your point for the generator. I can't generate static validator for the moment because those configurations are persisted in database and the validator depends on what is selected in a form (e.g. an user is related to a set of password rules). It would require to generate static code on configuration change which is a bit cumbersome for the type of application (restricted backend) and the use (only on password change). But I take the idea for later because it could be a simple use case to generate static code which I could need in a different part of the application.

My current implementation use __call on my main validator with arguments from all expectations. I put below how it works with the previous example in case of someone want to do something like this.

$configuration = ['minLength' => 8, 'blacklist' => ['password', 'azerty']];

function convertToAssertions(array $configuration): array {
   if(isset($configuration['minLength'])) {
     $assertions[] = ['minLength', [$configuration['minLength'], 'Assertion error message']];
   }

   if(isset($configuration['blacklist'])) {
     $assertions[] = ['notInArray', [$configuration['blacklist'], 'Other error message']];
   }

   return $assertions;
}

$assertions = convertToAssertions($configuration);

$object = new stdClass();
$object->foo = '';
$object->password = 'azerty';

$assert = Assert::lazy()->tryAll();
$assert->that($object->foo, 'foo)->notEmpty('Empty foo');

foreach ($assertions as [$assertion, $parameters]) {
    $assert->__call($assertion, $parameters);
}
$assert->verifyNow();

I close the issue.