Skip to content

Commit e351749

Browse files
committed
feat: Add support for specifiedBy directive
1 parent 3bc2778 commit e351749

16 files changed

+132
-26
lines changed

src/Language/Printer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,8 @@ protected function p(?Node $node): string
414414
return BlockString::print($node->value);
415415
}
416416

417-
return \json_encode($node->value, JSON_THROW_ON_ERROR);
417+
// Do not escape unicode or slashes in order to keep urls valid
418+
return \json_encode($node->value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
418419

419420
case $node instanceof UnionTypeDefinitionNode:
420421
$typesStr = $this->printList($node->types, ' | ');

src/Type/Definition/CustomScalarType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* serialize?: callable(mixed): mixed,
1919
* parseValue: callable(mixed): mixed,
2020
* parseLiteral: callable(ValueNode&Node, array<string, mixed>|null): mixed,
21+
* specifiedByURL?: string|null,
2122
* astNode?: ScalarTypeDefinitionNode|null,
2223
* extensionASTNodes?: array<ScalarTypeExtensionNode>|null
2324
* }
@@ -27,6 +28,7 @@
2728
* serialize: callable(mixed): mixed,
2829
* parseValue?: callable(mixed): mixed,
2930
* parseLiteral?: callable(ValueNode&Node, array<string, mixed>|null): mixed,
31+
* specifiedByURL?: string|null,
3032
* astNode?: ScalarTypeDefinitionNode|null,
3133
* extensionASTNodes?: array<ScalarTypeExtensionNode>|null
3234
* }

src/Type/Definition/Directive.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class Directive
2626
public const IF_ARGUMENT_NAME = 'if';
2727
public const SKIP_NAME = 'skip';
2828
public const DEPRECATED_NAME = 'deprecated';
29+
public const SPECIFIED_BY_NAME = 'specifiedBy';
2930
public const REASON_ARGUMENT_NAME = 'reason';
31+
public const URL_ARGUMENT_NAME = 'url';
3032

3133
/**
3234
* Lazily initialized.
@@ -84,9 +86,9 @@ public static function includeDirective(): Directive
8486
}
8587

8688
/**
89+
* @return array<string, Directive>
8790
* @throws InvariantViolation
8891
*
89-
* @return array<string, Directive>
9092
*/
9193
public static function getInternalDirectives(): array
9294
{
@@ -138,6 +140,19 @@ public static function getInternalDirectives(): array
138140
],
139141
],
140142
]),
143+
'specifiedBy' => new self([
144+
'name' => self::SPECIFIED_BY_NAME,
145+
'description' => 'Exposes a URL that specifies the behaviour of this scalar.',
146+
'locations' => [
147+
DirectiveLocation::SCALAR,
148+
],
149+
'args' => [
150+
self::URL_ARGUMENT_NAME => [
151+
'type' => Type::nonNull(Type::string()),
152+
'description' => 'The URL that specifies the behaviour of this scalar and points to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.',
153+
],
154+
],
155+
]),
141156
];
142157
}
143158

@@ -157,6 +172,14 @@ public static function deprecatedDirective(): Directive
157172
return $internal['deprecated'];
158173
}
159174

175+
/** @throws InvariantViolation */
176+
public static function specifiedByDirective(): Directive
177+
{
178+
$internal = self::getInternalDirectives();
179+
180+
return $internal['specifiedBy'];
181+
}
182+
160183
/** @throws InvariantViolation */
161184
public static function isSpecifiedDirective(Directive $directive): bool
162185
{

src/Type/Definition/ScalarType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* @phpstan-type ScalarConfig array{
2929
* name?: string|null,
3030
* description?: string|null,
31+
* specifiedByURL?: string|null,
3132
* astNode?: ScalarTypeDefinitionNode|null,
3233
* extensionASTNodes?: array<ScalarTypeExtensionNode>|null
3334
* }
@@ -37,6 +38,7 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
3738
use NamedTypeImplementation;
3839

3940
public ?ScalarTypeDefinitionNode $astNode;
41+
public ?string $specifiedByURL;
4042

4143
/** @var array<ScalarTypeExtensionNode> */
4244
public array $extensionASTNodes;
@@ -55,6 +57,7 @@ public function __construct(array $config = [])
5557
$this->description = $config['description'] ?? $this->description ?? null;
5658
$this->astNode = $config['astNode'] ?? null;
5759
$this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
60+
$this->specifiedByURL = $config['specifiedByURL'] ?? null;
5861

5962
$this->config = $config;
6063
}

src/Utils/ASTDefinitionBuilder.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,6 @@ public function buildField(FieldDefinitionNode $field): array
392392
* @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node
393393
*
394394
* @throws \Exception
395-
* @throws \ReflectionException
396395
* @throws InvariantViolation
397396
*/
398397
private function getDeprecationReason(Node $node): ?string
@@ -405,6 +404,25 @@ private function getDeprecationReason(Node $node): ?string
405404
return $deprecated['reason'] ?? null;
406405
}
407406

