-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<?php | ||
|
||
namespace TraderInteractive\Filter; | ||
|
||
use TraderInteractive\Exceptions\FilterException; | ||
|
||
/** | ||
* A collection of filters for JSON | ||
*/ | ||
final class Json | ||
{ | ||
/** | ||
* @var bool | ||
*/ | ||
const DEFAULT_SHOULD_ALLOW_NULL = false; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
const DEFAULT_RECURSION_DEPTH = 512; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
const ERROR_CANNOT_BE_NULL = "Value cannot be null"; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
const ERROR_NOT_A_STRING = "Value '%s' is not a string"; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
const ERROR_INVALID_JSON = "JSON failed validation with message '%s'"; | ||
|
||
/** | ||
* Parses a JSON string and returns the result. | ||
* | ||
* @param mixed $value The value to filter. | ||
* @param bool $shouldAllowNull Allows null values to pass through the filter when set to true. | ||
* @param int $depth The maximum recursion depth. | ||
* | ||
* @return array|bool|int|float|double|null | ||
* | ||
* @throws FilterException Thrown if the value is invalid. | ||
*/ | ||
public static function parse( | ||
$value, | ||
bool $shouldAllowNull = self::DEFAULT_SHOULD_ALLOW_NULL, | ||
int $depth = self::DEFAULT_RECURSION_DEPTH | ||
) { | ||
return self::decode($value, $shouldAllowNull, true, $depth); | ||
} | ||
|
||
/** | ||
* Checks that the JSON is valid and returns the original value. | ||
* | ||
* @param mixed $value The value to filter. | ||
* @param bool $shouldAllowNull Allows null values to pass through the filter when set to true. | ||
* @param int $depth The maximum recursion depth. | ||
* | ||
* @return string|null | ||
* | ||
* @throws FilterException Thrown if the value is invalid. | ||
*/ | ||
public static function validate( | ||
$value, | ||
bool $shouldAllowNull = self::DEFAULT_SHOULD_ALLOW_NULL, | ||
int $depth = self::DEFAULT_RECURSION_DEPTH | ||
) { | ||
self::decode($value, $shouldAllowNull, false, $depth); | ||
return $value; | ||
} | ||
|
||
/** | ||
* Parses a JSON string and returns the result. | ||
* | ||
* @param mixed $value The value to filter. | ||
* @param bool $shouldAllowNull Allows null values to pass through the filter when set to true. | ||
* @param bool $shouldDecodeToArray Decodes the JSON string to an associative array when set to true. | ||
* @param int $depth The maximum recursion depth. | ||
* | ||
* @return string|array|bool|int|float|double|null | ||
* | ||
* @throws FilterException Thrown if the value is invalid. | ||
*/ | ||
private static function decode($value, bool $shouldAllowNull, bool $shouldDecodeToArray, int $depth) | ||
{ | ||
if ($shouldAllowNull && $value === null) { | ||
return $value; | ||
} | ||
|
||
if (!$shouldAllowNull && $value === null) { | ||
throw new FilterException(self::ERROR_CANNOT_BE_NULL); | ||
} | ||
|
||
if (!is_string($value)) { | ||
throw new FilterException(sprintf(self::ERROR_NOT_A_STRING, var_export($value, true))); | ||
} | ||
|
||
$value = json_decode($value, $shouldDecodeToArray, $depth); | ||
$lastErrorCode = json_last_error(); | ||
if ($lastErrorCode !== JSON_ERROR_NONE) { | ||
$message = sprintf(self::ERROR_INVALID_JSON, json_last_error_msg()); | ||
throw new FilterException($message, $lastErrorCode); | ||
} | ||
|
||
return $value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
<?php | ||
|
||
namespace TraderInteractive\Filter; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use TraderInteractive\Exceptions\FilterException; | ||
|
||
/** | ||
* @coversDefaultClass \TraderInteractive\Filter\Json | ||
* @covers ::<private> | ||
*/ | ||
final class JsonFilterTest extends TestCase | ||
{ | ||
/** | ||
* @test | ||
* @covers ::parse | ||
* @dataProvider provideParse | ||
* | ||
* @param string $value The value to filter. | ||
* @param mixed $expected The expected result. | ||
*/ | ||
public function parse(string $value, $expected) | ||
{ | ||
$result = Json::parse($value); | ||
|
||
$this->assertSame($expected, $result); | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function provideParse() : array | ||
{ | ||
return [ | ||
'json' => [ | ||
'value' => '{"a":"b","c":[1,2,[3],{"4":"d"}], "e": "f"}', | ||
'expected' => [ | ||
'a' => 'b', | ||
'c' => [ | ||
1, | ||
2, | ||
[3], | ||
[4 => 'd'], | ||
], | ||
'e' => 'f', | ||
], | ||
], | ||
'null string' => [ | ||
'value' => 'null', | ||
'expected' => null, | ||
], | ||
'integer string' => [ | ||
'value' => '1', | ||
'expected' => 1, | ||
], | ||
'float string' => [ | ||
'value' => '0.000001', | ||
'expected' => 0.000001, | ||
], | ||
'double string' => [ | ||
'value' => '1.56e10', | ||
'expected' => 1.56e10, | ||
], | ||
'true string' => [ | ||
'value' => 'true', | ||
'expected' => true, | ||
], | ||
'false string' => [ | ||
'value' => 'false', | ||
'expected' => false, | ||
], | ||
]; | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::parse | ||
*/ | ||
public function parseNullWithAllowNull() | ||
{ | ||
$value = null; | ||
$result = Json::parse($value, true); | ||
|
||
$this->assertSame($value, $result); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::parse | ||
* @dataProvider provideInvalidJSON | ||
* | ||
* @param mixed $value The value to filter. | ||
* @param string $message The expected error message. | ||
*/ | ||
public function parseThrowsException($value, string $message) | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage($message); | ||
|
||
Json::parse($value); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::parse | ||
*/ | ||
public function parseThrowsExceptionOnNull() | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage(Json::ERROR_CANNOT_BE_NULL); | ||
|
||
Json::parse(null); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::parse | ||
*/ | ||
public function parseThrowsExceptionForRecursionDepth() | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage(sprintf(Json::ERROR_INVALID_JSON, 'Maximum stack depth exceeded')); | ||
|
||
Json::parse('[[]]', false, 1); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::validate | ||
* @dataProvider provideValidate | ||
* | ||
* @param string $value The value to filter. | ||
*/ | ||
public function validate(string $value) | ||
{ | ||
$result = Json::validate($value); | ||
|
||
$this->assertSame($value, $result); | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function provideValidate() : array | ||
{ | ||
return [ | ||
'json' => ['{"a": "b", "c":[1, {"2": 3},[4]], "d": "e"}'], | ||
'null' => ['null'], | ||
'integer string' => ['12345'], | ||
'float string' => ['1.000003'], | ||
'double string' => ['445.2e100'], | ||
'true string' => ['true'], | ||
'false string' => ['false'], | ||
]; | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::validate | ||
*/ | ||
public function validateNullWithAllowNull() | ||
{ | ||
$value = null; | ||
$result = Json::validate($value, true); | ||
|
||
$this->assertSame($value, $result); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::validate | ||
* @dataProvider provideInvalidJSON | ||
* | ||
* @param mixed $value The value to filter. | ||
* @param string $message The expected error message. | ||
*/ | ||
public function validateThrowsException($value, string $message) | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage($message); | ||
|
||
Json::validate($value); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::validate | ||
*/ | ||
public function validateThrowsExceptionOnNull() | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage(Json::ERROR_CANNOT_BE_NULL); | ||
|
||
Json::validate(null); | ||
} | ||
|
||
/** | ||
* @test | ||
* @covers ::validate | ||
*/ | ||
public function validateThrowsExceptionForRecursionDepth() | ||
{ | ||
$this->expectException(FilterException::class); | ||
$this->expectExceptionMessage(sprintf(Json::ERROR_INVALID_JSON, 'Maximum stack depth exceeded')); | ||
|
||
Json::validate('[[]]', false, 1); | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function provideInvalidJSON() : array | ||
{ | ||
return [ | ||
'not a string' => [ | ||
'value' => [], | ||
'message' => sprintf(Json::ERROR_NOT_A_STRING, var_export([], true)), | ||
], | ||
'empty string' => [ | ||
'value' => '', | ||
'message' => sprintf(Json::ERROR_INVALID_JSON, 'Syntax error'), | ||
], | ||
'only whitespace' => [ | ||
'value' => ' ', | ||
'message' => sprintf(Json::ERROR_INVALID_JSON, 'Syntax error'), | ||
], | ||
'non-json string' => [ | ||
'value' => 'some string', | ||
'message' => sprintf(Json::ERROR_INVALID_JSON, 'Syntax error'), | ||
], | ||
'invalid json' => [ | ||
'value' => '{"incomplete":', | ||
'message' => sprintf(Json::ERROR_INVALID_JSON, 'Syntax error'), | ||
], | ||
'unpaired UTF-16 surrogate' => [ | ||
'value' => '["\uD834"]', | ||
'message' => sprintf(Json::ERROR_INVALID_JSON, 'Single unpaired UTF-16 surrogate in unicode escape'), | ||
], | ||
]; | ||
} | ||
} |