mirror of
https://github.com/danog/endtoend-test-psl.git
synced 2024-11-26 20:34:59 +01:00
[Json] introduce the JSON API (#46)
This commit is contained in:
parent
25d419536e
commit
788871c9e3
@ -12,7 +12,8 @@
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"ext-bcmath": "*",
|
||||
"ext-mbstring": "*"
|
||||
"ext-mbstring": "*",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"vimeo/psalm": "dev-master",
|
||||
|
@ -6,6 +6,7 @@ namespace Psl\Collection;
|
||||
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* The base interface implemented for a ICollection type.
|
||||
@ -17,7 +18,7 @@ use IteratorAggregate;
|
||||
*
|
||||
* @extends IteratorAggregate<Tk, Tv>
|
||||
*/
|
||||
interface ICollection extends Countable, IteratorAggregate
|
||||
interface ICollection extends Countable, IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Is the ICollection empty?
|
||||
@ -36,6 +37,13 @@ interface ICollection extends Countable, IteratorAggregate
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Get an array copy of the current collection.
|
||||
*
|
||||
* @psalm-return array<Tk, Tv>
|
||||
*/
|
||||
public function jsonSerialize(): array;
|
||||
|
||||
/**
|
||||
* Returns a `ICollection` containing the values of the current `ICollection`
|
||||
* that meet a supplied condition.
|
||||
|
@ -132,6 +132,16 @@ final class Map implements IMap
|
||||
return $this->elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array copy of the current map.
|
||||
*
|
||||
* @psalm-return array<Tk, Tv>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
*
|
||||
|
@ -132,6 +132,16 @@ final class MutableMap implements IMutableMap
|
||||
return $this->elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array copy of the current map.
|
||||
*
|
||||
* @psalm-return array<Tk, Tv>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
*
|
||||
|
@ -65,7 +65,7 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the map empty?
|
||||
* Is the vector empty?
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
@ -73,7 +73,7 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of items in the current map.
|
||||
* Get the number of items in the current vector.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
@ -81,7 +81,7 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array copy of the current map.
|
||||
* Get an array copy of the current vector.
|
||||
*
|
||||
* @psalm-return list<T>
|
||||
*/
|
||||
@ -91,7 +91,17 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
* Get an array copy of the current vector.
|
||||
*
|
||||
* @psalm-return list<T>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*
|
||||
@ -105,7 +115,7 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified key is in the current map.
|
||||
* Determines if the specified key is in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*/
|
||||
@ -115,7 +125,7 @@ final class MutableVector implements IMutableVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
* Returns the value at the specified key in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*
|
||||
|
@ -65,7 +65,7 @@ final class Vector implements IVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the map empty?
|
||||
* Is the vector empty?
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
@ -73,7 +73,7 @@ final class Vector implements IVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of items in the current map.
|
||||
* Get the number of items in the current vector.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
@ -81,7 +81,7 @@ final class Vector implements IVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array copy of the current map.
|
||||
* Get an array copy of the current vector.
|
||||
*
|
||||
* @psalm-return list<T>
|
||||
*
|
||||
@ -92,8 +92,19 @@ final class Vector implements IVector
|
||||
return Arr\values($this->elements);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
* Get an array copy of the current vector.
|
||||
*
|
||||
* @psalm-return list<T>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*
|
||||
@ -107,7 +118,7 @@ final class Vector implements IVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified key is in the current map.
|
||||
* Determines if the specified key is in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*/
|
||||
@ -117,7 +128,7 @@ final class Vector implements IVector
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified key in the current map.
|
||||
* Returns the value at the specified key in the current vector.
|
||||
*
|
||||
* @psalm-param int $k
|
||||
*
|
||||
|
9
src/Psl/Exception/InvalidArgumentException.php
Normal file
9
src/Psl/Exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Exception;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
@ -303,10 +303,16 @@ final class Loader
|
||||
'Psl\Type\string',
|
||||
'Psl\Type\scalar',
|
||||
'Psl\Type\union',
|
||||
'Psl\Json\encode',
|
||||
'Psl\Json\decode',
|
||||
'Psl\Json\typed',
|
||||
];
|
||||
|
||||
public const INTERFACES = [
|
||||
'Psl\Exception\ExceptionInterface',
|
||||
'Psl\Exception\InvalidArgumentException',
|
||||
'Psl\Exception\RuntimeException',
|
||||
'Psl\Exception\InvariantViolationException',
|
||||
'Psl\Asio\IResultOrExceptionWrapper',
|
||||
'Psl\Collection\ICollection',
|
||||
'Psl\Collection\IIndexAccess',
|
||||
@ -337,6 +343,8 @@ final class Loader
|
||||
'Psl\Type\Exception\TypeCoercionException',
|
||||
'Psl\Type\Exception\TypeException',
|
||||
'Psl\Type\Type',
|
||||
'Psl\Json\Exception\JsonDecodeException',
|
||||
'Psl\Json\Exception\JsonEncodeException',
|
||||
];
|
||||
|
||||
public const TRAITS = [
|
||||
|
11
src/Psl/Json/Exception/JsonDecodeException.php
Normal file
11
src/Psl/Json/Exception/JsonDecodeException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json\Exception;
|
||||
|
||||
use Psl\Exception\InvalidArgumentException;
|
||||
|
||||
final class JsonDecodeException extends InvalidArgumentException
|
||||
{
|
||||
}
|
11
src/Psl/Json/Exception/JsonEncodeException.php
Normal file
11
src/Psl/Json/Exception/JsonEncodeException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json\Exception;
|
||||
|
||||
use Psl\Exception\InvalidArgumentException;
|
||||
|
||||
final class JsonEncodeException extends InvalidArgumentException
|
||||
{
|
||||
}
|
35
src/Psl/Json/decode.php
Normal file
35
src/Psl/Json/decode.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use const JSON_BIGINT_AS_STRING;
|
||||
use function json_decode;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonException;
|
||||
use Psl\Str;
|
||||
|
||||
/**
|
||||
* Decode a json encoded string into a dynamic variable.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Exception\JsonDecodeException If an error occurred.
|
||||
*/
|
||||
function decode(string $json, bool $assoc = true)
|
||||
{
|
||||
try {
|
||||
/** @var mixed $value */
|
||||
$value = json_decode(
|
||||
$json,
|
||||
$assoc,
|
||||
512,
|
||||
JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR,
|
||||
);
|
||||
} catch (JsonException $e) {
|
||||
throw new Exception\JsonDecodeException(Str\format('%s.', $e->getMessage()), (int)$e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
41
src/Psl/Json/encode.php
Normal file
41
src/Psl/Json/encode.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use function json_encode;
|
||||
use const JSON_PRESERVE_ZERO_FRACTION;
|
||||
use const JSON_PRETTY_PRINT;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
use const JSON_UNESCAPED_UNICODE;
|
||||
use JsonException;
|
||||
use Psl\Str;
|
||||
|
||||
/**
|
||||
* Returns a string containing the JSON representation of the supplied value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws Exception\JsonEncodeException If an error occurred.
|
||||
*/
|
||||
function encode($value, bool $pretty = false, int $flags = 0): string
|
||||
{
|
||||
$flags |= JSON_UNESCAPED_UNICODE
|
||||
| JSON_UNESCAPED_SLASHES
|
||||
| JSON_PRESERVE_ZERO_FRACTION
|
||||
| JSON_THROW_ON_ERROR;
|
||||
|
||||
if ($pretty) {
|
||||
$flags |= JSON_PRETTY_PRINT;
|
||||
}
|
||||
|
||||
try {
|
||||
$json = json_encode($value, $flags);
|
||||
} catch (JsonException $e) {
|
||||
throw new Exception\JsonEncodeException(Str\format('%s.', $e->getMessage()), (int)$e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
36
src/Psl/Json/typed.php
Normal file
36
src/Psl/Json/typed.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use Psl\Type\Exception\TypeAssertException;
|
||||
use Psl\Type\Exception\TypeCoercionException;
|
||||
use Psl\Type\Type;
|
||||
|
||||
/**
|
||||
* Decode a json encoded string into a dynamic variable.
|
||||
*
|
||||
* @psalm-template T
|
||||
*
|
||||
* @psalm-param Type<T> $type
|
||||
*
|
||||
* @psalm-return T
|
||||
*
|
||||
* @throws Exception\JsonDecodeException If an error occurred.
|
||||
*/
|
||||
function typed(string $json, Type $type)
|
||||
{
|
||||
$value = decode($json);
|
||||
|
||||
try {
|
||||
return $type->assert($value);
|
||||
} catch (TypeAssertException $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
return $type->coerce($value);
|
||||
} catch (TypeCoercionException $e) {
|
||||
throw new Exception\JsonDecodeException($e->getMessage(), (int)$e->getCode(), $e);
|
||||
}
|
||||
}
|
46
tests/Psl/Json/DecodeTest.php
Normal file
46
tests/Psl/Json/DecodeTest.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psl\Json;
|
||||
|
||||
class DecodeTest extends TestCase
|
||||
{
|
||||
public function testDecode(): void
|
||||
{
|
||||
$actual = Json\decode('{
|
||||
"name": "azjezz/psl",
|
||||
"type": "library",
|
||||
"description": "PHP Standard Library.",
|
||||
"keywords": ["php", "std", "stdlib", "utility", "psl"],
|
||||
"license": "MIT"
|
||||
}');
|
||||
|
||||
self::assertSame([
|
||||
'name' => 'azjezz/psl',
|
||||
'type' => 'library',
|
||||
'description' => 'PHP Standard Library.',
|
||||
'keywords' => ['php', 'std', 'stdlib', 'utility', 'psl'],
|
||||
'license' => 'MIT'
|
||||
], $actual);
|
||||
}
|
||||
|
||||
public function testDecodeThrowsForInvalidSyntax(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonDecodeException::class);
|
||||
$this->expectExceptionMessage('The decoded property name is invalid.');
|
||||
|
||||
Json\decode('{"\u0000": 1}', false);
|
||||
}
|
||||
|
||||
public function testDecodeMalformedUTF8(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonDecodeException::class);
|
||||
$this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded.');
|
||||
|
||||
Json\decode("\"\xC1\xBF\"");
|
||||
}
|
||||
}
|
77
tests/Psl/Json/EncodeTest.php
Normal file
77
tests/Psl/Json/EncodeTest.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psl\Json;
|
||||
use Psl\Math;
|
||||
|
||||
class EncodeTest extends TestCase
|
||||
{
|
||||
public function testEncode(): void
|
||||
{
|
||||
$actual = Json\encode(['a']);
|
||||
|
||||
self::assertSame('["a"]', $actual);
|
||||
}
|
||||
|
||||
public function testPrettyEncode(): void
|
||||
{
|
||||
$actual = Json\encode([
|
||||
'name' => 'azjezz/psl',
|
||||
'type' => 'library',
|
||||
'description' => 'PHP Standard Library.',
|
||||
'keywords' => ['php', 'std', 'stdlib', 'utility', 'psl'],
|
||||
'license' => 'MIT'
|
||||
], true);
|
||||
|
||||
$json = <<<JSON
|
||||
{
|
||||
"name": "azjezz/psl",
|
||||
"type": "library",
|
||||
"description": "PHP Standard Library.",
|
||||
"keywords": [
|
||||
"php",
|
||||
"std",
|
||||
"stdlib",
|
||||
"utility",
|
||||
"psl"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
JSON;
|
||||
|
||||
self::assertSame($json, $actual);
|
||||
}
|
||||
|
||||
public function testEncodeThrowsForMalformedUTF8(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonEncodeException::class);
|
||||
$this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded.');
|
||||
|
||||
Json\encode(["bad utf\xFF"]);
|
||||
}
|
||||
|
||||
public function testEncodeThrowsWithNAN(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonEncodeException::class);
|
||||
$this->expectExceptionMessage('Inf and NaN cannot be JSON encoded.');
|
||||
|
||||
Json\encode(Math\NaN);
|
||||
}
|
||||
|
||||
public function testEncodeThrowsWithInf(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonEncodeException::class);
|
||||
$this->expectExceptionMessage('Inf and NaN cannot be JSON encoded.');
|
||||
|
||||
Json\encode(Math\INFINITY);
|
||||
}
|
||||
|
||||
public function testEncodePreserveZeroFraction(): void
|
||||
{
|
||||
self::assertSame('1.0', Json\encode(1.0));
|
||||
}
|
||||
}
|
59
tests/Psl/Json/TypedTest.php
Normal file
59
tests/Psl/Json/TypedTest.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psl\Json;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psl\Json;
|
||||
use Psl\Type;
|
||||
|
||||
class TypedTest extends TestCase
|
||||
{
|
||||
public function testTyped(): void
|
||||
{
|
||||
$actual = Json\typed('{
|
||||
"name": "azjezz/psl",
|
||||
"type": "library",
|
||||
"description": "PHP Standard Library.",
|
||||
"keywords": ["php", "std", "stdlib", "utility", "psl"],
|
||||
"license": "MIT"
|
||||
}', Type\arr(Type\string(), Type\union(Type\string(), Type\arr(Type\int(), Type\string()))));
|
||||
|
||||
self::assertSame([
|
||||
'name' => 'azjezz/psl',
|
||||
'type' => 'library',
|
||||
'description' => 'PHP Standard Library.',
|
||||
'keywords' => ['php', 'std', 'stdlib', 'utility', 'psl'],
|
||||
'license' => 'MIT'
|
||||
], $actual);
|
||||
}
|
||||
|
||||
public function testTypedThrowsWhenUnableToCoerce(): void
|
||||
{
|
||||
$this->expectException(Json\Exception\JsonDecodeException::class);
|
||||
$this->expectExceptionMessage('Could not coerce "string" to type "int".');
|
||||
|
||||
Json\typed('{
|
||||
"name": "azjezz/psl",
|
||||
"type": "library",
|
||||
"description": "PHP Standard Library.",
|
||||
"keywords": ["php", "std", "stdlib", "utility", "psl"],
|
||||
"license": "MIT"
|
||||
}', Type\arr(Type\string(), Type\int()));
|
||||
}
|
||||
|
||||
public function testsTypedAsserts(): void
|
||||
{
|
||||
$actual = Json\typed('{"foo": "bar"}', Type\arr(Type\string(), Type\string()));
|
||||
|
||||
self::assertSame(['foo' => 'bar'], $actual);
|
||||
}
|
||||
|
||||
public function testTypedCoerce(): void
|
||||
{
|
||||
$actual = Json\typed('{"foo": 123}', Type\arr(Type\string(), Type\string()));
|
||||
|
||||
self::assertSame(['foo' => '123'], $actual);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user