[Json] introduce the JSON API (#46)

This commit is contained in:
Saif Eddin G 2020-08-08 06:09:49 +02:00 committed by GitHub
parent 25d419536e
commit 788871c9e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 397 additions and 14 deletions

View File

@ -12,7 +12,8 @@
"require": {
"php": "^7.4",
"ext-bcmath": "*",
"ext-mbstring": "*"
"ext-mbstring": "*",
"ext-json": "*"
},
"require-dev": {
"vimeo/psalm": "dev-master",

View File

@ -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.

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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
*

View File

@ -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
*

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Psl\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -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 = [

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Psl\Json\Exception;
use Psl\Exception\InvalidArgumentException;
final class JsonDecodeException extends InvalidArgumentException
{
}

View 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
View 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
View 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
View 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);
}
}

View 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\"");
}
}

View 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));
}
}

View 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);
}
}