Skip to content

Commit

Permalink
Implement a custom stringifier
Browse files Browse the repository at this point in the history
The standards `CompositeStringifier` from "respect/stringifier" has lots
of interesting stringifiers. However, this library is not 100% focused
on engineers. Someone could type a string that matches a callable, and
then you will overexpose the system.

This commit makes sure that callables are not interpreted as callables.
  • Loading branch information
henriquemoody committed Dec 26, 2024
1 parent a3c197f commit b8f4949
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 14 deletions.
9 changes: 3 additions & 6 deletions library/Message/StandardRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

use ReflectionClass;
use Respect\Stringifier\Stringifier;
use Respect\Stringifier\Stringifiers\CompositeStringifier;
use Respect\Validation\Mode;
use Respect\Validation\Result;
use Respect\Validation\Rule;
Expand All @@ -27,11 +26,9 @@ final class StandardRenderer implements Renderer
/** @var array<string, array<Template>> */
private array $templates = [];

private readonly Stringifier $stringifier;

public function __construct(?Stringifier $stringifier = null)
{
$this->stringifier = $stringifier ?? CompositeStringifier::createDefault();
public function __construct(
private readonly Stringifier $stringifier = new StandardStringifier(),
) {
}

public function render(Result $result, Translator $translator, ?string $template = null): string
Expand Down
92 changes: 92 additions & 0 deletions library/Message/StandardStringifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Message;

use DateTimeInterface;
use Respect\Stringifier\Quoter;
use Respect\Stringifier\Quoters\StandardQuoter;
use Respect\Stringifier\Stringifier;
use Respect\Stringifier\Stringifiers\ArrayObjectStringifier;
use Respect\Stringifier\Stringifiers\ArrayStringifier;
use Respect\Stringifier\Stringifiers\BoolStringifier;
use Respect\Stringifier\Stringifiers\CompositeStringifier;
use Respect\Stringifier\Stringifiers\DateTimeStringifier;
use Respect\Stringifier\Stringifiers\DeclaredStringifier;
use Respect\Stringifier\Stringifiers\EnumerationStringifier;
use Respect\Stringifier\Stringifiers\InfiniteNumberStringifier;
use Respect\Stringifier\Stringifiers\IteratorObjectStringifier;
use Respect\Stringifier\Stringifiers\JsonEncodableStringifier;
use Respect\Stringifier\Stringifiers\JsonSerializableObjectStringifier;
use Respect\Stringifier\Stringifiers\NotANumberStringifier;
use Respect\Stringifier\Stringifiers\NullStringifier;
use Respect\Stringifier\Stringifiers\ObjectStringifier;
use Respect\Stringifier\Stringifiers\ObjectWithDebugInfoStringifier;
use Respect\Stringifier\Stringifiers\ResourceStringifier;
use Respect\Stringifier\Stringifiers\StringableObjectStringifier;
use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier;

final class StandardStringifier implements Stringifier
{
private const MAXIMUM_DEPTH = 3;
private const MAXIMUM_NUMBER_OF_ITEMS = 5;
private const MAXIMUM_NUMBER_OF_PROPERTIES = self::MAXIMUM_NUMBER_OF_ITEMS;
private const MAXIMUM_LENGTH = 120;

private readonly Stringifier $stringifier;

public function __construct(
private readonly Quoter $quoter = new StandardQuoter(self::MAXIMUM_LENGTH)
) {
$this->stringifier = $this->createStringifier($quoter);
}

public function stringify(mixed $raw, int $depth): string
{
return $this->stringifier->stringify($raw, $depth) ?? $this->quoter->quote('unknown', $depth);
}

private function createStringifier(Quoter $quoter): Stringifier
{
$jsonEncodableStringifier = new JsonEncodableStringifier();

$stringifier = new CompositeStringifier(
new InfiniteNumberStringifier($quoter),
new NotANumberStringifier($quoter),
new ResourceStringifier($quoter),
new BoolStringifier($quoter),
new NullStringifier($quoter),
new DeclaredStringifier($quoter),
$jsonEncodableStringifier,
);
$arrayStringifier = new ArrayStringifier(
$stringifier,
$quoter,
self::MAXIMUM_DEPTH,
self::MAXIMUM_NUMBER_OF_ITEMS,
);
$stringifier->prependStringifier($arrayStringifier);
$stringifier->prependStringifier(new ObjectStringifier(
$stringifier,
$quoter,
self::MAXIMUM_DEPTH,
self::MAXIMUM_NUMBER_OF_PROPERTIES
));
$stringifier->prependStringifier(new EnumerationStringifier($quoter));
$stringifier->prependStringifier(new ObjectWithDebugInfoStringifier($arrayStringifier, $quoter));
$stringifier->prependStringifier(new ArrayObjectStringifier($arrayStringifier, $quoter));
$stringifier->prependStringifier(new JsonSerializableObjectStringifier($jsonEncodableStringifier, $quoter));
$stringifier->prependStringifier(new StringableObjectStringifier($jsonEncodableStringifier, $quoter));
$stringifier->prependStringifier(new ThrowableObjectStringifier($jsonEncodableStringifier, $quoter));
$stringifier->prependStringifier(new DateTimeStringifier($quoter, DateTimeInterface::ATOM));
$stringifier->prependStringifier(new IteratorObjectStringifier($stringifier, $quoter));

return $stringifier;
}
}
19 changes: 19 additions & 0 deletions tests/feature/Message/StandardStringifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

use Respect\Validation\Message\StandardStringifier;

test('Should return `unknown` when cannot stringify value', function (): void {
$resource = tmpfile();
fclose($resource);

$stringifier = new StandardStringifier();

expect($stringifier->stringify($resource, 0))->toBe('`unknown`');
});
4 changes: 2 additions & 2 deletions tests/feature/Rules/CallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

test('Scenario #3', expectMessage(
fn() => v::call('stripslashes', v::alwaysValid())->assert([]),
'`[]` must be a suitable argument for `stripslashes(string $string): string`',
'`[]` must be a suitable argument for "stripslashes"',
));

test('Scenario #4', expectFullMessage(
Expand All @@ -34,5 +34,5 @@

test('Scenario #6', expectFullMessage(
fn() => v::call('array_shift', v::alwaysValid())->assert(INF),
'- `INF` must be a suitable argument for `array_shift(array &$array): ?mixed`',
'- `INF` must be a suitable argument for "array_shift"',
));
4 changes: 2 additions & 2 deletions tests/feature/Rules/CallableTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

test('Scenario #2', expectMessage(
fn() => v::not(v::callableType())->assert('trim'),
'`trim(string $string, string $characters = " \\n\\r\\t\\u000b\\u0000"): string` must not be a callable',
'"trim" must not be a callable',
));

test('Scenario #3', expectFullMessage(
Expand All @@ -26,5 +26,5 @@
fn() => v::not(v::callableType())->assert(function (): void {
// Do nothing
}),
'- `function (): void` must not be a callable',
'- `Closure {}` must not be a callable',
));
2 changes: 1 addition & 1 deletion tests/feature/Rules/CnpjTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

test('Scenario #3', expectFullMessage(
fn() => v::cnpj()->assert('test'),
'- `test(?string $description = null, ?Closure $closure = null): Pest\\Support\\HigherOrderTapProxy|Pest\\PendingCalls\\Te ...` must be a valid CNPJ number',
'- "test" must be a valid CNPJ number',
));

test('Scenario #4', expectFullMessage(
Expand Down
2 changes: 1 addition & 1 deletion tests/feature/Rules/ObjectTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

test('Scenario #3', expectFullMessage(
fn() => v::objectType()->assert('test'),
'- `test(?string $description = null, ?Closure $closure = null): Pest\\Support\\HigherOrderTapProxy|Pest\\PendingCalls\\Te ...` must be an object',
'- "test" must be an object',
));

test('Scenario #4', expectFullMessage(
Expand Down
2 changes: 1 addition & 1 deletion tests/feature/Rules/ResourceTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

test('Scenario #1', expectMessage(
fn() => v::resourceType()->assert('test'),
'`test(?string $description = null, ?Closure $closure = null): Pest\\Support\\HigherOrderTapProxy|Pest\\PendingCalls\\Te ...` must be a resource',
'"test" must be a resource',
));

test('Scenario #2', expectMessage(
Expand Down
2 changes: 1 addition & 1 deletion tests/feature/Rules/UniqueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

test('Scenario #3', expectFullMessage(
fn() => v::unique()->assert('test'),
'- `test(?string $description = null, ?Closure $closure = null): Pest\\Support\\HigherOrderTapProxy|Pest\\PendingCalls\\Te ...` must not contain duplicates',
'- "test" must not contain duplicates',
));

test('Scenario #4', expectFullMessage(
Expand Down

0 comments on commit b8f4949

Please sign in to comment.