407+
/**
408+
* Given a collection of directives, returns the string value for the
409+
* specifiedBy url.
410+
*
411+
* @param ScalarTypeDefinitionNode $node
412+
*
413+
* @throws \Exception
414+
* @throws InvariantViolation
415+
*/
416+
private function getSpecifiedByURL(Node $node): ?string
417+
{
418+
$specifiedBy = Values::getDirectiveValues(
419+
Directive::specifiedByDirective(),
420+
$node
421+
);
422+
423+
return $specifiedBy['url'] ?? null;
424+
}
425+
408426
/**
409427
* @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes
410428
*
@@ -509,7 +527,10 @@ private function makeUnionDef(UnionTypeDefinitionNode $def): UnionType
509527
]);
510528
}
511529

512-
/** @throws InvariantViolation */
530+
/**
531+
* @throws InvariantViolation
532+
* @throws \Exception
533+
*/
513534
private function makeScalarDef(ScalarTypeDefinitionNode $def): CustomScalarType
514535
{
515536
$name = $def->name->value;
@@ -522,6 +543,7 @@ private function makeScalarDef(ScalarTypeDefinitionNode $def): CustomScalarType
522543
'astNode' => $def,
523544
'extensionASTNodes' => $extensionASTNodes,
524545
'serialize' => static fn ($value) => $value,
546+
'specifiedByURL' => $this->getSpecifiedByURL($def),
525547
]);
526548
}
527549

src/Utils/BuildSchema.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ static function (string $typeName): Type {
223223
if (! isset($directivesByName['deprecated'])) {
224224
$directives[] = Directive::deprecatedDirective();
225225
}
226+
if (! isset($directivesByName['specifiedBy'])) {
227+
$directives[] = Directive::specifiedByDirective();
228+
}
226229

227230
// Note: While this could make early assertions to get the correctly
228231
// typed values below, that would throw immediately while type system

src/Utils/SchemaExtender.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ protected function extendScalarType(ScalarType $type): CustomScalarType
228228
'serialize' => [$type, 'serialize'],
229229
'parseValue' => [$type, 'parseValue'],
230230
'parseLiteral' => [$type, 'parseLiteral'],
231+
'specifiedByURL' => $type->specifiedByURL,
231232
'astNode' => $type->astNode,
232233
'extensionASTNodes' => $extensionASTNodes,
233234
]);

src/Utils/SchemaPrinter.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,14 @@ protected static function printInputValue($arg): string
361361
* @phpstan-param Options $options
362362
*
363363
* @throws \JsonException
364+
* @throws InvariantViolation
365+
* @throws SerializationError
364366
*/
365367
protected static function printScalar(ScalarType $type, array $options): string
366368
{
367369
return static::printDescription($options, $type)
368-
. "scalar {$type->name}";
370+
. "scalar {$type->name}"
371+
. static::printSpecifiedBy($type);
369372
}
370373

371374
/**
@@ -452,6 +455,28 @@ protected static function printDeprecated($deprecation): string
452455
return " @deprecated(reason: {$reasonASTString})";
453456
}
454457

458+
/**
459+
* @param ScalarType $type
460+
*
461+
* @throws \JsonException
462+
* @throws InvariantViolation
463+
* @throws SerializationError
464+
*/
465+
protected static function printSpecifiedBy(ScalarType $type): string
466+
{
467+
$url = $type->specifiedByURL;
468+
if ($url === null) {
469+
return '';
470+
}
471+
472+
$urlAST = AST::astFromValue($url, Type::string());
473+
assert($urlAST instanceof StringValueNode);
474+
475+
$urlASTString = Printer::doPrint($urlAST);
476+
477+
return " @specifiedBy(url: {$urlASTString})";
478+
}
479+
455480
protected static function printImplementedInterfaces(ImplementingType $type): string
456481
{
457482
$interfaces = $type->getInterfaces();

src/Validator/Rules/QueryComplexity.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ protected function directiveExcludesField(FieldNode $node): bool
173173
return false;
174174
}
175175

176+
if ($directiveNode->name->value === Directive::SPECIFIED_BY_NAME) {
177+
return false;
178+
}
179+
176180
[$errors, $variableValues] = Values::getVariableValues(
177181
$this->context->getSchema(),
178182
$this->variableDefs,

tests/Type/IntrospectionTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,30 @@ public function testExecutesAnIntrospectionQuery(): void
962962
3 => 'INPUT_FIELD_DEFINITION',
963963
],
964964
],
965+
[
966+
'name' => 'specifiedBy',
967+
'args' => [
968+
0 => [
969+
'name' => 'url',
970+
'type' => [
971+
'kind' => 'NON_NULL',
972+
'name' => null,
973+
'ofType' => [
974+
'kind' => 'SCALAR',
975+
'name' => 'String',
976+
'ofType' => null,
977+
],
978+
],
979+
'defaultValue' => null,
980+
'isDeprecated' => false,
981+
'deprecationReason' => null,
982+
],
983+
],
984+
'isRepeatable' => false,
985+
'locations' => [
986+
0 => 'SCALAR',
987+
],
988+
],
965989
],
966990
],
967991
],

0 commit comments

Comments
 (0)