Skip to content

Commit 86ccf59

Browse files
committed
Add JSON scalar
1 parent 1f8101d commit 86ccf59

File tree

4 files changed

+168
-1
lines changed

4 files changed

+168
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ This package comes with a bunch of scalars that are ready-to-use and just work o
2626

2727
A [RFC 5321](https://tools.ietf.org/html/rfc5321) compliant email.
2828

29+
### [JSON](src/JSON.php)
30+
31+
Arbitrary data encoded in JavaScript Object Notation. See https://www.json.org/.
32+
2933
### [Mixed](src/Mixed.php)
3034

3135
Loose type that allows any value. Be careful when passing in large `Int` or `Float` literals,

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"php": ">=7.1",
1515
"webonyx/graphql-php": "^0.13",
1616
"spatie/regex": "^1.3",
17-
"egulias/email-validator": "^2.1"
17+
"egulias/email-validator": "^2.1",
18+
"thecodingmachine/safe": "^0.1.14"
1819
},
1920
"require-dev": {
2021
"ext-json": "*",

src/JSON.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MLL\GraphQLScalars;
6+
7+
use Exception;
8+
use GraphQL\Error\Error;
9+
use GraphQL\Language\AST\Node;
10+
use GraphQL\Type\Definition\ScalarType;
11+
use GraphQL\Utils\Utils;
12+
use Safe\Exceptions\JsonException;
13+
14+
class JSON extends ScalarType
15+
{
16+
/**
17+
* The description that is used for schema introspection.
18+
*
19+
* @var string
20+
*/
21+
public $description = 'Arbitrary data encoded in JavaScript Object Notation. See https://www.json.org/.';
22+
23+
/**
24+
* Serializes an internal value to include in a response.
25+
*
26+
* @param mixed $value
27+
*
28+
* @return string
29+
*/
30+
public function serialize($value): string
31+
{
32+
return \Safe\json_encode($value);
33+
}
34+
35+
/**
36+
* Parses an externally provided value (query variable) to use as an input
37+
*
38+
* In the case of an invalid value this method must throw an Exception
39+
*
40+
* @param mixed $value
41+
*
42+
* @return mixed
43+
*
44+
* @throws Error
45+
*/
46+
public function parseValue($value)
47+
{
48+
return $this->decodeJSON($value);
49+
}
50+
51+
/**
52+
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
53+
*
54+
* In the case of an invalid node or value this method must throw an Exception
55+
*
56+
* @param Node $valueNode
57+
* @param mixed[]|null $variables
58+
*
59+
* @return mixed
60+
*
61+
* @throws Exception
62+
*/
63+
public function parseLiteral($valueNode, ?array $variables = null)
64+
{
65+
if(!property_exists($valueNode, 'value')){
66+
throw new Error(
67+
'Can only parse literals that contain a value, got ' . Utils::printSafeJson($valueNode)
68+
);
69+
}
70+
71+
return $this->decodeJSON($valueNode->value);
72+
}
73+
74+
/**
75+
* Try to decode a user-given value into JSON.
76+
*
77+
* @param mixed $value
78+
*
79+
* @return mixed
80+
*
81+
* @throws Error
82+
*/
83+
protected function decodeJSON($value)
84+
{
85+
try {
86+
$parsed = \Safe\json_decode($value);
87+
} catch (JsonException $jsonException) {
88+
throw new Error(
89+
$jsonException->getMessage()
90+
);
91+
}
92+
93+
return $parsed;
94+
}
95+
}

tests/JSONTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests;
6+
7+
use GraphQL\Error\Error;
8+
use GraphQL\Language\AST\StringValueNode;
9+
use MLL\GraphQLScalars\JSON;
10+
use PHPUnit\Framework\TestCase;
11+
use Safe\Exceptions\JsonException;
12+
13+
class JSONTest extends TestCase
14+
{
15+
const INVALID_UTF8_SEQUENCE = "\xB1\x31";
16+
17+
public function testSerializeThrowsIfNonEncodableValueIsGiven(): void
18+
{
19+
$this->expectException(JsonException::class);
20+
21+
(new JSON())->serialize(self::INVALID_UTF8_SEQUENCE);
22+
}
23+
24+
public function testSerializeThrowsIfJSONIsInvalid(): void
25+
{
26+
$this->expectException(JsonException::class);
27+
28+
(new JSON())->serialize(self::INVALID_UTF8_SEQUENCE);
29+
}
30+
31+
public function testSerializePassesWhenJSONIsValid(): void
32+
{
33+
$serializedResult = (new JSON())->serialize(1);
34+
35+
$this->assertSame('1', $serializedResult);
36+
}
37+
38+
public function testParseValueThrowsIfJSONIsInvalid(): void
39+
{
40+
$this->expectException(Error::class);
41+
42+
(new JSON())->parseValue('foo');
43+
}
44+
45+
public function testParseValuePassesIfJSONIsValid(): void
46+
{
47+
$this->assertSame(
48+
[1, 2],
49+
(new JSON())->parseValue('[1, 2]')
50+
);
51+
}
52+
53+
public function testParseLiteralThrowsIfNotValidJSON(): void
54+
{
55+
$this->expectException(Error::class);
56+
57+
(new JSON())->parseLiteral(new StringValueNode(['value' => 'foo']));
58+
}
59+
60+
public function testParseLiteralPassesIfJSONIsValid(): void
61+
{
62+
$this->assertSame(
63+
'bar',
64+
(new JSON())->parseLiteral(new StringValueNode(['value' => '{"foo": "bar"}']))->foo
65+
);
66+
}
67+
}

0 commit comments

Comments
 (0)