Skip to content

Commit

Permalink
Support mixed types for validators (#8)
Browse files Browse the repository at this point in the history
* Support mixed types for validators

Changes:
- Allow arrays to be received as a body parameter
- All validators expect mixed input/output instead of just string
- More supporting tests for validators and data mapping
- Fixed documentation on validators

Fixes:
- When missing a JSON attribute throw exception not fatal error
- Correctly passing the validators from the parameters
  • Loading branch information
willitscale authored Jan 16, 2024
1 parent 907a58c commit d62aa14
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 24 deletions.
1 change: 1 addition & 0 deletions docs/TODO.MD
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Welcome to the TODO list. This is just a collection of thoughts and ideas on how
- The ability to print out the current paths of the application.
- A way to easily clear the route cache.
- The new caching mechanism should support multiple caches, future iterations of the caching will offer multi cache support.
- Support JSON arrays passed to the body and as an attribute of a class

## Resilience
- More Tests
Expand Down
6 changes: 4 additions & 2 deletions docs/VALIDATORS.MD
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ With Streetlamp comes a small selection of pre-built validators:
| `IntValidator` | Returns an int value | Validates input is an int within the given range |
| `RegExpValidator` | Returns a string matching the replace criteria if set, otherwise returns input | Validates input matches the regular expression pattern given |

Validators can be used along side either one of the input parameters or within data bindings.
Validators can be used alongside either one of the input parameters or within data bindings.

## Definition

Expand All @@ -28,7 +28,8 @@ FilterVarsValidator(int $filter, int|array $options = 0);
#[Method(HttpMethod::GET)]
#[Path('/validator/{validatorId}')]
public function simpleGetWithPathParameterAndValidator(
#[PathParameter('validatorId'] int $validatorId
#[PathParameter('validatorId']
#[MustBeDivisibleByThreeValidator] int $validatorId
): ResponseBuilder {
return (new ResponseBuilder())
->setData($validatorId)
Expand All @@ -45,6 +46,7 @@ When implementing a custom validator if you throw any exceptions, it's essential
Example of a custom validator:

```php
#[Attribute(Attribute::TARGET_PARAMETER)]
readonly class MustBeDivisibleByThreeValidator implements ValidatorInterface
{
public function validate(string $value): bool
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/DataBindings/DataBindingObjectInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

interface DataBindingObjectInterface
{
public function build(ReflectionClass $class, string $data): object;
public function build(ReflectionClass $reflectionClass, string $data): object;

public function getObject(ReflectionClass $class, mixed $data): object;
public function getObject(ReflectionClass $reflectionClass, mixed $data): object;

public function getSerializable(ReflectionClass $reflectionClass, object $object): mixed;
}
5 changes: 3 additions & 2 deletions src/Attributes/DataBindings/Json/JsonProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ public function __construct(
public function buildProperty(object $instance, ReflectionProperty $property, mixed $jsonValue): void
{
$key = (empty($this->alias)) ? $property->getName() : $this->alias;
$value = $jsonValue->$key;

if ($this->required && empty($value)) {
if ($this->required && empty($jsonValue->{$key})) {
$className = get_class($instance);
throw new InvalidParameterTypeException(
"JS001",
"Parameter $key in $className is required, but not passed."
);
}

$value = $jsonValue->{$key};

$attributes = $property->getAttributes();

foreach ($attributes as $attribute) {
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Parameter/BodyParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public function __construct(

/**
* @param array $pathMatches
* @return string|int|bool|float
* @return string|int|bool|float|array
* @throws MissingRequireBodyException
*/
public function value(array $pathMatches): string|int|bool|float
public function value(array $pathMatches): string|int|bool|float|array
{
$streamValue = file_get_contents($this->resourceIdentifier);

Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Parameter/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
abstract class Parameter
{
protected string $type;
protected array $validators = [];

public function __construct(
protected readonly string|null $key
protected readonly string|null $key,
protected array $validators = []
) {
}

Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/AlphabeticValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class AlphabeticValidator implements ValidatorInterface
{
public function validate(string $value): bool
public function validate(mixed $value): bool
{
return preg_match('/^[a-z]+$/i', $value);
}

public function sanitize(string $value): string
public function sanitize(mixed $value): mixed
{
return $value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/EmailValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class EmailValidator implements ValidatorInterface
{
public function validate(string $value): bool
public function validate(mixed $value): bool
{
return filter_var($value, FILTER_VALIDATE_EMAIL);
}

public function sanitize(string $value): string
public function sanitize(mixed $value): mixed
{
return $value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/FilterVarsValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ public function __construct(
) {
}

public function validate(string $value): bool
public function validate(mixed $value): bool
{
return (false !== filter_var($value, $this->filter, $this->options));
}

public function sanitize(string $value): string|int|float|bool
public function sanitize(mixed $value): mixed
{
return filter_var($value, $this->filter, $this->options);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/FloatValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public function __construct(private float $max = PHP_FLOAT_MAX, private float $m
{
}

public function validate(string $value): bool
public function validate(mixed $value): bool
{
return (floatval($value) == $value) && $value <= $this->max && $value >= $this->min;
}

public function sanitize(string $value): float
public function sanitize(mixed $value): float
{
return (float) $value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/IntValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public function __construct(private int $max = PHP_INT_MAX, private int $min = 0
{
}

public function validate(string $value): bool
public function validate(mixed $value): bool
{
return (intval($value) == $value) && $value <= $this->max && $value >= $this->min;
}

public function sanitize(string $value): int
public function sanitize(mixed $value): int
{
return (int) $value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/RegExpValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public function __construct(private string $pattern, private string|null $replac
{
}

public function validate(string $value): bool
public function validate(mixed $value): bool
{
return preg_match($this->pattern, $value);
}

public function sanitize(string $value): string
public function sanitize(mixed $value): mixed
{
if (!$this->replace) {
return $value;
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Validators/ValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

interface ValidatorInterface
{
public function validate(string $value): bool;
public function sanitize(string $value): string|int|float|bool;
public function validate(mixed $value): bool;
public function sanitize(mixed $value): mixed;
}
117 changes: 117 additions & 0 deletions tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,33 @@
namespace willitscale\StreetlampTests;

use willitscale\Streetlamp\Enums\MediaType;
use willitscale\Streetlamp\Exceptions\InvalidParameterTypeException;
use willitscale\Streetlamp\Exceptions\Validators\InvalidParameterFailedToPassFilterValidation;
use willitscale\StreetlampTest\RouteTestCase;

class RouterTest extends RouteTestCase
{
const TEST_BODY_FILE = __DIR__ . DIRECTORY_SEPARATOR . 'TestApp' . DIRECTORY_SEPARATOR . 'test.dat';
const COMPOSER_TEST_FILE = __DIR__ . DIRECTORY_SEPARATOR . 'TestApp' . DIRECTORY_SEPARATOR . 'composer.test.json';

public function setUp(): void
{
parent::setUp();

if (file_exists(self::TEST_BODY_FILE)) {
unlink(self::TEST_BODY_FILE);
}
}

protected function tearDown(): void
{
parent::tearDown();

if (file_exists(self::TEST_BODY_FILE)) {
unlink(self::TEST_BODY_FILE);
}
}

public function testRouterGetMethodWithNoParameters(): void
{
$router = $this->setupRouter(
Expand Down Expand Up @@ -173,4 +194,100 @@ public function testRouterCacheAlwaysReturnsTheParameterCachedValue(): void

$this->assertEquals($secondCachedValue, $response);
}

public function testRouterDataMappingCorrectlyCreatesAnObject(): void
{
$testData = [
'name' => 'Test',
'age' => 123
];

file_put_contents(self::TEST_BODY_FILE, json_encode($testData));

$router = $this->setupRouter(
'POST',
'/data/validation',
MediaType::APPLICATION_JSON->value,
__DIR__,
self::COMPOSER_TEST_FILE
);

$response = $router->route(true);

$this->assertEquals($testData, json_decode($response, true));
}

public function testRouterDataMappingWithIncorrectDataFailsToCreateObject(): void
{
$testData = [
'name' => 'Test'
];

$testFile =
file_put_contents(self::TEST_BODY_FILE, json_encode($testData));

$router = $this->setupRouter(
'POST',
'/data/validation',
MediaType::APPLICATION_JSON->value,
__DIR__,
self::COMPOSER_TEST_FILE
);

$this->expectException(InvalidParameterTypeException::class);
$router->route(true);
}

public function testRouterDataMappingCorrectlyCreatesAnArrayOfObjects(): void
{
$testData = [
[
'name' => 'Test',
'age' => 123
],
[
'name' => 'Tester',
'age' => 456
]
];

file_put_contents(self::TEST_BODY_FILE, json_encode($testData));

$router = $this->setupRouter(
'POST',
'/data/validations',
MediaType::APPLICATION_JSON->value,
__DIR__,
self::COMPOSER_TEST_FILE
);

$response = $router->route(true);
$this->assertEquals($testData, json_decode($response, true));
}

public function testRouterDataMappingWithIncorrectDataFailsToCreateAnArrayOfObjects(): void
{
$testData = [
[
'name' => 'Test',
'age' => 123
],
[
'name' => 'Tester'
]
];

file_put_contents(self::TEST_BODY_FILE, json_encode($testData));

$router = $this->setupRouter(
'POST',
'/data/validations',
MediaType::APPLICATION_JSON->value,
__DIR__,
self::COMPOSER_TEST_FILE
);

$this->expectException(InvalidParameterFailedToPassFilterValidation::class);
$router->route(true);
}
}
22 changes: 22 additions & 0 deletions tests/TestApp/DataType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace willitscale\StreetlampTests\TestApp;

use JsonSerializable;
use willitscale\Streetlamp\Attributes\DataBindings\Json\JsonObject;
use willitscale\Streetlamp\Attributes\DataBindings\Json\JsonProperty;

#[JsonObject]
class DataType implements JsonSerializable
{
public function __construct(
#[JsonProperty(true)] private string $name,
#[JsonProperty(true)] private int $age
) {
}

public function jsonSerialize(): mixed
{
return (object)get_object_vars($this);
}
}
38 changes: 38 additions & 0 deletions tests/TestApp/DataValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace willitscale\StreetlampTests\TestApp;

use Attribute;
use willitscale\Streetlamp\Attributes\Validators\ValidatorInterface;

#[Attribute]
class DataValidator implements ValidatorInterface
{
public function validate(mixed $value): bool
{
$value = json_decode($value);

if (!is_array($value)) {
return false;
}

foreach ($value as $object) {
if (!isset($object->name) || !isset($object->age)) {
return false;
}
}

return true;
}

public function sanitize(mixed $value): mixed
{
$value = json_decode($value);

foreach ($value as &$object) {
$object = new DataType($object->name, $object->age);
}

return $value;
}
}
Loading

0 comments on commit d62aa14

Please sign in to comment.