Skip to content

Commit 34eae0b

Browse files
committed
Schema validation + tests (#148)
1 parent d3580e9 commit 34eae0b

27 files changed

+4521
-1266
lines changed

docs/type-system/enum-types.md

-13
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,6 @@ $episodeEnum = new EnumType([
6464

6565
which is equivalent of:
6666
```php
67-
$episodeEnum = new EnumType([
68-
'name' => 'Episode',
69-
'description' => 'One of the films in the Star Wars Trilogy',
70-
'values' => [
71-
'NEWHOPE' => 'NEWHOPE',
72-
'EMPIRE' => 'EMPIRE',
73-
'JEDI' => 'JEDI'
74-
]
75-
]);
76-
```
77-
78-
which is in turn equivalent of:
79-
```php
8067
$episodeEnum = new EnumType([
8168
'name' => 'Episode',
8269
'description' => 'One of the films in the Star Wars Trilogy',

src/Error/Warning.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ final class Warning
1414

1515
static $warned = [];
1616

17+
static private $warningHandler;
18+
19+
public static function setWarningHandler(callable $warningHandler = null)
20+
{
21+
self::$warningHandler = $warningHandler;
22+
}
23+
1724
static function suppress($suppress = true)
1825
{
1926
if (true === $suppress) {
@@ -40,15 +47,21 @@ public static function enable($enable = true)
4047

4148
static function warnOnce($errorMessage, $warningId)
4249
{
43-
if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
50+
if (self::$warningHandler) {
51+
$fn = self::$warningHandler;
52+
$fn($errorMessage, $warningId);
53+
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
4454
self::$warned[$warningId] = true;
4555
trigger_error($errorMessage, E_USER_WARNING);
4656
}
4757
}
4858

4959
static function warn($errorMessage, $warningId)
5060
{
51-
if ((self::$enableWarnings & $warningId) > 0) {
61+
if (self::$warningHandler) {
62+
$fn = self::$warningHandler;
63+
$fn($errorMessage, $warningId);
64+
} else if ((self::$enableWarnings & $warningId) > 0) {
5265
trigger_error($errorMessage, E_USER_WARNING);
5366
}
5467
}

src/Type/Definition/CustomScalarType.php

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?php
22
namespace GraphQL\Type\Definition;
33

4+
use GraphQL\Utils\Utils;
5+
46
/**
57
* Class CustomScalarType
68
* @package GraphQL\Type\Definition
@@ -38,7 +40,11 @@ public function serialize($value)
3840
*/
3941
public function parseValue($value)
4042
{
41-
return call_user_func($this->config['parseValue'], $value);
43+
if (isset($this->config['parseValue'])) {
44+
return call_user_func($this->config['parseValue'], $value);
45+
} else {
46+
return null;
47+
}
4248
}
4349

4450
/**
@@ -47,6 +53,29 @@ public function parseValue($value)
4753
*/
4854
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
4955
{
50-
return call_user_func($this->config['parseLiteral'], $valueNode);
56+
if (isset($this->config['parseLiteral'])) {
57+
return call_user_func($this->config['parseLiteral'], $valueNode);
58+
} else {
59+
return null;
60+
}
61+
}
62+
63+
public function assertValid()
64+
{
65+
parent::assertValid();
66+
67+
Utils::invariant(
68+
isset($this->config['serialize']) && is_callable($this->config['serialize']),
69+
"{$this->name} must provide \"serialize\" function. If this custom Scalar " .
70+
'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
71+
'functions are also provided.'
72+
);
73+
if (isset($this->config['parseValue']) || isset($this->config['parseLiteral'])) {
74+
Utils::invariant(
75+
isset($this->config['parseValue']) && isset($this->config['parseLiteral']) &&
76+
is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']),
77+
"{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions."
78+
);
79+
}
5180
}
5281
}

src/Type/Definition/EnumType.php

+70-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace GraphQL\Type\Definition;
33

4+
use GraphQL\Error\InvariantViolation;
45
use GraphQL\Language\AST\EnumValueNode;
56
use GraphQL\Utils\MixedStore;
67
use GraphQL\Utils\Utils;
@@ -26,6 +27,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType
2627
*/
2728
private $nameLookup;
2829

30+
/**
31+
* @var array
32+
*/
33+
public $config;
34+
2935
public function __construct($config)
3036
{
3137
if (!isset($config['name'])) {
@@ -47,28 +53,41 @@ public function __construct($config)
4753

4854
$this->name = $config['name'];
4955
$this->description = isset($config['description']) ? $config['description'] : null;
50-
$this->values = [];
56+
$this->config = $config;
57+
}
58+
59+
/**
60+
* @return EnumValueDefinition[]
61+
*/
62+
public function getValues()
63+
{
64+
if ($this->values === null) {
65+
$this->values = [];
66+
$config = $this->config;
5167

52-
if (!empty($config['values'])) {
53-
foreach ($config['values'] as $name => $value) {
54-
if (!is_array($value)) {
68+
if (isset($config['values'])) {
69+
if (!is_array($config['values'])) {
70+
throw new InvariantViolation("{$this->name} values must be an array");
71+
}
72+
foreach ($config['values'] as $name => $value) {
5573
if (is_string($name)) {
56-
$value = ['name' => $name, 'value' => $value];
74+
if (!is_array($value)) {
75+
throw new InvariantViolation(
76+
"{$this->name}.$name must refer to an associative array with a " .
77+
'"value" key representing an internal value but got: ' . Utils::printSafe($value)
78+
);
79+
}
80+
$value += ['name' => $name, 'value' => $name];
5781
} else if (is_int($name) && is_string($value)) {
5882
$value = ['name' => $value, 'value' => $value];
83+
} else {
84+
throw new InvariantViolation("{$this->name} values must be an array with value names as keys.");
5985
}
86+
$this->values[] = new EnumValueDefinition($value);
6087
}
61-
// value will be equal to name only if 'value' is not set in definition
62-
$this->values[] = new EnumValueDefinition($value + ['name' => $name, 'value' => $name]);
6388
}
6489
}
65-
}
6690

67-
/**
68-
* @return EnumValueDefinition[]
69-
*/
70-
public function getValues()
71-
{
7291
return $this->values;
7392
}
7493

@@ -168,4 +187,42 @@ private function getNameLookup()
168187
}
169188
return $this->nameLookup;
170189
}
190+
191+
/**
192+
* @throws InvariantViolation
193+
*/
194+
public function assertValid()
195+
{
196+
parent::assertValid();
197+
198+
Utils::invariant(
199+
isset($this->config['values']),
200+
"{$this->name} values must be an array."
201+
);
202+
203+
$values = $this->getValues();
204+
205+
Utils::invariant(
206+
!empty($values),
207+
"{$this->name} values must be not empty."
208+
);
209+
foreach ($values as $value) {
210+
try {
211+
Utils::assertValidName($value->name);
212+
} catch (InvariantViolation $e) {
213+
throw new InvariantViolation(
214+
"{$this->name} has value with invalid name: " .
215+
Utils::printSafe($value->name) . " ({$e->getMessage()})"
216+
);
217+
}
218+
Utils::invariant(
219+
!in_array($value->name, ['true', 'false', 'null']),
220+
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
221+
);
222+
Utils::invariant(
223+
!isset($value->config['isDeprecated']),
224+
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
225+
);
226+
}
227+
}
171228
}

src/Type/Definition/EnumValueDefinition.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,19 @@ class EnumValueDefinition
2828
*/
2929
public $description;
3030

31+
/**
32+
* @var array
33+
*/
34+
public $config;
35+
3136
public function __construct(array $config)
3237
{
33-
Utils::assign($this, $config);
38+
$this->name = isset($config['name']) ? $config['name'] : null;
39+
$this->value = isset($config['value']) ? $config['value'] : null;
40+
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
41+
$this->description = isset($config['description']) ? $config['description'] : null;
42+
43+
$this->config = $config;
3444
}
3545

3646
/**

src/Type/Definition/FieldArgument.php

+28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?php
22
namespace GraphQL\Type\Definition;
33

4+
use GraphQL\Error\InvariantViolation;
5+
use GraphQL\Utils\Utils;
6+
47

58
/**
69
* Class FieldArgument
@@ -105,4 +108,29 @@ public function defaultValueExists()
105108
{
106109
return $this->defaultValueExists;
107110
}
111+
112+
public function assertValid(FieldDefinition $parentField, Type $parentType)
113+
{
114+
try {
115+
Utils::assertValidName($this->name);
116+
} catch (InvariantViolation $e) {
117+
throw new InvariantViolation(
118+
"{$parentType->name}.{$parentField->name}({$this->name}:) {$e->getMessage()}")
119+
;
120+
}
121+
$type = $this->type;
122+
if ($type instanceof WrappingType) {
123+
$type = $type->getWrappedType(true);
124+
}
125+
Utils::invariant(
126+
$type instanceof InputType,
127+
"{$parentType->name}.{$parentField->name}({$this->name}): argument type must be " .
128+
"Input Type but got: " . Utils::printSafe($this->type)
129+
);
130+
Utils::invariant(
131+
$this->description === null || is_string($this->description),
132+
"{$parentType->name}.{$parentField->name}({$this->name}): argument description type must be " .
133+
"string but got: " . Utils::printSafe($this->description)
134+
);
135+
}
108136
}

0 commit comments

Comments
 (0)