From 63d308691194f8b615df5a2cca5f505daa6f18bf Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Thu, 23 Sep 2021 18:33:43 -0700 Subject: [PATCH 01/11] wip --- src/Codegen/Builders/Fields/FieldBuilder.hack | 4 ++ src/Codegen/Generator.hack | 1 + src/Directive.hack | 5 ++ tests/Fixtures/Directives.hack | 5 ++ tests/Fixtures/Playground.hack | 46 ++++++++++++++++++- tests/gen/Bot.hack | 10 +++- tests/gen/Human.hack | 10 +++- tests/gen/Role.hack | 29 ++++++++++++ tests/gen/Schema.hack | 3 +- tests/gen/User.hack | 10 +++- 10 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/Directive.hack create mode 100644 tests/Fixtures/Directives.hack create mode 100644 tests/gen/Role.hack diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index 7a3ca5e..f8ce9a5 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -33,6 +33,10 @@ abstract class FieldBuilder { \ReflectionMethod $rm, bool $is_root_field = false, ): FieldBuilder { + if ($rm->getName() === 'getFavoriteColor') { + $directives = $rm->getAttributes(); + \var_dump($directives); + } $data = shape( 'name' => $field->getName(), 'method_name' => $rm->getName(), diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index f2074c1..8c6d188 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -31,6 +31,7 @@ final class Generator { 'output_directory' => string, 'namespace' => string, ?'codegen_config' => IHackCodegenConfig, + ?'custom_directives' => vec> ); private function __construct(private MultiParser $parser, private self::TGeneratorConfig $config) { diff --git a/src/Directive.hack b/src/Directive.hack new file mode 100644 index 0000000..bf877d3 --- /dev/null +++ b/src/Directive.hack @@ -0,0 +1,5 @@ +namespace Slack\GraphQL; + +interface Directive {} + +interface FieldDirective extends Directive, \HH\MethodAttribute {} \ No newline at end of file diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack new file mode 100644 index 0000000..c7d2645 --- /dev/null +++ b/tests/Fixtures/Directives.hack @@ -0,0 +1,5 @@ +namespace Directives; + +final class HasRole implements \Slack\GraphQL\FieldDirective { + public function __construct(private vec $roles) {} +} \ No newline at end of file diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 531fffc..8b9974a 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -2,7 +2,7 @@ use namespace Slack\GraphQL; -use namespace HH\Lib\{Math, Str, Vec}; +use namespace HH\Lib\{C, Math, Str, Vec}; <> type TCreateTeamInput = shape( @@ -59,6 +59,15 @@ interface User { <> public function isActive(): bool; + + <> + public function getRoles(): vec; +} + +<> +enum Role: string { + ADMIN = 'ADMIN'; + STAFF = 'STAFF'; } abstract class BaseUser implements User { @@ -68,6 +77,7 @@ abstract class BaseUser implements User { 'name' => string, 'team_id' => int, 'is_active' => bool, + ?'roles' => vec ) $data, ) {} @@ -86,6 +96,10 @@ abstract class BaseUser implements User { public function isActive(): bool { return $this->data['is_active']; } + + public function getRoles(): vec { + return $this->data['roles'] ?? vec[]; + } } <> @@ -94,9 +108,37 @@ enum FavoriteColor: int { BLUE = 2; } +final class PermissionService { + private static ?PermissionService $service = null; + + public static function getInstance(): PermissionService { + if (static::$service is null) { + static::$service = new self(); + } + return static::$service as nonnull; + } + + private User $user; + + public function __construct(?User $user = null) { + $this->user = new Human(shape('id' => 1, 'name' => 'foo', 'team_id' => 1, 'is_active' => true)); + } + + public function getCurrentUser(): User { + return $this->user; + } + + public function setCurrentUser(User $user): void { + $this->user = $user; + } +} + <> final class Human extends BaseUser { - <> + << + GraphQL\Field('favorite_color', 'Favorite color of the user'), + Directives\HasRole(vec['STAFF']) + >> public function getFavoriteColor(): FavoriteColor { return FavoriteColor::BLUE; } diff --git a/tests/gen/Bot.hack b/tests/gen/Bot.hack index aee6b1a..26496e2 100644 --- a/tests/gen/Bot.hack +++ b/tests/gen/Bot.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<803c718476e0461c3e9d70a1db853f34>> + * @generated SignedSource<<3b315c322b594d0d3e3317ae5e0f799e>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -20,6 +20,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { 'is_active', 'name', 'primary_function', + 'roles', 'team', ]; const dict> INTERFACES = dict[ @@ -58,6 +59,13 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { dict[], async ($parent, $args, $vars) ==> $parent->getPrimaryFunction(), ); + case 'roles': + return new GraphQL\FieldDefinition( + 'roles', + Role::nonNullable()->nullableOutputListOf(), + dict[], + async ($parent, $args, $vars) ==> $parent->getRoles(), + ); case 'team': return new GraphQL\FieldDefinition( 'team', diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index e2a2d27..e3bbc52 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<453bfb5200271c708353fecfc025619b>> + * @generated SignedSource<<74cee04a80f9d7fcba46fd5b48e66987>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -22,6 +22,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { 'is_active', 'name', 'named_friends', + 'roles', 'team', ]; const dict> INTERFACES = dict[ @@ -132,6 +133,13 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableInput()->coerceOptionalNamedNode('last', $args, $vars, null), ), ); + case 'roles': + return new GraphQL\FieldDefinition( + 'roles', + Role::nonNullable()->nullableOutputListOf(), + dict[], + async ($parent, $args, $vars) ==> $parent->getRoles(), + ); case 'team': return new GraphQL\FieldDefinition( 'team', diff --git a/tests/gen/Role.hack b/tests/gen/Role.hack new file mode 100644 index 0000000..3bbd6c8 --- /dev/null +++ b/tests/gen/Role.hack @@ -0,0 +1,29 @@ +/** + * This file is generated. Do not modify it manually! + * + * To re-generate this file run vendor/bin/hacktest + * + * + * @generated SignedSource<<78baf0213f0d9e14e803297f781e40f4>> + */ +namespace Slack\GraphQL\Test\Generated; +use namespace Slack\GraphQL; +use namespace Slack\GraphQL\Types; +use namespace HH\Lib\{C, Dict}; + +final class Role extends \Slack\GraphQL\Types\EnumType { + + const NAME = 'Role'; + const type THackType = \Role; + const \HH\enumname HACK_ENUM = \Role::class; + const vec ENUM_VALUES = vec[ + shape( + 'name' => 'ADMIN', + 'isDeprecated' => false, + ), + shape( + 'name' => 'STAFF', + 'isDeprecated' => false, + ), + ]; +} diff --git a/tests/gen/Schema.hack b/tests/gen/Schema.hack index 24ca744..b7ff1d0 100644 --- a/tests/gen/Schema.hack +++ b/tests/gen/Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<024a37e7259d3742ec2b1d7c931d239b>> + * @generated SignedSource<<1580f1b40c1aff38c6937d089b8198b0>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,6 +46,7 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'OutputTypeTestObj' => OutputTypeTestObj::class, 'PageInfo' => PageInfo::class, 'Query' => Query::class, + 'Role' => Role::class, 'String' => Types\StringType::class, 'StringTypeEdge' => StringTypeEdge::class, 'Team' => Team::class, diff --git a/tests/gen/User.hack b/tests/gen/User.hack index 77678fb..91c0eb1 100644 --- a/tests/gen/User.hack +++ b/tests/gen/User.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<481a5396998f1863b06a6992d837fe3b>> + * @generated SignedSource<<47c161bdd9510702ccbfb76106d4bc21>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -19,6 +19,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { 'id', 'is_active', 'name', + 'roles', 'team', ]; const keyset> POSSIBLE_TYPES = keyset[ @@ -51,6 +52,13 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { dict[], async ($parent, $args, $vars) ==> $parent->getName(), ); + case 'roles': + return new GraphQL\FieldDefinition( + 'roles', + Role::nonNullable()->nullableOutputListOf(), + dict[], + async ($parent, $args, $vars) ==> $parent->getRoles(), + ); case 'team': return new GraphQL\FieldDefinition( 'team', From 28c0633fa478fb280f080e2cee15a3e07c32c4a1 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Thu, 23 Sep 2021 23:52:23 -0700 Subject: [PATCH 02/11] get something working --- src/Codegen/Builders/Fields/FieldBuilder.hack | 22 ++++- .../IntrospectionSchemaFieldBuilder.hack | 2 + .../Fields/IntrospectionTypeFieldBuilder.hack | 1 + .../Builders/Fields/MethodFieldBuilder.hack | 1 + .../Builders/Fields/ShapeFieldBuilder.hack | 1 + src/Codegen/Builders/ObjectBuilder.hack | 4 + src/Codegen/DirectivesFinder.hack | 94 +++++++++++++++++++ src/Codegen/FieldResolver.hack | 18 ++-- src/Codegen/Generator.hack | 24 +++-- src/Directive.hack | 4 +- src/FieldDefinition.hack | 4 + tests/Fixtures/Directives.hack | 17 ++++ tests/Fixtures/Playground.hack | 7 +- tests/gen/AlphabetConnection.hack | 4 +- tests/gen/AnotherObjectShape.hack | 3 +- tests/gen/Bot.hack | 8 +- tests/gen/Concrete.hack | 5 +- tests/gen/ErrorTestObj.hack | 14 ++- tests/gen/Human.hack | 19 +++- tests/gen/InterfaceA.hack | 3 +- tests/gen/InterfaceB.hack | 4 +- tests/gen/IntrospectionTestObject.hack | 9 +- tests/gen/Mutation.hack | 4 +- tests/gen/NestedOutputShape.hack | 3 +- tests/gen/ObjectShape.hack | 5 +- tests/gen/OutputShape.hack | 5 +- tests/gen/OutputTypeTestObj.hack | 10 +- tests/gen/PageInfo.hack | 6 +- tests/gen/Query.hack | 22 ++++- tests/gen/StringTypeEdge.hack | 4 +- tests/gen/Team.hack | 6 +- tests/gen/User.hack | 7 +- tests/gen/UserConnection.hack | 4 +- tests/gen/UserEdge.hack | 4 +- tests/gen/__Directive.hack | 6 +- tests/gen/__EnumValue.hack | 6 +- tests/gen/__Field.hack | 8 +- tests/gen/__InputValue.hack | 6 +- tests/gen/__Schema.hack | 7 +- tests/gen/__Type.hack | 11 ++- 40 files changed, 343 insertions(+), 49 deletions(-) create mode 100644 src/Codegen/DirectivesFinder.hack diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index f8ce9a5..2fe9b32 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -14,6 +14,7 @@ abstract class FieldBuilder { abstract const type TField as shape( 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), + 'directives' => vec, ... ); @@ -31,12 +32,9 @@ abstract class FieldBuilder { public static function fromReflectionMethod( \Slack\GraphQL\Field $field, \ReflectionMethod $rm, + vec $directives, bool $is_root_field = false, ): FieldBuilder { - if ($rm->getName() === 'getFavoriteColor') { - $directives = $rm->getAttributes(); - \var_dump($directives); - } $data = shape( 'name' => $field->getName(), 'method_name' => $rm->getName(), @@ -58,6 +56,7 @@ abstract class FieldBuilder { return $data; }, ), + 'directives' => $directives ); if ($is_root_field) { @@ -79,6 +78,7 @@ abstract class FieldBuilder { 'name' => $name, 'output_type' => output_type(type_structure_to_type_alias($ts), false), 'is_optional' => Shapes::idx($ts, 'optional_shape_field') ?? false, + 'directives' => vec[], )); } @@ -86,7 +86,7 @@ abstract class FieldBuilder { * Construct a top-level GraphQL field. */ public static function forRootField(\Slack\GraphQL\Field $field, \ReflectionMethod $rm): FieldBuilder { - return FieldBuilder::fromReflectionMethod($field, $rm, true); + return FieldBuilder::fromReflectionMethod($field, $rm, vec[], true); } public static function introspectSchemaField(): FieldBuilder { @@ -130,6 +130,18 @@ abstract class FieldBuilder { $this->generateResolverBody($hb); $hb->addLine(','); + // Field directives + if ($this->data['directives']) { + $hb->addLine('vec[') + ->indent(); + foreach ($this->data['directives'] as $directive) { + $hb->addLine($directive.','); + } + $hb->unindent()->addLine('],'); + } else { + $hb->addLine('vec[],'); + } + // End of new GraphQL\FieldDefinition( $hb->unindent()->addLine(');'); $hb->unindent(); diff --git a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack index 9d93f49..2ed697f 100644 --- a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack @@ -8,6 +8,7 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder { const type TField = shape( 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), + 'directives' => vec, ... ); @@ -15,6 +16,7 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder { parent::__construct(shape( 'name' => '__schema', 'output_type' => shape('type' => '__Schema::nullableOutput()'), + 'directives' => vec[], )); } diff --git a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack index d10ac4c..b8da570 100644 --- a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack @@ -15,6 +15,7 @@ final class IntrospectTypeFieldBuilder extends MethodFieldBuilder { 'parameters' => vec[ shape('name' => 'name', 'type' => 'HH\string', 'is_optional' => false), ], + 'directives' => vec[], )); } <<__Override>> diff --git a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack index a0289cf..8ea97f4 100644 --- a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack @@ -33,6 +33,7 @@ class MethodFieldBuilder extends FieldBuilder { 'method_name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), 'parameters' => vec, + 'directives' => vec, ?'root_field_for_type' => string, ?'is_static' => bool, ); diff --git a/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack b/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack index 6b1b37d..38bfae3 100644 --- a/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack @@ -16,6 +16,7 @@ final class ShapeFieldBuilder extends FieldBuilder { 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), 'is_optional' => bool, + 'directives' => vec, ); <<__Override>> diff --git a/src/Codegen/Builders/ObjectBuilder.hack b/src/Codegen/Builders/ObjectBuilder.hack index 413e662..3d226ff 100644 --- a/src/Codegen/Builders/ObjectBuilder.hack +++ b/src/Codegen/Builders/ObjectBuilder.hack @@ -75,12 +75,14 @@ class ObjectBuilder extends CompositeBuilder { 'needs_await' => true, ), 'parameters' => vec[], + 'directives' => vec[], )), new MethodFieldBuilder(shape( 'name' => 'pageInfo', 'method_name' => 'getPageInfo', 'output_type' => shape('type' => 'PageInfo::nullableOutput()', 'needs_await' => true), 'parameters' => vec[], + 'directives' => vec[], )), ], dict[], // Connections do not implement any interfaces @@ -99,12 +101,14 @@ class ObjectBuilder extends CompositeBuilder { 'method_name' => 'getNode', 'output_type' => shape('type' => $output_type.'::nullableOutput()'), 'parameters' => vec[], + 'directives' => vec[], )), new MethodFieldBuilder(shape( 'name' => 'cursor', 'method_name' => 'getCursor', 'output_type' => shape('type' => 'Types\StringType::nullableOutput()'), 'parameters' => vec[], + 'directives' => vec[], )), ], dict[], diff --git a/src/Codegen/DirectivesFinder.hack b/src/Codegen/DirectivesFinder.hack new file mode 100644 index 0000000..70a723a --- /dev/null +++ b/src/Codegen/DirectivesFinder.hack @@ -0,0 +1,94 @@ +namespace Slack\GraphQL\Codegen; + +use namespace HH\Lib\{C, Str, Vec}; +use type Facebook\DefinitionFinder\ScannedMethod; + +final class DirectivesFinder { + public function __construct(private keyset $directives) {} + + public async function findDirectivesForField(ScannedMethod $method): Awaitable> { + $directives = vec[]; + + // Get all use namespaces and types present + $file_path = $method->getFileName(); + $use_decls = await self::getUseDeclarations($file_path); + + // For each attribute, see if it matches a user-defined directive + // This requires building the qualified name, which is not + // available in `getAttributes`, unfortunately. + $attributes = $method->getAttributes(); + foreach ($attributes as $attribute => $args) { + if (Str\starts_with($attribute, '\\')) { + $qualified_name = $attribute; + } else { + $namespace = Str\split($attribute, '\\') + |> Vec\take($$, C\count($$) - 1) + |> Str\join($$, '\\'); + // If it's a type, not a namespace, then there's nothing to split + // so just use the whole string. + if (!$namespace) { + $namespace = $attribute; + } + $qualified_name = ''; + foreach ($use_decls as $decl) { + if (C\contains_key($decl['clauses'], $namespace)) { + $qualified_name = '\\'.$decl['root'].'\\'.$attribute; + break; + } + } + } + if (C\contains_key($this->directives, $qualified_name)) { + $directives[] = Str\format( + 'new %s(%s)', + $qualified_name, + Vec\map($args, $arg ==> \var_export($arg, true) + |> Str\replace($$, 'varray', 'vec') + |> Str\replace($$, 'darray', 'dict') + |> Str\replace($$, 'array', 'shape') + ) + |> Str\join($$, ', ') + ); + } + } + + return $directives; + } + + <<__Memoize>> + private static async function getUseDeclarations(string $file_path): Awaitable string, + 'clauses' => keyset + )>> { + $ast = await \Facebook\HHAST\from_file_async(\Facebook\HHAST\File::fromPath($file_path)); + return $ast->getDescendantsByType<\Facebook\HHAST\INamespaceUseDeclaration>() + |> Vec\map($$, $decl ==> { + $kind = $decl->getKind(); + if ($kind is null || $kind->getText() === 'function') { + return null; + } + $clauses = $decl->getClausesx()->toVec(); + if ($decl is \Facebook\HHAST\NamespaceGroupUseDeclaration) { + $root = $decl->getPrefixx()->getCode() + |> Str\strip_suffix($$, '\\'); + $clauses = Vec\map($clauses, $clause ==> $clause->getFirstToken()?->getCode()) + |> Vec\filter_nulls($$); + } else { + $clauses = C\firstx($clauses) + |> Str\split($$->getCode(), '\\'); + $num_clauses = C\count($clauses); + if ($num_clauses > 1) { + $root = Vec\take($clauses, $num_clauses - 1) + |> Str\join($$, '\\'); + $clauses = Vec\drop($clauses, $num_clauses - 1); + } else { + $root = ''; + } + } + return shape( + 'root' => $root, + 'clauses' => keyset($clauses), + ); + }) + |> Vec\filter_nulls($$); + } +} \ No newline at end of file diff --git a/src/Codegen/FieldResolver.hack b/src/Codegen/FieldResolver.hack index c6b5ca1..36fe3dc 100644 --- a/src/Codegen/FieldResolver.hack +++ b/src/Codegen/FieldResolver.hack @@ -14,14 +14,14 @@ final class FieldResolver { private dict $scanned_classes; private dict> $resolved_fields = dict[]; - public function __construct(vec $classes) { + public function __construct(vec $classes, private DirectivesFinder $directives_finder) { $this->scanned_classes = Dict\from_values($classes, $class ==> $class->getName()); } - public function resolveFields(): dict> { + public async function resolveFields(): Awaitable>> { foreach ($this->scanned_classes as $class) { if (!$this->shouldResolve($class)) continue; - $this->resolveClass($class); + await $this->resolveClass($class); } return Dict\map($this->resolved_fields, $fields ==> vec(Dict\sort_by_key($fields))); } @@ -34,7 +34,7 @@ final class FieldResolver { ) is nonnull; } - private function resolveClass(DefinitionFinder\ScannedClassish $class): dict { + private async function resolveClass(DefinitionFinder\ScannedClassish $class): Awaitable> { if (C\contains_key($this->resolved_fields, $class->getName())) { return $this->resolved_fields[$class->getName()]; } @@ -51,16 +51,16 @@ final class FieldResolver { foreach ($parents as $parent) { $parent_class = $this->scanned_classes[$parent] ?? null; if ($parent_class) { - $fields = Dict\merge($fields, $this->resolveClass($parent_class)); + $fields = Dict\merge($fields, await $this->resolveClass($parent_class)); } } - $fields = Dict\merge($fields, $this->collectObjectFields($class)); + $fields = Dict\merge($fields, await $this->collectObjectFields($class)); $this->resolved_fields[$class->getName()] = $fields; return $fields; } - private function collectObjectFields(DefinitionFinder\ScannedClassish $class): dict { + private async function collectObjectFields(DefinitionFinder\ScannedClassish $class): Awaitable> { $fields = dict[]; foreach ($class->getMethods() as $method) { if (C\is_empty($method->getAttributes())) continue; @@ -69,7 +69,9 @@ final class FieldResolver { $graphql_field = $rm->getAttributeClass(\Slack\GraphQL\Field::class); if ($graphql_field is null) continue; - $fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($graphql_field, $rm); + $directives = await $this->directives_finder->findDirectivesForField($method); + + $fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($graphql_field, $rm, $directives); } return $fields; diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index 8c6d188..73b94fc 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -31,7 +31,6 @@ final class Generator { 'output_directory' => string, 'namespace' => string, ?'codegen_config' => IHackCodegenConfig, - ?'custom_directives' => vec> ); private function __construct(private MultiParser $parser, private self::TGeneratorConfig $config) { @@ -61,7 +60,9 @@ final class Generator { } public async function generate(): Awaitable { - $objects = await $this->collectObjects(); + $custom_directives = $this->collectDirectives(); + $directives_finder = new DirectivesFinder($custom_directives); + $objects = await $this->collectObjects($directives_finder); self::removeDirectory($this->config['output_directory']); \mkdir($this->config['output_directory']); @@ -196,7 +197,7 @@ final class Generator { return $resolve_method; } - private async function collectObjects(): Awaitable> { + private async function collectObjects(DirectivesFinder $directive_finder): Awaitable> { $objects = vec[]; $query_fields = dict[ '__schema' => FieldBuilder::introspectSchemaField(), @@ -206,8 +207,8 @@ final class Generator { $classish_objects = $this->parser->getClassishObjects(); - $field_resolver = new FieldResolver($classish_objects); - $class_fields = $field_resolver->resolveFields(); + $field_resolver = new FieldResolver($classish_objects, $directive_finder); + $class_fields = await $field_resolver->resolveFields(); $hack_class_to_graphql_interface = dict[]; $hack_class_to_graphql_object = dict[]; @@ -344,4 +345,15 @@ final class Generator { ObjectBuilder::forEdge($type_info['gql_type'], $type_info['hack_type'], $type_info['output_type']), ]; } -} + + private function collectDirectives(): keyset { + $custom_directives = keyset[]; + foreach ($this->parser->getClasses() as $class) { + $rc = new \ReflectionClass($class->getName()); + if (C\contains($rc->getInterfaceNames(), \Slack\GraphQL\Directive::class)) { + $custom_directives[] = '\\'.$rc->getName(); + } + } + return $custom_directives; + } +} \ No newline at end of file diff --git a/src/Directive.hack b/src/Directive.hack index bf877d3..4f73d47 100644 --- a/src/Directive.hack +++ b/src/Directive.hack @@ -2,4 +2,6 @@ namespace Slack\GraphQL; interface Directive {} -interface FieldDirective extends Directive, \HH\MethodAttribute {} \ No newline at end of file +interface FieldDirective extends Directive, \HH\MethodAttribute { + public function beforeResolve(): Awaitable; +} \ No newline at end of file diff --git a/src/FieldDefinition.hack b/src/FieldDefinition.hack index 4c965aa..790e8d1 100644 --- a/src/FieldDefinition.hack +++ b/src/FieldDefinition.hack @@ -29,6 +29,7 @@ final class FieldDefinition implements IResolvableFiel dict, Variables, ): Awaitable) $resolver, + private vec $directives ) {} public async function resolveAsync( @@ -38,6 +39,9 @@ final class FieldDefinition implements IResolvableFiel ): Awaitable> { $resolver = $this->resolver; try { + foreach ($this->directives as $directive) { + await $directive->beforeResolve(); + } $value = await $resolver( $parent, // Validation guarantees all of the grouped field nodes have the same arguments, so it doesn't matter diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index c7d2645..ebcd76a 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -2,4 +2,21 @@ namespace Directives; final class HasRole implements \Slack\GraphQL\FieldDirective { public function __construct(private vec $roles) {} + public async function beforeResolve(): Awaitable { + echo "HasRole\n"; + } +} + +final class LogSampled implements \Slack\GraphQL\FieldDirective { + public function __construct(private float $frequency, private string $prefix) {} + public async function beforeResolve(): Awaitable { + echo "LogSampled\n"; + } +} + +final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { + public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} + public async function beforeResolve(): Awaitable { + echo "TestShapeDirective\n"; + } } \ No newline at end of file diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 8b9974a..3da72b6 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -2,6 +2,8 @@ use namespace Slack\GraphQL; +use type Directives\HasRole; +use type Directives\{TestShapeDirective}; use namespace HH\Lib\{C, Math, Str, Vec}; <> @@ -31,7 +33,6 @@ final class TeamStore { if (self::$store is null) { self::$store = new self(); } - return self::$store; } @@ -137,7 +138,9 @@ final class PermissionService { final class Human extends BaseUser { << GraphQL\Field('favorite_color', 'Favorite color of the user'), - Directives\HasRole(vec['STAFF']) + HasRole(vec['STAFF']), + \Directives\LogSampled(33.3, 'foo'), + TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true) >> public function getFavoriteColor(): FavoriteColor { return FavoriteColor::BLUE; diff --git a/tests/gen/AlphabetConnection.hack b/tests/gen/AlphabetConnection.hack index 8f6b988..83644cc 100644 --- a/tests/gen/AlphabetConnection.hack +++ b/tests/gen/AlphabetConnection.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<62edee806dca502f5b400aefff59f52a>> + * @generated SignedSource<<719b63da1617621fd11c2eecc7a5937a>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,7 @@ final class AlphabetConnection extends \Slack\GraphQL\Types\ObjectType { StringTypeEdge::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> await $parent->getEdges(), + vec[], ); case 'pageInfo': return new GraphQL\FieldDefinition( @@ -39,6 +40,7 @@ final class AlphabetConnection extends \Slack\GraphQL\Types\ObjectType { PageInfo::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getPageInfo(), + vec[], ); default: return null; diff --git a/tests/gen/AnotherObjectShape.hack b/tests/gen/AnotherObjectShape.hack index 1f5b18f..53fc88d 100644 --- a/tests/gen/AnotherObjectShape.hack +++ b/tests/gen/AnotherObjectShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1df6595e51f53c069509e4a7216ba04f>> + * @generated SignedSource<<1611425c7afdec8569bbf17bf71fe48f>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -31,6 +31,7 @@ final class AnotherObjectShape extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent['abc'], + vec[], ); default: return null; diff --git a/tests/gen/Bot.hack b/tests/gen/Bot.hack index 26496e2..178097e 100644 --- a/tests/gen/Bot.hack +++ b/tests/gen/Bot.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<3b315c322b594d0d3e3317ae5e0f799e>> + * @generated SignedSource<<1567641b10047f4bea25dcbba5c438dc>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -37,6 +37,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getId(), + vec[], ); case 'is_active': return new GraphQL\FieldDefinition( @@ -44,6 +45,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->isActive(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -51,6 +53,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getName(), + vec[], ); case 'primary_function': return new GraphQL\FieldDefinition( @@ -58,6 +61,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getPrimaryFunction(), + vec[], ); case 'roles': return new GraphQL\FieldDefinition( @@ -65,6 +69,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Role::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getRoles(), + vec[], ); case 'team': return new GraphQL\FieldDefinition( @@ -72,6 +77,7 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { Team::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getTeam(), + vec[], ); default: return null; diff --git a/tests/gen/Concrete.hack b/tests/gen/Concrete.hack index 05cbb58..a4e6fb0 100644 --- a/tests/gen/Concrete.hack +++ b/tests/gen/Concrete.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<5e33a7e29516c49900df494dac33f7f5>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -35,6 +35,7 @@ final class Concrete extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->bar(), + vec[], ); case 'baz': return new GraphQL\FieldDefinition( @@ -42,6 +43,7 @@ final class Concrete extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->baz(), + vec[], ); case 'foo': return new GraphQL\FieldDefinition( @@ -49,6 +51,7 @@ final class Concrete extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->foo(), + vec[], ); default: return null; diff --git a/tests/gen/ErrorTestObj.hack b/tests/gen/ErrorTestObj.hack index b2e43e0..419515a 100644 --- a/tests/gen/ErrorTestObj.hack +++ b/tests/gen/ErrorTestObj.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<4938ded1db5fdd04f9875088d7f4978d>> + * @generated SignedSource<<85f7e816fb6c20b5fc75832ff1b72814>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -42,6 +42,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->bad_int_list_n_of_n(), + vec[], ); case 'bad_int_list_n_of_nn': return new GraphQL\FieldDefinition( @@ -49,6 +50,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->bad_int_list_n_of_nn(), + vec[], ); case 'bad_int_list_nn_of_nn': return new GraphQL\FieldDefinition( @@ -56,6 +58,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nonNullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->bad_int_list_nn_of_nn(), + vec[], ); case 'hidden_exception': return new GraphQL\FieldDefinition( @@ -63,6 +66,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->hidden_exception(), + vec[], ); case 'nested': return new GraphQL\FieldDefinition( @@ -70,6 +74,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->nested(), + vec[], ); case 'nested_list_n_of_n': return new GraphQL\FieldDefinition( @@ -77,6 +82,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nullableOutput()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->nested_list_n_of_n(), + vec[], ); case 'nested_list_n_of_nn': return new GraphQL\FieldDefinition( @@ -84,6 +90,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->nested_list_n_of_nn(), + vec[], ); case 'nested_list_nn_of_nn': return new GraphQL\FieldDefinition( @@ -91,6 +98,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nonNullable()->nonNullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->nested_list_nn_of_nn(), + vec[], ); case 'nested_nn': return new GraphQL\FieldDefinition( @@ -98,6 +106,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nonNullable(), dict[], async ($parent, $args, $vars) ==> $parent->nested_nn(), + vec[], ); case 'no_error': return new GraphQL\FieldDefinition( @@ -105,6 +114,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->no_error(), + vec[], ); case 'non_nullable': return new GraphQL\FieldDefinition( @@ -112,6 +122,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable(), dict[], async ($parent, $args, $vars) ==> $parent->non_nullable(), + vec[], ); case 'user_facing_error': return new GraphQL\FieldDefinition( @@ -119,6 +130,7 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->user_facing_error(), + vec[], ); default: return null; diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index e3bbc52..3bb5fa4 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<74cee04a80f9d7fcba46fd5b48e66987>> + * @generated SignedSource<<5456f22b07464c25859ffcdd506754a6>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,6 +39,16 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { FavoriteColor::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), + vec[ + new \Directives\HasRole(vec [ + 'STAFF', + ]), + new \Directives\LogSampled(33.3, 'foo'), + new \Directives\TestShapeDirective(shape ( + 'foo' => 1, + 'bar' => 'abc', + ), true), + ], ); case 'friends': return new GraphQL\FieldDefinition( @@ -72,6 +82,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableInput()->coerceOptionalNamedNode('first', $args, $vars, null), Types\IntType::nullableInput()->coerceOptionalNamedNode('last', $args, $vars, null), ), + vec[], ); case 'id': return new GraphQL\FieldDefinition( @@ -79,6 +90,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getId(), + vec[], ); case 'is_active': return new GraphQL\FieldDefinition( @@ -86,6 +98,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->isActive(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -93,6 +106,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getName(), + vec[], ); case 'named_friends': return new GraphQL\FieldDefinition( @@ -132,6 +146,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableInput()->coerceOptionalNamedNode('first', $args, $vars, null), Types\IntType::nullableInput()->coerceOptionalNamedNode('last', $args, $vars, null), ), + vec[], ); case 'roles': return new GraphQL\FieldDefinition( @@ -139,6 +154,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Role::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getRoles(), + vec[], ); case 'team': return new GraphQL\FieldDefinition( @@ -146,6 +162,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { Team::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getTeam(), + vec[], ); default: return null; diff --git a/tests/gen/InterfaceA.hack b/tests/gen/InterfaceA.hack index ba89bb8..6651b79 100644 --- a/tests/gen/InterfaceA.hack +++ b/tests/gen/InterfaceA.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<2692931475deaf2db40669c79479288f>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,7 @@ final class InterfaceA extends \Slack\GraphQL\Types\InterfaceType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->foo(), + vec[], ); default: return null; diff --git a/tests/gen/InterfaceB.hack b/tests/gen/InterfaceB.hack index 483b134..f15c546 100644 --- a/tests/gen/InterfaceB.hack +++ b/tests/gen/InterfaceB.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<4452f961c73140a819658affb076471a>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -33,6 +33,7 @@ final class InterfaceB extends \Slack\GraphQL\Types\InterfaceType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->bar(), + vec[], ); case 'foo': return new GraphQL\FieldDefinition( @@ -40,6 +41,7 @@ final class InterfaceB extends \Slack\GraphQL\Types\InterfaceType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->foo(), + vec[], ); default: return null; diff --git a/tests/gen/IntrospectionTestObject.hack b/tests/gen/IntrospectionTestObject.hack index 7a1644b..fc9df0d 100644 --- a/tests/gen/IntrospectionTestObject.hack +++ b/tests/gen/IntrospectionTestObject.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<2ec633867a1874f20746329c497145ab>> + * @generated SignedSource<<96d26624e992a9825611f6456287747c>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -37,6 +37,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getDefaultListOfNonNullableInt(), + vec[], ); case 'default_list_of_nullable_int': return new GraphQL\FieldDefinition( @@ -44,6 +45,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getDefaultListOfNullableInt(), + vec[], ); case 'default_nullable_string': return new GraphQL\FieldDefinition( @@ -51,6 +53,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getDefaultNullableString(), + vec[], ); case 'non_null_int': return new GraphQL\FieldDefinition( @@ -58,6 +61,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable(), dict[], async ($parent, $args, $vars) ==> $parent->getNonNullInt(), + vec[], ); case 'non_null_list_of_non_null': return new GraphQL\FieldDefinition( @@ -65,6 +69,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nonNullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getNonNullListOfNonNull(), + vec[], ); case 'non_null_string': return new GraphQL\FieldDefinition( @@ -72,6 +77,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nonNullable(), dict[], async ($parent, $args, $vars) ==> $parent->getNonNullString(), + vec[], ); case 'nullable_string': return new GraphQL\FieldDefinition( @@ -79,6 +85,7 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getNullableString(), + vec[], ); default: return null; diff --git a/tests/gen/Mutation.hack b/tests/gen/Mutation.hack index 5a134de..94fbe7e 100644 --- a/tests/gen/Mutation.hack +++ b/tests/gen/Mutation.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<8effc458f88d700e19fc5f34452ec88f>> + * @generated SignedSource<<3c2d153ebae73993aabf8a07ad1c4140>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,6 +39,7 @@ final class Mutation extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserMutationAttributes::createUser( CreateUserInput::nonNullable()->coerceNamedNode('input', $args, $vars), ), + vec[], ); case 'pokeUser': return new GraphQL\FieldDefinition( @@ -53,6 +54,7 @@ final class Mutation extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserMutationAttributes::pokeUser( Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), ), + vec[], ); default: return null; diff --git a/tests/gen/NestedOutputShape.hack b/tests/gen/NestedOutputShape.hack index 389f7c5..738029c 100644 --- a/tests/gen/NestedOutputShape.hack +++ b/tests/gen/NestedOutputShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<5e9816ae88463151bbbadeb9b46700a7>> + * @generated SignedSource<<4e04f7362248af9d50f125809c616da4>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -31,6 +31,7 @@ final class NestedOutputShape extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent['vec_of_string'], + vec[], ); default: return null; diff --git a/tests/gen/ObjectShape.hack b/tests/gen/ObjectShape.hack index 9b63d57..1a98c2b 100644 --- a/tests/gen/ObjectShape.hack +++ b/tests/gen/ObjectShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<4ee05a200f698afc8c2432c2a1b97359>> + * @generated SignedSource<<61f3547801db2170a80dec294a02a9a9>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -33,6 +33,7 @@ final class ObjectShape extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['foo'], + vec[], ); case 'bar': return new GraphQL\FieldDefinition( @@ -40,6 +41,7 @@ final class ObjectShape extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['bar'] ?? null, + vec[], ); case 'baz': return new GraphQL\FieldDefinition( @@ -47,6 +49,7 @@ final class ObjectShape extends \Slack\GraphQL\Types\ObjectType { AnotherObjectShape::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['baz'], + vec[], ); default: return null; diff --git a/tests/gen/OutputShape.hack b/tests/gen/OutputShape.hack index 10c4a20..186a406 100644 --- a/tests/gen/OutputShape.hack +++ b/tests/gen/OutputShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<10a6010662c45c2b62ad31c58ebbb25d>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -33,6 +33,7 @@ final class OutputShape extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['string'], + vec[], ); case 'vec_of_int': return new GraphQL\FieldDefinition( @@ -40,6 +41,7 @@ final class OutputShape extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent['vec_of_int'], + vec[], ); case 'nested_shape': return new GraphQL\FieldDefinition( @@ -47,6 +49,7 @@ final class OutputShape extends \Slack\GraphQL\Types\ObjectType { NestedOutputShape::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['nested_shape'], + vec[], ); default: return null; diff --git a/tests/gen/OutputTypeTestObj.hack b/tests/gen/OutputTypeTestObj.hack index aab4325..3c3b40e 100644 --- a/tests/gen/OutputTypeTestObj.hack +++ b/tests/gen/OutputTypeTestObj.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<26a44836f820c6e077cb276f7420989f>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -38,6 +38,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->awaitable(), + vec[], ); case 'awaitable_nullable': return new GraphQL\FieldDefinition( @@ -45,6 +46,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->awaitable_nullable(), + vec[], ); case 'awaitable_nullable_list': return new GraphQL\FieldDefinition( @@ -52,6 +54,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> await $parent->awaitable_nullable_list(), + vec[], ); case 'list': return new GraphQL\FieldDefinition( @@ -59,6 +62,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->list(), + vec[], ); case 'nested_lists': return new GraphQL\FieldDefinition( @@ -66,6 +70,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput()->nonNullableOutputListOf()->nullableOutputListOf()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->nested_lists(), + vec[], ); case 'nullable': return new GraphQL\FieldDefinition( @@ -73,6 +78,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->nullable(), + vec[], ); case 'output_shape': return new GraphQL\FieldDefinition( @@ -80,6 +86,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { OutputShape::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->output_shape(), + vec[], ); case 'scalar': return new GraphQL\FieldDefinition( @@ -87,6 +94,7 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->scalar(), + vec[], ); default: return null; diff --git a/tests/gen/PageInfo.hack b/tests/gen/PageInfo.hack index 93c39c7..c656a31 100644 --- a/tests/gen/PageInfo.hack +++ b/tests/gen/PageInfo.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<39088bc448c50e4310ce649372f459cf>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -34,6 +34,7 @@ final class PageInfo extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['startCursor'] ?? null, + vec[], ); case 'endCursor': return new GraphQL\FieldDefinition( @@ -41,6 +42,7 @@ final class PageInfo extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['endCursor'] ?? null, + vec[], ); case 'hasPreviousPage': return new GraphQL\FieldDefinition( @@ -48,6 +50,7 @@ final class PageInfo extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['hasPreviousPage'] ?? null, + vec[], ); case 'hasNextPage': return new GraphQL\FieldDefinition( @@ -55,6 +58,7 @@ final class PageInfo extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['hasNextPage'] ?? null, + vec[], ); default: return null; diff --git a/tests/gen/Query.hack b/tests/gen/Query.hack index 9da502b..79b44ae 100644 --- a/tests/gen/Query.hack +++ b/tests/gen/Query.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -48,6 +48,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { __Schema::nullableOutput(), dict[], async ($parent, $args, $vars) ==> new Schema(), + vec[], ); case '__type': return new GraphQL\FieldDefinition( @@ -63,6 +64,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nonNullable()->coerceNamedNode('name', $args, $vars), ), + vec[], ); case 'alphabetConnection': return new GraphQL\FieldDefinition( @@ -96,6 +98,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableInput()->coerceOptionalNamedNode('first', $args, $vars, null), Types\IntType::nullableInput()->coerceOptionalNamedNode('last', $args, $vars, null), ), + vec[], ); case 'arg_test': return new GraphQL\FieldDefinition( @@ -121,6 +124,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableInput()->coerceNamedNode('nullable', $args, $vars), Types\IntType::nullableInput()->coerceOptionalNamedNode('optional', $args, $vars, 42), ), + vec[], ); case 'bot': return new GraphQL\FieldDefinition( @@ -135,6 +139,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserQueryAttributes::getBot( Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), ), + vec[], ); case 'error_test': return new GraphQL\FieldDefinition( @@ -142,6 +147,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \ErrorTestObj::get(), + vec[], ); case 'error_test_nn': return new GraphQL\FieldDefinition( @@ -149,6 +155,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { ErrorTestObj::nonNullable(), dict[], async ($parent, $args, $vars) ==> \ErrorTestObj::getNonNullable(), + vec[], ); case 'getConcrete': return new GraphQL\FieldDefinition( @@ -156,6 +163,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { Concrete::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \Concrete::getConcrete(), + vec[], ); case 'getInterfaceA': return new GraphQL\FieldDefinition( @@ -163,6 +171,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { InterfaceA::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \Concrete::getInterfaceA(), + vec[], ); case 'getInterfaceB': return new GraphQL\FieldDefinition( @@ -170,6 +179,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { InterfaceB::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \Concrete::getInterfaceB(), + vec[], ); case 'getObjectShape': return new GraphQL\FieldDefinition( @@ -177,6 +187,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { ObjectShape::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \ObjectTypeTestEntrypoint::getObjectShape(), + vec[], ); case 'human': return new GraphQL\FieldDefinition( @@ -191,6 +202,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserQueryAttributes::getHuman( Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), ), + vec[], ); case 'introspection_test': return new GraphQL\FieldDefinition( @@ -198,6 +210,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { IntrospectionTestObject::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \IntrospectionTestObject::get(), + vec[], ); case 'list_arg_test': return new GraphQL\FieldDefinition( @@ -212,6 +225,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> \ArgumentTestObj::listArgTest( Types\IntType::nonNullable()->nullableInputListOf()->nonNullableInputListOf()->nullableInputListOf()->coerceNamedNode('arg', $args, $vars), ), + vec[], ); case 'nested_list_sum': return new GraphQL\FieldDefinition( @@ -226,6 +240,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> \UserQueryAttributes::getNestedListSum( Types\IntType::nonNullable()->nonNullableInputListOf()->nonNullableInputListOf()->coerceNamedNode('numbers', $args, $vars), ), + vec[], ); case 'optional_field_test': return new GraphQL\FieldDefinition( @@ -240,6 +255,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> \UserQueryAttributes::optionalFieldTest( CreateUserInput::nonNullable()->coerceNamedNode('input', $args, $vars), ), + vec[], ); case 'output_type_test': return new GraphQL\FieldDefinition( @@ -247,6 +263,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { OutputTypeTestObj::nullableOutput(), dict[], async ($parent, $args, $vars) ==> \OutputTypeTestObj::get(), + vec[], ); case 'takes_favorite_color': return new GraphQL\FieldDefinition( @@ -261,6 +278,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> \UserQueryAttributes::takesFavoriteColor( FavoriteColor::nonNullable()->coerceNamedNode('favorite_color', $args, $vars), ), + vec[], ); case 'user': return new GraphQL\FieldDefinition( @@ -275,6 +293,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserQueryAttributes::getUser( Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), ), + vec[], ); case 'viewer': return new GraphQL\FieldDefinition( @@ -282,6 +301,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { User::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await \UserQueryAttributes::getViewer(), + vec[], ); default: return null; diff --git a/tests/gen/StringTypeEdge.hack b/tests/gen/StringTypeEdge.hack index 27333d5..26a8152 100644 --- a/tests/gen/StringTypeEdge.hack +++ b/tests/gen/StringTypeEdge.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<9edbef9197acde53464ef3e6c06253e3>> + * @generated SignedSource<<2f08369620321d56e1063d4826923e8e>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,7 @@ final class StringTypeEdge extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getNode(), + vec[], ); case 'cursor': return new GraphQL\FieldDefinition( @@ -39,6 +40,7 @@ final class StringTypeEdge extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getCursor(), + vec[], ); default: return null; diff --git a/tests/gen/Team.hack b/tests/gen/Team.hack index d541d9d..a7b32b7 100644 --- a/tests/gen/Team.hack +++ b/tests/gen/Team.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<42f8451c72c0c7713661e2da057f8902>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -41,6 +41,7 @@ final class Team extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> $parent->getDescription( Types\BooleanType::nonNullable()->coerceNamedNode('short', $args, $vars), ), + vec[], ); case 'id': return new GraphQL\FieldDefinition( @@ -48,6 +49,7 @@ final class Team extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getId(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -55,6 +57,7 @@ final class Team extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getName(), + vec[], ); case 'num_users': return new GraphQL\FieldDefinition( @@ -62,6 +65,7 @@ final class Team extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getNumUsers(), + vec[], ); default: return null; diff --git a/tests/gen/User.hack b/tests/gen/User.hack index 91c0eb1..a4a3b5a 100644 --- a/tests/gen/User.hack +++ b/tests/gen/User.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<47c161bdd9510702ccbfb76106d4bc21>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -37,6 +37,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { Types\IntType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getId(), + vec[], ); case 'is_active': return new GraphQL\FieldDefinition( @@ -44,6 +45,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->isActive(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -51,6 +53,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getName(), + vec[], ); case 'roles': return new GraphQL\FieldDefinition( @@ -58,6 +61,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { Role::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getRoles(), + vec[], ); case 'team': return new GraphQL\FieldDefinition( @@ -65,6 +69,7 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { Team::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getTeam(), + vec[], ); default: return null; diff --git a/tests/gen/UserConnection.hack b/tests/gen/UserConnection.hack index f43672f..328e355 100644 --- a/tests/gen/UserConnection.hack +++ b/tests/gen/UserConnection.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<10f75bca86cba0f89742c4b2faf55f99>> + * @generated SignedSource<<8148f902c0b3c1fad35b44af1750c389>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,7 @@ final class UserConnection extends \Slack\GraphQL\Types\ObjectType { UserEdge::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> await $parent->getEdges(), + vec[], ); case 'pageInfo': return new GraphQL\FieldDefinition( @@ -39,6 +40,7 @@ final class UserConnection extends \Slack\GraphQL\Types\ObjectType { PageInfo::nullableOutput(), dict[], async ($parent, $args, $vars) ==> await $parent->getPageInfo(), + vec[], ); default: return null; diff --git a/tests/gen/UserEdge.hack b/tests/gen/UserEdge.hack index f19bb53..9300645 100644 --- a/tests/gen/UserEdge.hack +++ b/tests/gen/UserEdge.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<10bd530f34fa766a0c89a1f81e8f8ad3>> + * @generated SignedSource<<8881f007a7263fc895b7e0d6b52a8956>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,7 @@ final class UserEdge extends \Slack\GraphQL\Types\ObjectType { User::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getNode(), + vec[], ); case 'cursor': return new GraphQL\FieldDefinition( @@ -39,6 +40,7 @@ final class UserEdge extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getCursor(), + vec[], ); default: return null; diff --git a/tests/gen/__Directive.hack b/tests/gen/__Directive.hack index 4853811..f94be66 100644 --- a/tests/gen/__Directive.hack +++ b/tests/gen/__Directive.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -34,6 +34,7 @@ final class __Directive extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['name'], + vec[], ); case 'description': return new GraphQL\FieldDefinition( @@ -41,6 +42,7 @@ final class __Directive extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['description'], + vec[], ); case 'locations': return new GraphQL\FieldDefinition( @@ -48,6 +50,7 @@ final class __Directive extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent['locations'], + vec[], ); case 'args': return new GraphQL\FieldDefinition( @@ -55,6 +58,7 @@ final class __Directive extends \Slack\GraphQL\Types\ObjectType { __InputValue::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent['args'], + vec[], ); default: return null; diff --git a/tests/gen/__EnumValue.hack b/tests/gen/__EnumValue.hack index 7c8dfd0..43abddb 100644 --- a/tests/gen/__EnumValue.hack +++ b/tests/gen/__EnumValue.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<2a376d1323cd8b83e6af8c63aa02034c>> + * @generated SignedSource<<723b75b056d5ca1b83a331a7c8c81d82>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -34,6 +34,7 @@ final class __EnumValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['name'], + vec[], ); case 'isDeprecated': return new GraphQL\FieldDefinition( @@ -41,6 +42,7 @@ final class __EnumValue extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['isDeprecated'], + vec[], ); case 'description': return new GraphQL\FieldDefinition( @@ -48,6 +50,7 @@ final class __EnumValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['description'] ?? null, + vec[], ); case 'deprecationReason': return new GraphQL\FieldDefinition( @@ -55,6 +58,7 @@ final class __EnumValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['deprecationReason'] ?? null, + vec[], ); default: return null; diff --git a/tests/gen/__Field.hack b/tests/gen/__Field.hack index dd48c35..79a5879 100644 --- a/tests/gen/__Field.hack +++ b/tests/gen/__Field.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1d6ae474d70729d9c6200f3336b331be>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -36,6 +36,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { __InputValue::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getArgs(), + vec[], ); case 'deprecationReason': return new GraphQL\FieldDefinition( @@ -43,6 +44,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getDeprecationReason(), + vec[], ); case 'description': return new GraphQL\FieldDefinition( @@ -50,6 +52,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getDescription(), + vec[], ); case 'isDeprecated': return new GraphQL\FieldDefinition( @@ -57,6 +60,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { Types\BooleanType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->isDeprecated(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -64,6 +68,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getName(), + vec[], ); case 'type': return new GraphQL\FieldDefinition( @@ -71,6 +76,7 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getType(), + vec[], ); default: return null; diff --git a/tests/gen/__InputValue.hack b/tests/gen/__InputValue.hack index 84df0d3..28bbc86 100644 --- a/tests/gen/__InputValue.hack +++ b/tests/gen/__InputValue.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<8d46941c60191c5c536e393a2ced87f2>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -34,6 +34,7 @@ final class __InputValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['name'], + vec[], ); case 'description': return new GraphQL\FieldDefinition( @@ -41,6 +42,7 @@ final class __InputValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['description'] ?? null, + vec[], ); case 'type': return new GraphQL\FieldDefinition( @@ -48,6 +50,7 @@ final class __InputValue extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['type'], + vec[], ); case 'defaultValue': return new GraphQL\FieldDefinition( @@ -55,6 +58,7 @@ final class __InputValue extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent['defaultValue'] ?? null, + vec[], ); default: return null; diff --git a/tests/gen/__Schema.hack b/tests/gen/__Schema.hack index a254d4f..6a722a9 100644 --- a/tests/gen/__Schema.hack +++ b/tests/gen/__Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<3a2ace1dfaf20502d018e61b7180b689>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -35,6 +35,7 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { __Directive::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getDirectives(), + vec[], ); case 'mutationType': return new GraphQL\FieldDefinition( @@ -42,6 +43,7 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionMutationType(), + vec[], ); case 'queryType': return new GraphQL\FieldDefinition( @@ -49,6 +51,7 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionQueryType(), + vec[], ); case 'subscriptionType': return new GraphQL\FieldDefinition( @@ -56,6 +59,7 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionSubscriptionType(), + vec[], ); case 'types': return new GraphQL\FieldDefinition( @@ -63,6 +67,7 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { __Type::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getTypes(), + vec[], ); default: return null; diff --git a/tests/gen/__Type.hack b/tests/gen/__Type.hack index 05376fb..9ab67f4 100644 --- a/tests/gen/__Type.hack +++ b/tests/gen/__Type.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<46725fb0d0175c7f30579a28e030cf3b>> + * @generated SignedSource<<7fd4a983d839245ee2f2e216f6ef0e09>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,6 +39,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionDescription(), + vec[], ); case 'enumValues': return new GraphQL\FieldDefinition( @@ -54,6 +55,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> $parent->getIntrospectionEnumValues( Types\BooleanType::nonNullable()->coerceOptionalNamedNode('includeDeprecated', $args, $vars, false), ), + vec[], ); case 'fields': return new GraphQL\FieldDefinition( @@ -69,6 +71,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> $parent->getIntrospectionFields( Types\BooleanType::nonNullable()->coerceOptionalNamedNode('includeDeprecated', $args, $vars, false), ), + vec[], ); case 'inputFields': return new GraphQL\FieldDefinition( @@ -76,6 +79,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { __InputValue::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionInputFields(), + vec[], ); case 'interfaces': return new GraphQL\FieldDefinition( @@ -83,6 +87,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { __Type::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionInterfaces(), + vec[], ); case 'kind': return new GraphQL\FieldDefinition( @@ -90,6 +95,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { __TypeKind::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionKind(), + vec[], ); case 'name': return new GraphQL\FieldDefinition( @@ -97,6 +103,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { Types\StringType::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionName(), + vec[], ); case 'ofType': return new GraphQL\FieldDefinition( @@ -104,6 +111,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { __Type::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionOfType(), + vec[], ); case 'possibleTypes': return new GraphQL\FieldDefinition( @@ -111,6 +119,7 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { __Type::nonNullable()->nullableOutputListOf(), dict[], async ($parent, $args, $vars) ==> $parent->getIntrospectionPossibleTypes(), + vec[], ); default: return null; From f42c11f80be50f3ca6fb3bd4573594b3578bd435 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Fri, 24 Sep 2021 14:27:56 -0700 Subject: [PATCH 03/11] wip --- src/Directive.hack | 2 +- src/FieldDefinition.hack | 2 +- tests/Fixtures/Directives.hack | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Directive.hack b/src/Directive.hack index 4f73d47..663511c 100644 --- a/src/Directive.hack +++ b/src/Directive.hack @@ -3,5 +3,5 @@ namespace Slack\GraphQL; interface Directive {} interface FieldDirective extends Directive, \HH\MethodAttribute { - public function beforeResolve(): Awaitable; + public function beforeResolve(IFieldDefinition $field): Awaitable; } \ No newline at end of file diff --git a/src/FieldDefinition.hack b/src/FieldDefinition.hack index 790e8d1..f542b3c 100644 --- a/src/FieldDefinition.hack +++ b/src/FieldDefinition.hack @@ -40,7 +40,7 @@ final class FieldDefinition implements IResolvableFiel $resolver = $this->resolver; try { foreach ($this->directives as $directive) { - await $directive->beforeResolve(); + await $directive->beforeResolve($this); } $value = await $resolver( $parent, diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index ebcd76a..ff74b24 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -1,22 +1,24 @@ namespace Directives; +use namespace Slack\GraphQL; + final class HasRole implements \Slack\GraphQL\FieldDirective { public function __construct(private vec $roles) {} - public async function beforeResolve(): Awaitable { + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { echo "HasRole\n"; } } final class LogSampled implements \Slack\GraphQL\FieldDirective { public function __construct(private float $frequency, private string $prefix) {} - public async function beforeResolve(): Awaitable { + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { echo "LogSampled\n"; } } final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} - public async function beforeResolve(): Awaitable { + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { echo "TestShapeDirective\n"; } } \ No newline at end of file From aec6564785d12ddc693dc7b768c5a623f375699a Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Sat, 25 Sep 2021 15:39:23 -0700 Subject: [PATCH 04/11] wip --- tests/FixtureTest.hack | 2 ++ tests/Fixtures/Directives.hack | 40 +++++++++++++++++----------------- tests/Fixtures/Playground.hack | 10 ++++----- tests/gen/Human.hack | 13 ++--------- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/tests/FixtureTest.hack b/tests/FixtureTest.hack index f959880..256e55a 100644 --- a/tests/FixtureTest.hack +++ b/tests/FixtureTest.hack @@ -18,6 +18,7 @@ abstract class FixtureTest extends \Facebook\HackTest\HackTest { <<__Memoize>> private static async function runCodegenAsync(): Awaitable { + $start_ts = microtime(true); await GraphQL\Codegen\Generator::forPath( __DIR__.'/Fixtures', shape( @@ -25,6 +26,7 @@ abstract class FixtureTest extends \Facebook\HackTest\HackTest { 'namespace' => 'Slack\GraphQL\Test\Generated', ), ); + echo "Generated fixtures in: ".(microtime(true) - $start_ts)."\n"; } <<\Facebook\HackTest\DataProvider('getTestCases')>> diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index ff74b24..800a1c0 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -1,24 +1,24 @@ -namespace Directives; +// namespace Directives; -use namespace Slack\GraphQL; +// use namespace Slack\GraphQL; -final class HasRole implements \Slack\GraphQL\FieldDirective { - public function __construct(private vec $roles) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { - echo "HasRole\n"; - } -} +// final class HasRole implements \Slack\GraphQL\FieldDirective { +// public function __construct(private vec $roles) {} +// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { +// echo "HasRole\n"; +// } +// } -final class LogSampled implements \Slack\GraphQL\FieldDirective { - public function __construct(private float $frequency, private string $prefix) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { - echo "LogSampled\n"; - } -} +// final class LogSampled implements \Slack\GraphQL\FieldDirective { +// public function __construct(private float $frequency, private string $prefix) {} +// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { +// echo "LogSampled\n"; +// } +// } -final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { - public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { - echo "TestShapeDirective\n"; - } -} \ No newline at end of file +// final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { +// public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} +// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { +// echo "TestShapeDirective\n"; +// } +// } \ No newline at end of file diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 3da72b6..5fd4a68 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -2,8 +2,8 @@ use namespace Slack\GraphQL; -use type Directives\HasRole; -use type Directives\{TestShapeDirective}; +// use type Directives\HasRole; +// use type Directives\{TestShapeDirective}; use namespace HH\Lib\{C, Math, Str, Vec}; <> @@ -138,9 +138,9 @@ final class PermissionService { final class Human extends BaseUser { << GraphQL\Field('favorite_color', 'Favorite color of the user'), - HasRole(vec['STAFF']), - \Directives\LogSampled(33.3, 'foo'), - TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true) + // HasRole(vec['STAFF']), + // \Directives\LogSampled(33.3, 'foo'), + // TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true) >> public function getFavoriteColor(): FavoriteColor { return FavoriteColor::BLUE; diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index 3bb5fa4..0d1ba21 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<5456f22b07464c25859ffcdd506754a6>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,16 +39,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { FavoriteColor::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), - vec[ - new \Directives\HasRole(vec [ - 'STAFF', - ]), - new \Directives\LogSampled(33.3, 'foo'), - new \Directives\TestShapeDirective(shape ( - 'foo' => 1, - 'bar' => 'abc', - ), true), - ], + vec[], ); case 'friends': return new GraphQL\FieldDefinition( From 750097b4cb5e21a35ac1d17160065041bcb64fe5 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Sun, 26 Sep 2021 09:27:25 -0700 Subject: [PATCH 05/11] wip --- src/Codegen/Builders/Fields/FieldBuilder.hack | 2 +- src/Codegen/DirectivesFinder.hack | 82 ++++++++++++------- src/Codegen/FieldResolver.hack | 13 ++- src/Codegen/Generator.hack | 2 +- src/Directive.hack | 3 +- src/FieldDefinition.hack | 2 +- tests/Fixtures/Directives.hack | 48 ++++++----- tests/Fixtures/Playground.hack | 13 +-- tests/gen/Human.hack | 13 ++- 9 files changed, 112 insertions(+), 66 deletions(-) diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index 2fe9b32..0ef7437 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -56,7 +56,7 @@ abstract class FieldBuilder { return $data; }, ), - 'directives' => $directives + 'directives' => $directives, ); if ($is_root_field) { diff --git a/src/Codegen/DirectivesFinder.hack b/src/Codegen/DirectivesFinder.hack index 70a723a..285963e 100644 --- a/src/Codegen/DirectivesFinder.hack +++ b/src/Codegen/DirectivesFinder.hack @@ -1,3 +1,4 @@ + namespace Slack\GraphQL\Codegen; use namespace HH\Lib\{C, Str, Vec}; @@ -7,6 +8,10 @@ final class DirectivesFinder { public function __construct(private keyset $directives) {} public async function findDirectivesForField(ScannedMethod $method): Awaitable> { + if (!$this->directives) { + return vec[]; + } + $directives = vec[]; // Get all use namespaces and types present @@ -18,35 +23,50 @@ final class DirectivesFinder { // available in `getAttributes`, unfortunately. $attributes = $method->getAttributes(); foreach ($attributes as $attribute => $args) { - if (Str\starts_with($attribute, '\\')) { - $qualified_name = $attribute; - } else { - $namespace = Str\split($attribute, '\\') - |> Vec\take($$, C\count($$) - 1) - |> Str\join($$, '\\'); - // If it's a type, not a namespace, then there's nothing to split - // so just use the whole string. - if (!$namespace) { - $namespace = $attribute; - } - $qualified_name = ''; + // Consider names starting with `\` to be fully qualified + if (!Str\starts_with($attribute, '\\')) { + // + // Try to determine the fully qualified name of the attribute. + // + // There are two cases here: + // If the attribute is declared as `Baz`, then either: + // 1) Baz is not namespaced at all. + // 2) Baz is a imported via a use type statement, e.g., `use type Foo\Bar\Baz`. + // If the attribute is declared as `Bar/Baz`, then either: + // 1) Bar is a top-level namespace. + // 2) Bar is imported via a use namespace statement, e.g., `use namespace Foo\Bar`. + // + // In each case, we need to match the whatever comes before the first `\` in the attribute + // name (or the whole attribute name, if it contains no slashes) with whatever comes after + // the last `\` in each use type or use namespace statement. + // + // Once we have a match, we can build the fully qualified name by adding the attribute name + // to everything that comes before the last `\` in the eligible use statement. + // + $needle = Str\split($attribute, '\\') + |> C\firstx($$); foreach ($use_decls as $decl) { - if (C\contains_key($decl['clauses'], $namespace)) { - $qualified_name = '\\'.$decl['root'].'\\'.$attribute; + if (C\contains_key($decl['leaves'], $needle)) { + // We have a match such as `Bar\Baz` with `use namespace Foo\Bar`; + // build the qualified name as `Foo\Bar\Baz`. + $attribute = '\\'.$decl['root'].'\\'.$attribute; break; } } } - if (C\contains_key($this->directives, $qualified_name)) { + + if (C\contains_key($this->directives, $attribute)) { $directives[] = Str\format( 'new %s(%s)', - $qualified_name, - Vec\map($args, $arg ==> \var_export($arg, true) - |> Str\replace($$, 'varray', 'vec') - |> Str\replace($$, 'darray', 'dict') - |> Str\replace($$, 'array', 'shape') + $attribute, + Vec\map( + $args, + $arg ==> \var_export($arg, true) + |> Str\replace($$, 'varray', 'vec') + |> Str\replace($$, 'darray', 'dict') + |> Str\replace($$, 'array', 'shape'), ) - |> Str\join($$, ', ') + |> Str\join($$, ', '), ); } } @@ -57,7 +77,7 @@ final class DirectivesFinder { <<__Memoize>> private static async function getUseDeclarations(string $file_path): Awaitable string, - 'clauses' => keyset + 'leaves' => keyset, )>> { $ast = await \Facebook\HHAST\from_file_async(\Facebook\HHAST\File::fromPath($file_path)); return $ast->getDescendantsByType<\Facebook\HHAST\INamespaceUseDeclaration>() @@ -66,29 +86,29 @@ final class DirectivesFinder { if ($kind is null || $kind->getText() === 'function') { return null; } - $clauses = $decl->getClausesx()->toVec(); + $leaves = $decl->getClausesx()->toVec(); if ($decl is \Facebook\HHAST\NamespaceGroupUseDeclaration) { $root = $decl->getPrefixx()->getCode() |> Str\strip_suffix($$, '\\'); - $clauses = Vec\map($clauses, $clause ==> $clause->getFirstToken()?->getCode()) + $leaves = Vec\map($leaves, $leaf ==> $leaf->getFirstToken()?->getCode()) |> Vec\filter_nulls($$); } else { - $clauses = C\firstx($clauses) + $leaves = C\firstx($leaves) |> Str\split($$->getCode(), '\\'); - $num_clauses = C\count($clauses); - if ($num_clauses > 1) { - $root = Vec\take($clauses, $num_clauses - 1) + $num_leaves = C\count($leaves); + if ($num_leaves > 1) { + $root = Vec\take($leaves, $num_leaves - 1) |> Str\join($$, '\\'); - $clauses = Vec\drop($clauses, $num_clauses - 1); + $leaves = Vec\drop($leaves, $num_leaves - 1); } else { $root = ''; } } return shape( 'root' => $root, - 'clauses' => keyset($clauses), + 'leaves' => keyset($leaves), ); }) |> Vec\filter_nulls($$); } -} \ No newline at end of file +} diff --git a/src/Codegen/FieldResolver.hack b/src/Codegen/FieldResolver.hack index 36fe3dc..95fa299 100644 --- a/src/Codegen/FieldResolver.hack +++ b/src/Codegen/FieldResolver.hack @@ -14,7 +14,10 @@ final class FieldResolver { private dict $scanned_classes; private dict> $resolved_fields = dict[]; - public function __construct(vec $classes, private DirectivesFinder $directives_finder) { + public function __construct( + vec $classes, + private DirectivesFinder $directives_finder, + ) { $this->scanned_classes = Dict\from_values($classes, $class ==> $class->getName()); } @@ -34,7 +37,9 @@ final class FieldResolver { ) is nonnull; } - private async function resolveClass(DefinitionFinder\ScannedClassish $class): Awaitable> { + private async function resolveClass( + DefinitionFinder\ScannedClassish $class, + ): Awaitable> { if (C\contains_key($this->resolved_fields, $class->getName())) { return $this->resolved_fields[$class->getName()]; } @@ -60,7 +65,9 @@ final class FieldResolver { return $fields; } - private async function collectObjectFields(DefinitionFinder\ScannedClassish $class): Awaitable> { + private async function collectObjectFields( + DefinitionFinder\ScannedClassish $class, + ): Awaitable> { $fields = dict[]; foreach ($class->getMethods() as $method) { if (C\is_empty($method->getAttributes())) continue; diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index 73b94fc..b51e4d2 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -356,4 +356,4 @@ final class Generator { } return $custom_directives; } -} \ No newline at end of file +} diff --git a/src/Directive.hack b/src/Directive.hack index 663511c..ccdce41 100644 --- a/src/Directive.hack +++ b/src/Directive.hack @@ -1,7 +1,8 @@ + namespace Slack\GraphQL; interface Directive {} interface FieldDirective extends Directive, \HH\MethodAttribute { public function beforeResolve(IFieldDefinition $field): Awaitable; -} \ No newline at end of file +} diff --git a/src/FieldDefinition.hack b/src/FieldDefinition.hack index f542b3c..27a8f5c 100644 --- a/src/FieldDefinition.hack +++ b/src/FieldDefinition.hack @@ -29,7 +29,7 @@ final class FieldDefinition implements IResolvableFiel dict, Variables, ): Awaitable) $resolver, - private vec $directives + private vec $directives, ) {} public async function resolveAsync( diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index 800a1c0..c72349d 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -1,24 +1,32 @@ -// namespace Directives; -// use namespace Slack\GraphQL; +namespace Directives; -// final class HasRole implements \Slack\GraphQL\FieldDirective { -// public function __construct(private vec $roles) {} -// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { -// echo "HasRole\n"; -// } -// } +use namespace Slack\GraphQL; -// final class LogSampled implements \Slack\GraphQL\FieldDirective { -// public function __construct(private float $frequency, private string $prefix) {} -// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { -// echo "LogSampled\n"; -// } -// } +final class HasRole implements \Slack\GraphQL\FieldDirective { + public function __construct(private vec $roles) {} + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + echo "HasRole\n"; + } +} -// final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { -// public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} -// public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { -// echo "TestShapeDirective\n"; -// } -// } \ No newline at end of file +final class LogSampled implements \Slack\GraphQL\FieldDirective { + public function __construct(private float $frequency, private string $prefix) {} + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + echo "LogSampled\n"; + } +} + +final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { + public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + echo "TestShapeDirective\n"; + } +} + +final class AnotherFieldDirective implements \Slack\GraphQL\FieldDirective { + public function __construct() {} + public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + echo "AnotherFieldDirective\n"; + } +} diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 5fd4a68..6f400f9 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -2,8 +2,8 @@ use namespace Slack\GraphQL; -// use type Directives\HasRole; -// use type Directives\{TestShapeDirective}; +use type Directives\HasRole; +use type Directives\{TestShapeDirective}; use namespace HH\Lib\{C, Math, Str, Vec}; <> @@ -78,7 +78,7 @@ abstract class BaseUser implements User { 'name' => string, 'team_id' => int, 'is_active' => bool, - ?'roles' => vec + ?'roles' => vec, ) $data, ) {} @@ -138,9 +138,10 @@ final class PermissionService { final class Human extends BaseUser { << GraphQL\Field('favorite_color', 'Favorite color of the user'), - // HasRole(vec['STAFF']), - // \Directives\LogSampled(33.3, 'foo'), - // TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true) + HasRole(vec['STAFF']), + \Directives\LogSampled(33.3, 'foo'), + TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true), + Directives\AnotherFieldDirective, >> public function getFavoriteColor(): FavoriteColor { return FavoriteColor::BLUE; diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index 0d1ba21..3bb5fa4 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<5456f22b07464c25859ffcdd506754a6>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,7 +39,16 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { FavoriteColor::nullableOutput(), dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), - vec[], + vec[ + new \Directives\HasRole(vec [ + 'STAFF', + ]), + new \Directives\LogSampled(33.3, 'foo'), + new \Directives\TestShapeDirective(shape ( + 'foo' => 1, + 'bar' => 'abc', + ), true), + ], ); case 'friends': return new GraphQL\FieldDefinition( From bc52cda9b143f19b3e3df2675c88e0bbded5c3cf Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Mon, 27 Sep 2021 22:28:59 -0700 Subject: [PATCH 06/11] wip --- src/Codegen/DirectivesFinder.hack | 8 +++++-- tests/Fixtures/Playground.hack | 39 ------------------------------- tests/gen/Bot.hack | 11 +-------- tests/gen/Human.hack | 16 ++++--------- tests/gen/Role.hack | 29 ----------------------- tests/gen/Schema.hack | 3 +-- tests/gen/User.hack | 11 +-------- 7 files changed, 13 insertions(+), 104 deletions(-) delete mode 100644 tests/gen/Role.hack diff --git a/src/Codegen/DirectivesFinder.hack b/src/Codegen/DirectivesFinder.hack index 285963e..4b568c0 100644 --- a/src/Codegen/DirectivesFinder.hack +++ b/src/Codegen/DirectivesFinder.hack @@ -45,11 +45,12 @@ final class DirectivesFinder { // $needle = Str\split($attribute, '\\') |> C\firstx($$); + $attribute = '\\'.$attribute; foreach ($use_decls as $decl) { if (C\contains_key($decl['leaves'], $needle)) { // We have a match such as `Bar\Baz` with `use namespace Foo\Bar`; // build the qualified name as `Foo\Bar\Baz`. - $attribute = '\\'.$decl['root'].'\\'.$attribute; + $attribute = '\\'.$decl['root'].$attribute; break; } } @@ -64,7 +65,10 @@ final class DirectivesFinder { $arg ==> \var_export($arg, true) |> Str\replace($$, 'varray', 'vec') |> Str\replace($$, 'darray', 'dict') - |> Str\replace($$, 'array', 'shape'), + |> Str\replace($$, 'array', 'shape') + |> Str\replace($$, 'vec ', 'vec') + |> Str\replace($$, 'dict ', 'dict') + |> Str\replace($$, 'shape ', 'shape') ) |> Str\join($$, ', '), ); diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 6f400f9..334f974 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -60,15 +60,6 @@ interface User { <> public function isActive(): bool; - - <> - public function getRoles(): vec; -} - -<> -enum Role: string { - ADMIN = 'ADMIN'; - STAFF = 'STAFF'; } abstract class BaseUser implements User { @@ -78,7 +69,6 @@ abstract class BaseUser implements User { 'name' => string, 'team_id' => int, 'is_active' => bool, - ?'roles' => vec, ) $data, ) {} @@ -97,10 +87,6 @@ abstract class BaseUser implements User { public function isActive(): bool { return $this->data['is_active']; } - - public function getRoles(): vec { - return $this->data['roles'] ?? vec[]; - } } <> @@ -109,31 +95,6 @@ enum FavoriteColor: int { BLUE = 2; } -final class PermissionService { - private static ?PermissionService $service = null; - - public static function getInstance(): PermissionService { - if (static::$service is null) { - static::$service = new self(); - } - return static::$service as nonnull; - } - - private User $user; - - public function __construct(?User $user = null) { - $this->user = new Human(shape('id' => 1, 'name' => 'foo', 'team_id' => 1, 'is_active' => true)); - } - - public function getCurrentUser(): User { - return $this->user; - } - - public function setCurrentUser(User $user): void { - $this->user = $user; - } -} - <> final class Human extends BaseUser { << diff --git a/tests/gen/Bot.hack b/tests/gen/Bot.hack index 178097e..57cdd89 100644 --- a/tests/gen/Bot.hack +++ b/tests/gen/Bot.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1567641b10047f4bea25dcbba5c438dc>> + * @generated SignedSource<<82fd88d11578d2accf0fd7734c729e03>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -20,7 +20,6 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { 'is_active', 'name', 'primary_function', - 'roles', 'team', ]; const dict> INTERFACES = dict[ @@ -63,14 +62,6 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> $parent->getPrimaryFunction(), vec[], ); - case 'roles': - return new GraphQL\FieldDefinition( - 'roles', - Role::nonNullable()->nullableOutputListOf(), - dict[], - async ($parent, $args, $vars) ==> $parent->getRoles(), - vec[], - ); case 'team': return new GraphQL\FieldDefinition( 'team', diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index 3bb5fa4..2ad72e0 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<5456f22b07464c25859ffcdd506754a6>> + * @generated SignedSource<<1fa3be81354e83f33011553aedd901c7>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -22,7 +22,6 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { 'is_active', 'name', 'named_friends', - 'roles', 'team', ]; const dict> INTERFACES = dict[ @@ -40,14 +39,15 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), vec[ - new \Directives\HasRole(vec [ + new \Directives\HasRole(vec[ 'STAFF', ]), new \Directives\LogSampled(33.3, 'foo'), - new \Directives\TestShapeDirective(shape ( + new \Directives\TestShapeDirective(shape( 'foo' => 1, 'bar' => 'abc', ), true), + new \Directives\AnotherFieldDirective(), ], ); case 'friends': @@ -148,14 +148,6 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { ), vec[], ); - case 'roles': - return new GraphQL\FieldDefinition( - 'roles', - Role::nonNullable()->nullableOutputListOf(), - dict[], - async ($parent, $args, $vars) ==> $parent->getRoles(), - vec[], - ); case 'team': return new GraphQL\FieldDefinition( 'team', diff --git a/tests/gen/Role.hack b/tests/gen/Role.hack deleted file mode 100644 index 3bbd6c8..0000000 --- a/tests/gen/Role.hack +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This file is generated. Do not modify it manually! - * - * To re-generate this file run vendor/bin/hacktest - * - * - * @generated SignedSource<<78baf0213f0d9e14e803297f781e40f4>> - */ -namespace Slack\GraphQL\Test\Generated; -use namespace Slack\GraphQL; -use namespace Slack\GraphQL\Types; -use namespace HH\Lib\{C, Dict}; - -final class Role extends \Slack\GraphQL\Types\EnumType { - - const NAME = 'Role'; - const type THackType = \Role; - const \HH\enumname HACK_ENUM = \Role::class; - const vec ENUM_VALUES = vec[ - shape( - 'name' => 'ADMIN', - 'isDeprecated' => false, - ), - shape( - 'name' => 'STAFF', - 'isDeprecated' => false, - ), - ]; -} diff --git a/tests/gen/Schema.hack b/tests/gen/Schema.hack index b7ff1d0..24ca744 100644 --- a/tests/gen/Schema.hack +++ b/tests/gen/Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1580f1b40c1aff38c6937d089b8198b0>> + * @generated SignedSource<<024a37e7259d3742ec2b1d7c931d239b>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,7 +46,6 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'OutputTypeTestObj' => OutputTypeTestObj::class, 'PageInfo' => PageInfo::class, 'Query' => Query::class, - 'Role' => Role::class, 'String' => Types\StringType::class, 'StringTypeEdge' => StringTypeEdge::class, 'Team' => Team::class, diff --git a/tests/gen/User.hack b/tests/gen/User.hack index a4a3b5a..1cfb64e 100644 --- a/tests/gen/User.hack +++ b/tests/gen/User.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -19,7 +19,6 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { 'id', 'is_active', 'name', - 'roles', 'team', ]; const keyset> POSSIBLE_TYPES = keyset[ @@ -55,14 +54,6 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { async ($parent, $args, $vars) ==> $parent->getName(), vec[], ); - case 'roles': - return new GraphQL\FieldDefinition( - 'roles', - Role::nonNullable()->nullableOutputListOf(), - dict[], - async ($parent, $args, $vars) ==> $parent->getRoles(), - vec[], - ); case 'team': return new GraphQL\FieldDefinition( 'team', From 355b1412fe3c4b358cc7a5d41d7b0c9f2aeffac8 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Tue, 28 Sep 2021 11:09:03 -0700 Subject: [PATCH 07/11] require directives to be registered; do not use HHAST --- src/Codegen/Builders/Fields/FieldBuilder.hack | 12 +- .../IntrospectionSchemaFieldBuilder.hack | 4 +- .../Fields/IntrospectionTypeFieldBuilder.hack | 2 +- .../Builders/Fields/MethodFieldBuilder.hack | 2 +- .../Builders/Fields/ShapeFieldBuilder.hack | 2 +- src/Codegen/Builders/ObjectBuilder.hack | 8 +- src/Codegen/DirectivesFinder.hack | 133 ++++-------------- src/Codegen/FieldResolver.hack | 2 +- src/Codegen/Generator.hack | 18 +-- src/Directive.hack | 8 +- src/FieldDefinition.hack | 2 +- tests/FixtureTest.hack | 8 ++ tests/Fixtures/Directives.hack | 10 +- tests/gen/Human.hack | 4 +- 14 files changed, 76 insertions(+), 139 deletions(-) diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index 0ef7437..fcc7661 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -14,7 +14,7 @@ abstract class FieldBuilder { abstract const type TField as shape( 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), - 'directives' => vec, + 'directives' => dict>, ... ); @@ -32,7 +32,7 @@ abstract class FieldBuilder { public static function fromReflectionMethod( \Slack\GraphQL\Field $field, \ReflectionMethod $rm, - vec $directives, + dict> $directives, bool $is_root_field = false, ): FieldBuilder { $data = shape( @@ -78,7 +78,7 @@ abstract class FieldBuilder { 'name' => $name, 'output_type' => output_type(type_structure_to_type_alias($ts), false), 'is_optional' => Shapes::idx($ts, 'optional_shape_field') ?? false, - 'directives' => vec[], + 'directives' => dict[], )); } @@ -86,7 +86,7 @@ abstract class FieldBuilder { * Construct a top-level GraphQL field. */ public static function forRootField(\Slack\GraphQL\Field $field, \ReflectionMethod $rm): FieldBuilder { - return FieldBuilder::fromReflectionMethod($field, $rm, vec[], true); + return FieldBuilder::fromReflectionMethod($field, $rm, dict[], true); } public static function introspectSchemaField(): FieldBuilder { @@ -134,8 +134,8 @@ abstract class FieldBuilder { if ($this->data['directives']) { $hb->addLine('vec[') ->indent(); - foreach ($this->data['directives'] as $directive) { - $hb->addLine($directive.','); + foreach ($this->data['directives'] as $directive => $arguments) { + $hb->addLinef('new \%s(%s),', $directive, Str\join($arguments, ', ')); } $hb->unindent()->addLine('],'); } else { diff --git a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack index 2ed697f..9fbaa81 100644 --- a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack @@ -8,7 +8,7 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder { const type TField = shape( 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), - 'directives' => vec, + 'directives' => dict>, ... ); @@ -16,7 +16,7 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder { parent::__construct(shape( 'name' => '__schema', 'output_type' => shape('type' => '__Schema::nullableOutput()'), - 'directives' => vec[], + 'directives' => dict[], )); } diff --git a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack index b8da570..dcf80be 100644 --- a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack @@ -15,7 +15,7 @@ final class IntrospectTypeFieldBuilder extends MethodFieldBuilder { 'parameters' => vec[ shape('name' => 'name', 'type' => 'HH\string', 'is_optional' => false), ], - 'directives' => vec[], + 'directives' => dict[], )); } <<__Override>> diff --git a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack index 8ea97f4..fb9f051 100644 --- a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack @@ -33,7 +33,7 @@ class MethodFieldBuilder extends FieldBuilder { 'method_name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), 'parameters' => vec, - 'directives' => vec, + 'directives' => dict>, ?'root_field_for_type' => string, ?'is_static' => bool, ); diff --git a/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack b/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack index 38bfae3..35b08e8 100644 --- a/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/ShapeFieldBuilder.hack @@ -16,7 +16,7 @@ final class ShapeFieldBuilder extends FieldBuilder { 'name' => string, 'output_type' => shape('type' => string, ?'needs_await' => bool), 'is_optional' => bool, - 'directives' => vec, + 'directives' => dict>, ); <<__Override>> diff --git a/src/Codegen/Builders/ObjectBuilder.hack b/src/Codegen/Builders/ObjectBuilder.hack index 3d226ff..0f0f1b4 100644 --- a/src/Codegen/Builders/ObjectBuilder.hack +++ b/src/Codegen/Builders/ObjectBuilder.hack @@ -75,14 +75,14 @@ class ObjectBuilder extends CompositeBuilder { 'needs_await' => true, ), 'parameters' => vec[], - 'directives' => vec[], + 'directives' => dict[], )), new MethodFieldBuilder(shape( 'name' => 'pageInfo', 'method_name' => 'getPageInfo', 'output_type' => shape('type' => 'PageInfo::nullableOutput()', 'needs_await' => true), 'parameters' => vec[], - 'directives' => vec[], + 'directives' => dict[], )), ], dict[], // Connections do not implement any interfaces @@ -101,14 +101,14 @@ class ObjectBuilder extends CompositeBuilder { 'method_name' => 'getNode', 'output_type' => shape('type' => $output_type.'::nullableOutput()'), 'parameters' => vec[], - 'directives' => vec[], + 'directives' => dict[], )), new MethodFieldBuilder(shape( 'name' => 'cursor', 'method_name' => 'getCursor', 'output_type' => shape('type' => 'Types\StringType::nullableOutput()'), 'parameters' => vec[], - 'directives' => vec[], + 'directives' => dict[], )), ], dict[], diff --git a/src/Codegen/DirectivesFinder.hack b/src/Codegen/DirectivesFinder.hack index 4b568c0..e103694 100644 --- a/src/Codegen/DirectivesFinder.hack +++ b/src/Codegen/DirectivesFinder.hack @@ -1,118 +1,47 @@ + + namespace Slack\GraphQL\Codegen; use namespace HH\Lib\{C, Str, Vec}; use type Facebook\DefinitionFinder\ScannedMethod; final class DirectivesFinder { - public function __construct(private keyset $directives) {} - - public async function findDirectivesForField(ScannedMethod $method): Awaitable> { - if (!$this->directives) { - return vec[]; - } - - $directives = vec[]; - - // Get all use namespaces and types present - $file_path = $method->getFileName(); - $use_decls = await self::getUseDeclarations($file_path); - - // For each attribute, see if it matches a user-defined directive - // This requires building the qualified name, which is not - // available in `getAttributes`, unfortunately. - $attributes = $method->getAttributes(); - foreach ($attributes as $attribute => $args) { - // Consider names starting with `\` to be fully qualified - if (!Str\starts_with($attribute, '\\')) { - // - // Try to determine the fully qualified name of the attribute. - // - // There are two cases here: - // If the attribute is declared as `Baz`, then either: - // 1) Baz is not namespaced at all. - // 2) Baz is a imported via a use type statement, e.g., `use type Foo\Bar\Baz`. - // If the attribute is declared as `Bar/Baz`, then either: - // 1) Bar is a top-level namespace. - // 2) Bar is imported via a use namespace statement, e.g., `use namespace Foo\Bar`. - // - // In each case, we need to match the whatever comes before the first `\` in the attribute - // name (or the whole attribute name, if it contains no slashes) with whatever comes after - // the last `\` in each use type or use namespace statement. - // - // Once we have a match, we can build the fully qualified name by adding the attribute name - // to everything that comes before the last `\` in the eligible use statement. - // - $needle = Str\split($attribute, '\\') - |> C\firstx($$); - $attribute = '\\'.$attribute; - foreach ($use_decls as $decl) { - if (C\contains_key($decl['leaves'], $needle)) { - // We have a match such as `Bar\Baz` with `use namespace Foo\Bar`; - // build the qualified name as `Foo\Bar\Baz`. - $attribute = '\\'.$decl['root'].$attribute; - break; - } - } - } - - if (C\contains_key($this->directives, $attribute)) { - $directives[] = Str\format( - 'new %s(%s)', - $attribute, - Vec\map( - $args, - $arg ==> \var_export($arg, true) - |> Str\replace($$, 'varray', 'vec') + public function __construct( + private shape( + ?'fields' => vec>, + ?'objects' => vec>, + ) $directives, + ) {} + + public async function findDirectivesForField(\ReflectionMethod $rm): Awaitable>> { + $directives = dict[]; + + $custom_directive_types = $this->directives['fields'] ?? vec[]; + foreach ($custom_directive_types as $directive_type) { + $directive = $rm->getAttributeClass($directive_type); + if ($directive is nonnull) { + $rc = new \ReflectionClass($directive); + $constructor = $rc->getMethod('__construct'); + $arguments = vec[]; + if ($constructor) { + $parameters = $constructor->getParameters(); + foreach ($parameters as $parameter) { + $value = $rc->getProperty($parameter->getName()); + $value->setAccessible(true); + $arguments[] = \var_export($value->getValue($directive), true) |> Str\replace($$, 'darray', 'dict') + |> Str\replace($$, 'varray', 'vec') |> Str\replace($$, 'array', 'shape') - |> Str\replace($$, 'vec ', 'vec') |> Str\replace($$, 'dict ', 'dict') - |> Str\replace($$, 'shape ', 'shape') - ) - |> Str\join($$, ', '), - ); + |> Str\replace($$, 'vec ', 'vec') + |> Str\replace($$, 'shape ', 'shape'); + } + } + $directives[$rc->getName()] = $arguments; } } return $directives; } - - <<__Memoize>> - private static async function getUseDeclarations(string $file_path): Awaitable string, - 'leaves' => keyset, - )>> { - $ast = await \Facebook\HHAST\from_file_async(\Facebook\HHAST\File::fromPath($file_path)); - return $ast->getDescendantsByType<\Facebook\HHAST\INamespaceUseDeclaration>() - |> Vec\map($$, $decl ==> { - $kind = $decl->getKind(); - if ($kind is null || $kind->getText() === 'function') { - return null; - } - $leaves = $decl->getClausesx()->toVec(); - if ($decl is \Facebook\HHAST\NamespaceGroupUseDeclaration) { - $root = $decl->getPrefixx()->getCode() - |> Str\strip_suffix($$, '\\'); - $leaves = Vec\map($leaves, $leaf ==> $leaf->getFirstToken()?->getCode()) - |> Vec\filter_nulls($$); - } else { - $leaves = C\firstx($leaves) - |> Str\split($$->getCode(), '\\'); - $num_leaves = C\count($leaves); - if ($num_leaves > 1) { - $root = Vec\take($leaves, $num_leaves - 1) - |> Str\join($$, '\\'); - $leaves = Vec\drop($leaves, $num_leaves - 1); - } else { - $root = ''; - } - } - return shape( - 'root' => $root, - 'leaves' => keyset($leaves), - ); - }) - |> Vec\filter_nulls($$); - } } diff --git a/src/Codegen/FieldResolver.hack b/src/Codegen/FieldResolver.hack index 95fa299..b230914 100644 --- a/src/Codegen/FieldResolver.hack +++ b/src/Codegen/FieldResolver.hack @@ -76,7 +76,7 @@ final class FieldResolver { $graphql_field = $rm->getAttributeClass(\Slack\GraphQL\Field::class); if ($graphql_field is null) continue; - $directives = await $this->directives_finder->findDirectivesForField($method); + $directives = await $this->directives_finder->findDirectivesForField($rm); $fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($graphql_field, $rm, $directives); } diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index b51e4d2..cf838d0 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -31,6 +31,10 @@ final class Generator { 'output_directory' => string, 'namespace' => string, ?'codegen_config' => IHackCodegenConfig, + ?'custom_directives' => shape( + ?'fields' => vec>, + ?'objects' => vec>, + ), ); private function __construct(private MultiParser $parser, private self::TGeneratorConfig $config) { @@ -60,8 +64,7 @@ final class Generator { } public async function generate(): Awaitable { - $custom_directives = $this->collectDirectives(); - $directives_finder = new DirectivesFinder($custom_directives); + $directives_finder = new DirectivesFinder($this->config['custom_directives'] ?? shape()); $objects = await $this->collectObjects($directives_finder); self::removeDirectory($this->config['output_directory']); @@ -345,15 +348,4 @@ final class Generator { ObjectBuilder::forEdge($type_info['gql_type'], $type_info['hack_type'], $type_info['output_type']), ]; } - - private function collectDirectives(): keyset { - $custom_directives = keyset[]; - foreach ($this->parser->getClasses() as $class) { - $rc = new \ReflectionClass($class->getName()); - if (C\contains($rc->getInterfaceNames(), \Slack\GraphQL\Directive::class)) { - $custom_directives[] = '\\'.$rc->getName(); - } - } - return $custom_directives; - } } diff --git a/src/Directive.hack b/src/Directive.hack index ccdce41..dc216e8 100644 --- a/src/Directive.hack +++ b/src/Directive.hack @@ -1,8 +1,14 @@ + + namespace Slack\GraphQL; interface Directive {} interface FieldDirective extends Directive, \HH\MethodAttribute { - public function beforeResolve(IFieldDefinition $field): Awaitable; + public function beforeResolveField(IFieldDefinition $field): Awaitable; +} + +interface ObjectDirective extends Directive, \HH\ClassAttribute { + public function beforeResolveObject(): Awaitable; } diff --git a/src/FieldDefinition.hack b/src/FieldDefinition.hack index 27a8f5c..d22f64b 100644 --- a/src/FieldDefinition.hack +++ b/src/FieldDefinition.hack @@ -40,7 +40,7 @@ final class FieldDefinition implements IResolvableFiel $resolver = $this->resolver; try { foreach ($this->directives as $directive) { - await $directive->beforeResolve($this); + await $directive->beforeResolveField($this); } $value = await $resolver( $parent, diff --git a/tests/FixtureTest.hack b/tests/FixtureTest.hack index 256e55a..fcbe636 100644 --- a/tests/FixtureTest.hack +++ b/tests/FixtureTest.hack @@ -24,6 +24,14 @@ abstract class FixtureTest extends \Facebook\HackTest\HackTest { shape( 'output_directory' => __DIR__.'/gen', 'namespace' => 'Slack\GraphQL\Test\Generated', + 'custom_directives' => shape( + 'fields' => vec[ + Directives\AnotherFieldDirective::class, + Directives\HasRole::class, + Directives\LogSampled::class, + Directives\TestShapeDirective::class, + ] + ) ), ); echo "Generated fixtures in: ".(microtime(true) - $start_ts)."\n"; diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index c72349d..70e6d96 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -1,32 +1,34 @@ + + namespace Directives; use namespace Slack\GraphQL; final class HasRole implements \Slack\GraphQL\FieldDirective { public function __construct(private vec $roles) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { echo "HasRole\n"; } } final class LogSampled implements \Slack\GraphQL\FieldDirective { public function __construct(private float $frequency, private string $prefix) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { echo "LogSampled\n"; } } final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { echo "TestShapeDirective\n"; } } final class AnotherFieldDirective implements \Slack\GraphQL\FieldDirective { public function __construct() {} - public async function beforeResolve(GraphQL\IFieldDefinition $field): Awaitable { + public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { echo "AnotherFieldDirective\n"; } } diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index 2ad72e0..d27006c 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1fa3be81354e83f33011553aedd901c7>> + * @generated SignedSource<<4aff110e85a01b651fda1013e243938c>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,6 +39,7 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), vec[ + new \Directives\AnotherFieldDirective(), new \Directives\HasRole(vec[ 'STAFF', ]), @@ -47,7 +48,6 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { 'foo' => 1, 'bar' => 'abc', ), true), - new \Directives\AnotherFieldDirective(), ], ); case 'friends': From 407706826be4a837c38259b45f362476e2e87539 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Tue, 28 Sep 2021 15:15:01 -0700 Subject: [PATCH 08/11] object directives --- composer.json | 1 + src/Codegen/Builders/CompositeBuilder.hack | 15 +++ src/Codegen/Builders/DirectivesBuilder.hack | 25 +++++ src/Codegen/Builders/Fields/FieldBuilder.hack | 18 ++-- src/Codegen/Builders/InterfaceBuilder.hack | 3 +- src/Codegen/Builders/ObjectBuilder.hack | 20 ++-- src/Codegen/DirectivesFinder.hack | 62 +++++++----- src/Codegen/FieldResolver.hack | 18 ++-- src/Codegen/Generator.hack | 37 +++++--- src/Codegen/Types.hack | 19 +++- src/Directive.hack | 37 +++++++- src/FieldDefinition.hack | 15 ++- src/Types/InputOutput/Scalar/FloatType.hack | 37 ++++++++ src/Types/Output/CompositeType.hack | 8 ++ tests/BaseTest.hack | 56 +++++++++++ tests/DirectiveContext.hack | 49 ++++++++++ tests/DirectiveTest.hack | 95 +++++++++++++++++++ tests/FixtureTest.hack | 43 +-------- tests/Fixtures/Directives.hack | 70 +++++++++++--- tests/Fixtures/Playground.hack | 12 ++- tests/Validation/BaseValidationTest.hack | 18 +--- tests/gen/AlphabetConnection.hack | 6 +- tests/gen/AnotherObjectShape.hack | 6 +- tests/gen/Bot.hack | 8 +- tests/gen/Concrete.hack | 6 +- tests/gen/ErrorTestObj.hack | 6 +- tests/gen/FooInterface.hack | 6 +- tests/gen/FooObject.hack | 6 +- tests/gen/Human.hack | 23 +++-- tests/gen/IIntrospectionInterfaceA.hack | 6 +- tests/gen/IIntrospectionInterfaceB.hack | 6 +- tests/gen/IIntrospectionInterfaceC.hack | 6 +- tests/gen/ImplementInterfaceA.hack | 6 +- tests/gen/ImplementInterfaceB.hack | 6 +- tests/gen/ImplementInterfaceC.hack | 6 +- tests/gen/InterfaceA.hack | 6 +- tests/gen/InterfaceB.hack | 6 +- tests/gen/IntrospectionTestObject.hack | 6 +- tests/gen/Mutation.hack | 6 +- tests/gen/NestedOutputShape.hack | 6 +- tests/gen/ObjectShape.hack | 6 +- tests/gen/OutputShape.hack | 6 +- tests/gen/OutputTypeTestObj.hack | 6 +- tests/gen/PageInfo.hack | 6 +- tests/gen/Query.hack | 6 +- tests/gen/Schema.hack | 3 +- tests/gen/StringTypeEdge.hack | 6 +- tests/gen/Team.hack | 6 +- tests/gen/User.hack | 8 +- tests/gen/UserConnection.hack | 6 +- tests/gen/UserEdge.hack | 6 +- tests/gen/__Directive.hack | 6 +- tests/gen/__EnumValue.hack | 6 +- tests/gen/__Field.hack | 6 +- tests/gen/__InputValue.hack | 6 +- tests/gen/__Schema.hack | 6 +- tests/gen/__Type.hack | 6 +- tests/schema.json | 9 ++ 58 files changed, 709 insertions(+), 192 deletions(-) create mode 100644 src/Codegen/Builders/DirectivesBuilder.hack create mode 100644 src/Types/InputOutput/Scalar/FloatType.hack create mode 100644 tests/BaseTest.hack create mode 100644 tests/DirectiveContext.hack create mode 100644 tests/DirectiveTest.hack diff --git a/composer.json b/composer.json index 478b38d..0cc21b5 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ ], "require": { "hhvm/hsl": "^4.41.0", + "hhvm/type-assert": "=4.2.0", "facebook/hack-codegen": "^v4.3.12", "facebook/definition-finder": "^v2.13.5" }, diff --git a/src/Codegen/Builders/CompositeBuilder.hack b/src/Codegen/Builders/CompositeBuilder.hack index d92ab8b..2f28dd5 100644 --- a/src/Codegen/Builders/CompositeBuilder.hack +++ b/src/Codegen/Builders/CompositeBuilder.hack @@ -19,11 +19,13 @@ use type Facebook\HackCodegen\{ * The annotated Hack type should be either a class, interface, or shape. */ abstract class CompositeBuilder extends OutputTypeBuilder<\Slack\GraphQL\__Private\CompositeType> { + use DirectivesBuilder; public function __construct( \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, protected vec $fields, + protected dict> $directives, ) { parent::__construct($type_info, $hack_type); } @@ -31,6 +33,7 @@ abstract class CompositeBuilder extends OutputTypeBuilder<\Slack\GraphQL\__Priva public function build(HackCodegenFactory $cg): CodegenClass { return parent::build($cg) ->addMethod($this->generateGetFieldDefinition($cg)) + ->addMethod($this->generateGetDirectives($cg)) ->addConstant($this->generateFieldNamesConstant($cg, $this->getFieldNames())); } @@ -58,6 +61,18 @@ abstract class CompositeBuilder extends OutputTypeBuilder<\Slack\GraphQL\__Priva return $method; } + private function generateGetDirectives(HackCodegenFactory $cg): CodegenMethod { + $hb = hb($cg); + $hb->add('return '); + $hb = $this->buildDirectives($hb); + $hb->addLine(';'); + + return $cg->codegenMethod('getDirectives') + ->setPublic() + ->setReturnType('vec') + ->setBody($hb->getCode()); + } + final public function getFieldNames(): keyset { return Keyset\map($this->fields, $field ==> $field->getName()) |> Keyset\filter($$, $name ==> !Str\starts_with($name, '__')); diff --git a/src/Codegen/Builders/DirectivesBuilder.hack b/src/Codegen/Builders/DirectivesBuilder.hack new file mode 100644 index 0000000..01bc8c6 --- /dev/null +++ b/src/Codegen/Builders/DirectivesBuilder.hack @@ -0,0 +1,25 @@ + + + +namespace Slack\GraphQL\Codegen; + +use namespace HH\Lib\Str; +use type Facebook\HackCodegen\HackBuilder; + +trait DirectivesBuilder { + protected dict> $directives; + + protected function buildDirectives(HackBuilder $hb): HackBuilder { + if ($this->directives) { + $hb->addLine('vec[') + ->indent(); + foreach ($this->directives as $directive => $arguments) { + $hb->addLinef('new \%s(%s),', $directive, Str\join($arguments, ', ')); + } + $hb->unindent()->add(']'); + } else { + $hb->add('vec[]'); + } + return $hb; + } +} diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index fcc7661..0f27502 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -10,6 +10,7 @@ use type Facebook\HackCodegen\{HackBuilder, HackBuilderValues}; * Base builder for constructing GraphQL fields. */ abstract class FieldBuilder { + use DirectivesBuilder; abstract const type TField as shape( 'name' => string, @@ -24,7 +25,11 @@ abstract class FieldBuilder { // Constructors - public function __construct(protected this::TField $data) {} + protected dict> $directives; + + public function __construct(protected this::TField $data) { + $this->directives = $data['directives']; + } /** * Construct a GraphQL field from a Hack method. @@ -131,16 +136,7 @@ abstract class FieldBuilder { $hb->addLine(','); // Field directives - if ($this->data['directives']) { - $hb->addLine('vec[') - ->indent(); - foreach ($this->data['directives'] as $directive => $arguments) { - $hb->addLinef('new \%s(%s),', $directive, Str\join($arguments, ', ')); - } - $hb->unindent()->addLine('],'); - } else { - $hb->addLine('vec[],'); - } + $hb = $this->buildDirectives($hb)->addLine(','); // End of new GraphQL\FieldDefinition( $hb->unindent()->addLine(');'); diff --git a/src/Codegen/Builders/InterfaceBuilder.hack b/src/Codegen/Builders/InterfaceBuilder.hack index 5becea0..1c84c7a 100644 --- a/src/Codegen/Builders/InterfaceBuilder.hack +++ b/src/Codegen/Builders/InterfaceBuilder.hack @@ -20,9 +20,10 @@ final class InterfaceBuilder extends CompositeBuilder { \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, vec $fields, + dict> $directives, private dict $hack_class_to_graphql_object, ) { - parent::__construct($type_info, $hack_type, $fields); + parent::__construct($type_info, $hack_type, $fields, $directives); } <<__Override>> diff --git a/src/Codegen/Builders/ObjectBuilder.hack b/src/Codegen/Builders/ObjectBuilder.hack index 0f0f1b4..fb4072f 100644 --- a/src/Codegen/Builders/ObjectBuilder.hack +++ b/src/Codegen/Builders/ObjectBuilder.hack @@ -20,9 +20,10 @@ class ObjectBuilder extends CompositeBuilder { \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, vec $fields, + dict> $directives, private dict $hack_class_to_graphql_interface, ) { - parent::__construct($type_info, $hack_type, $fields); + parent::__construct($type_info, $hack_type, $fields, $directives); } <<__Override>> @@ -33,10 +34,10 @@ class ObjectBuilder extends CompositeBuilder { private function generateInterfacesConstant(HackCodegenFactory $cg): CodegenClassConstant { $interfaces = dict[]; - foreach ($this->hack_class_to_graphql_interface as $hack_class => $graphql_type) { - if (\is_subclass_of($this->hack_type, $hack_class)) { - $interfaces[$graphql_type] = Str\format('%s::class', $graphql_type); - } + foreach ( + get_interfaces($this->hack_type, $this->hack_class_to_graphql_interface) as $hack_class => $graphql_type + ) { + $interfaces[$graphql_type] = Str\format('%s::class', $graphql_type); } return $cg->codegenClassConstant('INTERFACES') @@ -58,11 +59,16 @@ class ObjectBuilder extends CompositeBuilder { $type_info, $type_alias->getName(), Vec\map_with_key($ts['fields'], ($name, $ts) ==> FieldBuilder::fromShapeField($name, $ts)), + dict[], // No directives (maybe we'll support them later) dict[], // Objects generated from shapes cannot implement interfaces ); } - public static function forConnection(string $name, string $edge_name): ObjectBuilder { + public static function forConnection( + string $name, + string $edge_name, + dict> $directives, + ): ObjectBuilder { return new ObjectBuilder( new \Slack\GraphQL\ObjectType($name, $name), // TODO: Description $name, // hack type @@ -85,6 +91,7 @@ class ObjectBuilder extends CompositeBuilder { 'directives' => dict[], )), ], + $directives, dict[], // Connections do not implement any interfaces ); } @@ -111,6 +118,7 @@ class ObjectBuilder extends CompositeBuilder { 'directives' => dict[], )), ], + dict[], // If we support custom edges, we'll want to support adding directives to them dict[], ); } diff --git a/src/Codegen/DirectivesFinder.hack b/src/Codegen/DirectivesFinder.hack index e103694..4279274 100644 --- a/src/Codegen/DirectivesFinder.hack +++ b/src/Codegen/DirectivesFinder.hack @@ -3,42 +3,56 @@ namespace Slack\GraphQL\Codegen; -use namespace HH\Lib\{C, Str, Vec}; -use type Facebook\DefinitionFinder\ScannedMethod; +use namespace HH\Lib\{C, Dict, Str, Vec}; final class DirectivesFinder { public function __construct( private shape( - ?'fields' => vec>, - ?'objects' => vec>, + ?'fields' => keyset>, + ?'objects' => keyset>, ) $directives, + private dict $hack_class_to_graphql_interface, ) {} - public async function findDirectivesForField(\ReflectionMethod $rm): Awaitable>> { + public function findDirectivesForField(\ReflectionMethod $rm): dict> { + return self::findDirectives( + $this->directives['fields'] ?? keyset[], + $directive_type ==> $rm->getAttributeClass($directive_type), + ); + } + + public function findDirectivesForObject(\ReflectionClass $rc): dict> { + return vec[$rc->getName()] + |> Vec\concat( + $$, + get_interfaces($rc->getName(), $this->hack_class_to_graphql_interface) + |> Vec\keys($$), + ) + |> Vec\reverse($$) + |> Vec\map($$, $object_name ==> $this->findDirectivesForObjectName($object_name)) + |> Dict\merge(C\firstx($$), ...Vec\drop($$, 1)); + } + + <<__Memoize>> + private function findDirectivesForObjectName(string $object_name): dict> { + $rc = new \ReflectionClass($object_name); + return self::findDirectives( + $this->directives['objects'] ?? keyset[], + $directive_type ==> $rc->getAttributeClass($directive_type), + ); + } + + private static function findDirectives( + keyset> $custom_directive_types, + (function(classname): ?T) $getter, + ): dict> { $directives = dict[]; - $custom_directive_types = $this->directives['fields'] ?? vec[]; foreach ($custom_directive_types as $directive_type) { - $directive = $rm->getAttributeClass($directive_type); + $directive = $getter($directive_type); if ($directive is nonnull) { $rc = new \ReflectionClass($directive); - $constructor = $rc->getMethod('__construct'); - $arguments = vec[]; - if ($constructor) { - $parameters = $constructor->getParameters(); - foreach ($parameters as $parameter) { - $value = $rc->getProperty($parameter->getName()); - $value->setAccessible(true); - $arguments[] = \var_export($value->getValue($directive), true) - |> Str\replace($$, 'darray', 'dict') - |> Str\replace($$, 'varray', 'vec') - |> Str\replace($$, 'array', 'shape') - |> Str\replace($$, 'dict ', 'dict') - |> Str\replace($$, 'vec ', 'vec') - |> Str\replace($$, 'shape ', 'shape'); - } - } - $directives[$rc->getName()] = $arguments; + $directives[$rc->getName()] = $directive->formatArgs(); } } diff --git a/src/Codegen/FieldResolver.hack b/src/Codegen/FieldResolver.hack index b230914..4e567e9 100644 --- a/src/Codegen/FieldResolver.hack +++ b/src/Codegen/FieldResolver.hack @@ -21,10 +21,10 @@ final class FieldResolver { $this->scanned_classes = Dict\from_values($classes, $class ==> $class->getName()); } - public async function resolveFields(): Awaitable>> { + public function resolveFields(): dict> { foreach ($this->scanned_classes as $class) { if (!$this->shouldResolve($class)) continue; - await $this->resolveClass($class); + $this->resolveClass($class); } return Dict\map($this->resolved_fields, $fields ==> vec(Dict\sort_by_key($fields))); } @@ -37,9 +37,7 @@ final class FieldResolver { ) is nonnull; } - private async function resolveClass( - DefinitionFinder\ScannedClassish $class, - ): Awaitable> { + private function resolveClass(DefinitionFinder\ScannedClassish $class): dict { if (C\contains_key($this->resolved_fields, $class->getName())) { return $this->resolved_fields[$class->getName()]; } @@ -56,18 +54,16 @@ final class FieldResolver { foreach ($parents as $parent) { $parent_class = $this->scanned_classes[$parent] ?? null; if ($parent_class) { - $fields = Dict\merge($fields, await $this->resolveClass($parent_class)); + $fields = Dict\merge($fields, $this->resolveClass($parent_class)); } } - $fields = Dict\merge($fields, await $this->collectObjectFields($class)); + $fields = Dict\merge($fields, $this->collectObjectFields($class)); $this->resolved_fields[$class->getName()] = $fields; return $fields; } - private async function collectObjectFields( - DefinitionFinder\ScannedClassish $class, - ): Awaitable> { + private function collectObjectFields(DefinitionFinder\ScannedClassish $class): dict { $fields = dict[]; foreach ($class->getMethods() as $method) { if (C\is_empty($method->getAttributes())) continue; @@ -76,7 +72,7 @@ final class FieldResolver { $graphql_field = $rm->getAttributeClass(\Slack\GraphQL\Field::class); if ($graphql_field is null) continue; - $directives = await $this->directives_finder->findDirectivesForField($rm); + $directives = $this->directives_finder->findDirectivesForField($rm); $fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($graphql_field, $rm, $directives); } diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index cf838d0..7ccee6e 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -32,8 +32,8 @@ final class Generator { 'namespace' => string, ?'codegen_config' => IHackCodegenConfig, ?'custom_directives' => shape( - ?'fields' => vec>, - ?'objects' => vec>, + ?'fields' => keyset>, + ?'objects' => keyset>, ), ); @@ -64,8 +64,7 @@ final class Generator { } public async function generate(): Awaitable { - $directives_finder = new DirectivesFinder($this->config['custom_directives'] ?? shape()); - $objects = await $this->collectObjects($directives_finder); + $objects = await $this->collectObjects(); self::removeDirectory($this->config['output_directory']); \mkdir($this->config['output_directory']); @@ -200,7 +199,7 @@ final class Generator { return $resolve_method; } - private async function collectObjects(DirectivesFinder $directive_finder): Awaitable> { + private async function collectObjects(): Awaitable> { $objects = vec[]; $query_fields = dict[ '__schema' => FieldBuilder::introspectSchemaField(), @@ -210,9 +209,6 @@ final class Generator { $classish_objects = $this->parser->getClassishObjects(); - $field_resolver = new FieldResolver($classish_objects, $directive_finder); - $class_fields = await $field_resolver->resolveFields(); - $hack_class_to_graphql_interface = dict[]; $hack_class_to_graphql_object = dict[]; foreach ($classish_objects as $class) { @@ -232,6 +228,14 @@ final class Generator { $hack_class_to_graphql_object = Dict\sort_by_key($hack_class_to_graphql_object); $hack_class_to_graphql_interface = Dict\sort_by_key($hack_class_to_graphql_interface); + $directive_finder = new DirectivesFinder( + $this->config['custom_directives'] ?? shape(), + $hack_class_to_graphql_interface, + ); + + $field_resolver = new FieldResolver($classish_objects, $directive_finder); + $class_fields = $field_resolver->resolveFields(); + $input_types = $this->parser->getTypes(); foreach ($input_types as $type) { $rt = new \ReflectionTypeAlias($type->getName()); @@ -249,7 +253,7 @@ final class Generator { foreach ($classish_objects as $class) { if (Str\ends_with($class->getName(), 'Connection')) { // TODO: Assert that any class which subclasses Connection has a name ending in `Connection`. - $objects = Vec\concat($objects, $this->getConnectionObjects($class)); + $objects = Vec\concat($objects, $this->getConnectionObjects($class, $directive_finder)); } elseif (!C\is_empty($class->getAttributes())) { $rc = new \ReflectionClass($class->getName()); $fields = $class_fields[$class->getName()]; @@ -265,6 +269,7 @@ final class Generator { $graphql_interface, $rc->getName(), $fields, + $directive_finder->findDirectivesForObject($rc), $hack_class_to_graphql_object, ); } else if ($graphql_object is nonnull) { @@ -272,6 +277,7 @@ final class Generator { $graphql_object, $rc->getName(), $fields, + $directive_finder->findDirectivesForObject($rc), $hack_class_to_graphql_interface, ); } @@ -315,6 +321,7 @@ final class Generator { new \Slack\GraphQL\ObjectType('Query', 'Query'), 'Slack\\GraphQL\\Root', vec(Dict\sort_by_key($query_fields)), + dict[], $hack_class_to_graphql_interface, ); @@ -323,6 +330,7 @@ final class Generator { new \Slack\GraphQL\ObjectType('Mutation', 'Mutation'), 'Slack\\GraphQL\\Root', vec(Dict\sort_by_key($mutation_fields)), + dict[], $hack_class_to_graphql_interface, ); } @@ -330,7 +338,10 @@ final class Generator { return Vec\sort_by($objects, $object ==> $object->getGraphQLType()); } - private function getConnectionObjects(DefinitionFinder\ScannedClassish $class): vec { + private function getConnectionObjects( + DefinitionFinder\ScannedClassish $class, + DirectivesFinder $df, + ): vec { $rc = new \ReflectionClass($class->getName()); invariant(is_connection_type($rc), '"%s" must subclass "Connection"', $rc->getName()); $hack_type = $rc->getTypeConstants() @@ -344,7 +355,11 @@ final class Generator { $rc->getName(), ); return vec[ - ObjectBuilder::forConnection($class->getName(), $type_info['gql_type'].'Edge'), + ObjectBuilder::forConnection( + $class->getName(), + $type_info['gql_type'].'Edge', + $df->findDirectivesForObject($rc), + ), ObjectBuilder::forEdge($type_info['gql_type'], $type_info['hack_type'], $type_info['output_type']), ]; } diff --git a/src/Codegen/Types.hack b/src/Codegen/Types.hack index 5da9c42..c9b907e 100644 --- a/src/Codegen/Types.hack +++ b/src/Codegen/Types.hack @@ -3,11 +3,12 @@ namespace Slack\GraphQL\Codegen; -use namespace HH\Lib\Str; +use namespace HH\Lib\{Dict, Str}; use namespace Slack\GraphQL\Types; const dict> BUILTIN_TYPES = dict[ Types\IntType::NAME => Types\IntType::class, + Types\FloatType::NAME => Types\FloatType::class, Types\StringType::NAME => Types\StringType::class, Types\BooleanType::NAME => Types\BooleanType::class, ]; @@ -85,6 +86,8 @@ function get_graphql_leaf_type(string $hack_type): ?string { switch ($hack_type) { case 'HH\int': return Types\IntType::class; + case 'HH\float': + return Types\FloatType::class; case 'HH\string': return Types\StringType::class; case 'HH\bool': @@ -102,6 +105,10 @@ function get_graphql_leaf_type(string $hack_type): ?string { } } +function is_float_type(string $hack_type): bool { + return $hack_type === 'HH\float' || $hack_type === '?HH\float'; +} + /** * Get the GraphQL type to output for a hack type. */ @@ -257,3 +264,13 @@ function get_node_type_info(string $hack_type): ?shape( 'output_type' => $output_type, ); } + +function get_interfaces( + string $hack_type, + dict $hack_class_to_graphql_interface, +): dict { + return Dict\filter_with_key( + $hack_class_to_graphql_interface, + ($interface, $_gql_type) ==> \is_subclass_of($hack_type, $interface), + ); +} diff --git a/src/Directive.hack b/src/Directive.hack index dc216e8..eab0fd2 100644 --- a/src/Directive.hack +++ b/src/Directive.hack @@ -3,12 +3,43 @@ namespace Slack\GraphQL; -interface Directive {} +/** + * A custom schema directive. + */ +interface Directive { + /** + * Return the arguments necessary to reconstitute the directive by calling `new`. + * + * For example, if the directive has the following constructor: + * + * __construct(private float $x, private string $y) {} + * + * You'd implement `formatArgs()` as follows: + * + * return vec[Str\format("%f", $this->x), Str\format('"%s"', $this->y)]; + */ + public function formatArgs(): vec; +} + +/** + * A field-level directive. + */ interface FieldDirective extends Directive, \HH\MethodAttribute { - public function beforeResolveField(IFieldDefinition $field): Awaitable; + + /** + * Hook which executes before resolving a field. + */ + public function beforeResolveField(mixed $object, string $field): Awaitable; } +/** + * An object-level directive. + */ interface ObjectDirective extends Directive, \HH\ClassAttribute { - public function beforeResolveObject(): Awaitable; + + /** + * Hook which executes before resolving an object. + */ + public function beforeResolveObject(string $object_name): Awaitable; } diff --git a/src/FieldDefinition.hack b/src/FieldDefinition.hack index d22f64b..1a8c6da 100644 --- a/src/FieldDefinition.hack +++ b/src/FieldDefinition.hack @@ -39,9 +39,7 @@ final class FieldDefinition implements IResolvableFiel ): Awaitable> { $resolver = $this->resolver; try { - foreach ($this->directives as $directive) { - await $directive->beforeResolveField($this); - } + await $this->beforeResolve($parent); $value = await $resolver( $parent, // Validation guarantees all of the grouped field nodes have the same arguments, so it doesn't matter @@ -88,4 +86,15 @@ final class FieldDefinition implements IResolvableFiel public function getArgs(): vec { return vec($this->arguments); } + + private async function beforeResolve(TParent $parent): Awaitable { + foreach ($this->directives as $directive) { + await $directive->beforeResolveField($parent, $this->getName()); + } + + $unwrapped_type = $this->type->unwrapType(); + if ($unwrapped_type is Types\CompositeType) { + await $unwrapped_type->beforeResolveObject(); + } + } } diff --git a/src/Types/InputOutput/Scalar/FloatType.hack b/src/Types/InputOutput/Scalar/FloatType.hack new file mode 100644 index 0000000..861e344 --- /dev/null +++ b/src/Types/InputOutput/Scalar/FloatType.hack @@ -0,0 +1,37 @@ + + + +namespace Slack\GraphQL\Types; + +use namespace Graphpinator\Parser\Value; +use namespace Slack\GraphQL; + +final class FloatType extends ScalarType { + + const type THackType = float; + const string NAME = 'Float'; + + const int MIN_SAFE_VALUE = -2147483648; + const int MAX_SAFE_VALUE = 2147483647; + + <<__Override>> + public function coerceValue(mixed $value): float { + if (!$value is float) { + throw new GraphQL\UserFacingError('Expected a float, got %s', (string)$value); + } + return $value; + } + + <<__Override>> + final public function coerceNonVariableNode(Value\Value $node, dict $variable_values): float { + if (!$node is Value\FloatLiteral) { + throw new GraphQL\UserFacingError('Expected a Float literal, got %s', \get_class($node)); + } + return $node->getRawValue(); + } + + <<__Override>> + protected function serialize(float $value): float { + return $value; + } +} diff --git a/src/Types/Output/CompositeType.hack b/src/Types/Output/CompositeType.hack index d24c991..6e62349 100644 --- a/src/Types/Output/CompositeType.hack +++ b/src/Types/Output/CompositeType.hack @@ -16,8 +16,16 @@ abstract class CompositeType extends NamedType { abstract public function getFieldDefinition(string $field_name): ?GraphQL\IFieldDefinition; + abstract public function getDirectives(): vec; + <<__Override>> final public function getFields(bool $includeDeprecated = false): vec { return Vec\map($this::FIELD_NAMES, $field_name ==> $this->getFieldDefinition($field_name) as nonnull); } + + final public async function beforeResolveObject(): Awaitable { + foreach ($this->getDirectives() as $directive) { + await $directive->beforeResolveObject($this::NAME); + } + } } diff --git a/tests/BaseTest.hack b/tests/BaseTest.hack new file mode 100644 index 0000000..f320120 --- /dev/null +++ b/tests/BaseTest.hack @@ -0,0 +1,56 @@ + + + +use function Facebook\FBExpect\expect; +use namespace HH\Lib\C; +use namespace Slack\GraphQL; + +abstract class BaseTest extends \Facebook\HackTest\HackTest { + abstract const type TTestCases; + + abstract public static function getTestCases(): this::TTestCases; + + <<__Override>> + public static async function beforeFirstTestAsync(): Awaitable { + await self::runCodegenAsync(); + } + + <<__Memoize>> + private static async function runCodegenAsync(): Awaitable { + $start_ts = microtime(true); + await GraphQL\Codegen\Generator::forPath( + __DIR__.'/Fixtures', + shape( + 'output_directory' => __DIR__.'/gen', + 'namespace' => 'Slack\GraphQL\Test\Generated', + 'custom_directives' => shape( + 'fields' => keyset[ + Directives\AnotherDirective::class, + Directives\HasRole::class, + Directives\LogSampled::class, + Directives\TestShapeDirective::class, + ], + 'objects' => keyset[ + Directives\AnotherDirective::class, + Directives\HasRole::class, + Directives\LogSampled::class, + ], + ), + ), + ); + echo "Generated fixtures in: ".(microtime(true) - $start_ts)."\n"; + } + + private function getResolver(GraphQL\Resolver::TOptions $options = shape()): GraphQL\Resolver { + return new GraphQL\Resolver(new \Slack\GraphQL\Test\Generated\Schema(), $options); + } + + public async function resolve( + string $query, + dict $variables = dict[], + GraphQL\Resolver::TOptions $options = shape(), + ): Awaitable { + $resolver = $this->getResolver($options); + return await $resolver->resolve($query, $variables); + } +} diff --git a/tests/DirectiveContext.hack b/tests/DirectiveContext.hack new file mode 100644 index 0000000..195fcf9 --- /dev/null +++ b/tests/DirectiveContext.hack @@ -0,0 +1,49 @@ +use namespace HH\Lib\C; + +final class DirectiveContext { + public static dict>> $field_resolutions = dict[]; + public static dict> $object_resolutions = dict[]; + + public static function reset(): void { + self::$field_resolutions = dict[]; + self::$object_resolutions = dict[]; + } + + public static function incrementResolveField( + \Slack\GraphQL\FieldDirective $directive, + mixed $object, + string $field, + ): void { + $directive_class = \get_class($directive); + if (!C\contains_key(self::$field_resolutions, $directive_class)) { + self::$field_resolutions[$directive_class] = dict[]; + } + + $object_class = \get_class($object); + if (!C\contains_key(self::$field_resolutions[$directive_class], $object_class)) { + self::$field_resolutions[$directive_class][$object_class] = dict[]; + } + + if (!C\contains_key(self::$field_resolutions[$directive_class][$object_class], $field)) { + self::$field_resolutions[$directive_class][$object_class][$field] = 0; + } + + self::$field_resolutions[$directive_class][$object_class][$field]++; + } + + public static function incrementResolveObject( + \Slack\GraphQL\ObjectDirective $directive, + string $object_name, + ): void { + $directive_class = \get_class($directive); + if (!C\contains_key(self::$object_resolutions, $directive_class)) { + self::$object_resolutions[$directive_class] = dict[]; + } + + if (!C\contains_key(self::$object_resolutions[$directive_class], $object_name)) { + self::$object_resolutions[$directive_class][$object_name] = 0; + } + + self::$object_resolutions[$directive_class][$object_name]++; + } +} diff --git a/tests/DirectiveTest.hack b/tests/DirectiveTest.hack new file mode 100644 index 0000000..86f7982 --- /dev/null +++ b/tests/DirectiveTest.hack @@ -0,0 +1,95 @@ + + + +use function Facebook\FBExpect\expect; +use namespace HH\Lib\C; +use namespace Slack\GraphQL; + +final class DirectiveTest extends BaseTest { + + const type TExpectedResponse = shape( + 'field_resolutions' => dict>>, + 'object_resolutions' => dict>, + ); + + const type TTestCases = dict, this::TExpectedResponse)>; + + public static function getTestCases(): this::TTestCases { + return dict[ + 'runs the expected number of directives in the expected order' => tuple( + 'query { + human(id: 2) { + favorite_color + } + }', + dict[], + shape( + 'object_resolutions' => dict[ + \Directives\AnotherDirective::class => dict[ + 'Human' => 1, + ], + \Directives\HasRole::class => dict[ + 'Human' => 1, + ], + \Directives\LogSampled::class => dict[ + 'Human' => 1, + ], + ], + 'field_resolutions' => dict[ + \Directives\AnotherDirective::class => dict[ + 'Human' => dict[ + 'favorite_color' => 1, + ], + ], + \Directives\HasRole::class => dict[ + 'Human' => dict[ + 'favorite_color' => 1, + ], + ], + \Directives\LogSampled::class => dict[ + 'Human' => dict[ + 'favorite_color' => 1, + ], + ], + \Directives\TestShapeDirective::class => dict[ + 'Human' => dict[ + 'favorite_color' => 1, + ], + ], + ], + ), + ), + 'runs directives for an interface' => tuple( + 'query { + viewer { + id + } + }', + dict[], + shape( + 'object_resolutions' => dict[ + \Directives\AnotherDirective::class => dict[ + 'User' => 1, + ], + ], + 'field_resolutions' => dict[], + ), + ), + ]; + } + + public async function beforeEachTestAsync(): Awaitable { + DirectiveContext::reset(); + } + + <<\Facebook\HackTest\DataProvider('getTestCases')>> + final public async function test( + string $query, + dict $variables, + this::TExpectedResponse $expected_response, + ): Awaitable { + await $this->resolve($query, $variables); + expect(DirectiveContext::$field_resolutions)->toEqual($expected_response['field_resolutions']); + expect(DirectiveContext::$object_resolutions)->toEqual($expected_response['object_resolutions']); + } +} diff --git a/tests/FixtureTest.hack b/tests/FixtureTest.hack index fcbe636..4b7c0dd 100644 --- a/tests/FixtureTest.hack +++ b/tests/FixtureTest.hack @@ -5,38 +5,10 @@ use function Facebook\FBExpect\expect; use namespace HH\Lib\C; use namespace Slack\GraphQL; -abstract class FixtureTest extends \Facebook\HackTest\HackTest { +abstract class FixtureTest extends BaseTest { const type TTestCases = dict, mixed)>; - abstract public static function getTestCases(): this::TTestCases; - - <<__Override>> - public static async function beforeFirstTestAsync(): Awaitable { - await self::runCodegenAsync(); - } - - <<__Memoize>> - private static async function runCodegenAsync(): Awaitable { - $start_ts = microtime(true); - await GraphQL\Codegen\Generator::forPath( - __DIR__.'/Fixtures', - shape( - 'output_directory' => __DIR__.'/gen', - 'namespace' => 'Slack\GraphQL\Test\Generated', - 'custom_directives' => shape( - 'fields' => vec[ - Directives\AnotherFieldDirective::class, - Directives\HasRole::class, - Directives\LogSampled::class, - Directives\TestShapeDirective::class, - ] - ) - ), - ); - echo "Generated fixtures in: ".(microtime(true) - $start_ts)."\n"; - } - <<\Facebook\HackTest\DataProvider('getTestCases')>> final public async function test( string $query, @@ -53,17 +25,4 @@ abstract class FixtureTest extends \Facebook\HackTest\HackTest { $out = await $this->resolve($query, $variables); expect($out)->toHaveSameShapeAs($expected_response); } - - private function getResolver(GraphQL\Resolver::TOptions $options = shape()): GraphQL\Resolver { - return new GraphQL\Resolver(new \Slack\GraphQL\Test\Generated\Schema(), $options); - } - - public async function resolve( - string $query, - dict $variables = dict[], - GraphQL\Resolver::TOptions $options = shape(), - ): Awaitable { - $resolver = $this->getResolver($options); - return await $resolver->resolve($query, $variables); - } } diff --git a/tests/Fixtures/Directives.hack b/tests/Fixtures/Directives.hack index 70e6d96..6b4e28f 100644 --- a/tests/Fixtures/Directives.hack +++ b/tests/Fixtures/Directives.hack @@ -4,31 +4,75 @@ namespace Directives; use namespace Slack\GraphQL; +use namespace HH\Lib\{C, Str, Vec}; -final class HasRole implements \Slack\GraphQL\FieldDirective { - public function __construct(private vec $roles) {} - public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { - echo "HasRole\n"; +abstract class RoleType {} +final class AdminRoleType extends RoleType {} +final class StaffRoleType extends RoleType {} + +final class HasRole implements \Slack\GraphQL\FieldDirective, \Slack\GraphQL\ObjectDirective { + public function __construct(private vec> $roles) {} + + public function formatArgs(): vec { + return vec[Str\format( + 'vec[%s]', + Vec\map($this->roles, $role ==> Str\format('\%s::class', $role)) |> Str\join($$, ', '), + )]; + } + + public async function beforeResolveField(mixed $object, string $field): Awaitable { + \DirectiveContext::incrementResolveField($this, $object, $field); + } + + public async function beforeResolveObject(string $object_name): Awaitable { + \DirectiveContext::incrementResolveObject($this, $object_name); } } -final class LogSampled implements \Slack\GraphQL\FieldDirective { +final class LogSampled implements \Slack\GraphQL\FieldDirective, \Slack\GraphQL\ObjectDirective { public function __construct(private float $frequency, private string $prefix) {} - public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { - echo "LogSampled\n"; + + public function formatArgs(): vec { + return vec[ + Str\format('%f', $this->frequency), + Str\format('"%s"', $this->prefix), + ]; + } + + public async function beforeResolveField(mixed $object, string $field): Awaitable { + \DirectiveContext::incrementResolveField($this, $object, $field); + } + + public async function beforeResolveObject(string $object_name): Awaitable { + \DirectiveContext::incrementResolveObject($this, $object_name); } } final class TestShapeDirective implements \Slack\GraphQL\FieldDirective { public function __construct(private shape('foo' => int, 'bar' => string) $args, private bool $extra) {} - public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { - echo "TestShapeDirective\n"; + + public function formatArgs(): vec { + return vec[ + Str\format('shape(\'foo\' => %d, \'bar\' => "%s")', $this->args['foo'], $this->args['bar']), + $this->extra ? 'true' : 'false', + ]; + } + + public async function beforeResolveField(mixed $object, string $field): Awaitable { + \DirectiveContext::incrementResolveField($this, $object, $field); } } -final class AnotherFieldDirective implements \Slack\GraphQL\FieldDirective { - public function __construct() {} - public async function beforeResolveField(GraphQL\IFieldDefinition $field): Awaitable { - echo "AnotherFieldDirective\n"; +final class AnotherDirective implements \Slack\GraphQL\FieldDirective, \Slack\GraphQL\ObjectDirective { + public function formatArgs(): vec { + return vec[]; + } + + public async function beforeResolveField(mixed $object, string $field): Awaitable { + \DirectiveContext::incrementResolveField($this, $object, $field); + } + + public async function beforeResolveObject(string $object_name): Awaitable { + \DirectiveContext::incrementResolveObject($this, $object_name); } } diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 334f974..a279985 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -47,7 +47,7 @@ final class TeamStore { } } -<> +<> interface User { <> public function getId(): int; @@ -95,14 +95,18 @@ enum FavoriteColor: int { BLUE = 2; } -<> +<< + GraphQL\ObjectType('Human', 'Human'), + HasRole(vec[Directives\AdminRoleType::class]), + Directives\LogSampled(0.0, 'bar'), +>> final class Human extends BaseUser { << GraphQL\Field('favorite_color', 'Favorite color of the user'), - HasRole(vec['STAFF']), + HasRole(vec[Directives\StaffRoleType::class]), \Directives\LogSampled(33.3, 'foo'), TestShapeDirective(shape('foo' => 1, 'bar' => 'abc'), true), - Directives\AnotherFieldDirective, + Directives\AnotherDirective, >> public function getFavoriteColor(): FavoriteColor { return FavoriteColor::BLUE; diff --git a/tests/Validation/BaseValidationTest.hack b/tests/Validation/BaseValidationTest.hack index d163f63..2fa8671 100644 --- a/tests/Validation/BaseValidationTest.hack +++ b/tests/Validation/BaseValidationTest.hack @@ -6,28 +6,12 @@ use namespace HH\Lib\Vec; use namespace Slack\GraphQL; use namespace Slack\GraphQL\Validation; -abstract class BaseValidationTest extends \Facebook\HackTest\HackTest { +abstract class BaseValidationTest extends BaseTest { const type TTestCases = dict)>; abstract const classname RULE; - <<__Override>> - public static async function beforeFirstTestAsync(): Awaitable { - await self::runCodegenAsync(); - } - - <<__Memoize>> - private static async function runCodegenAsync(): Awaitable { - await GraphQL\Codegen\Generator::forPath( - __DIR__.'/../Fixtures', - shape( - 'output_directory' => __DIR__.'/../gen', - 'namespace' => 'Slack\GraphQL\Test\Generated', - ), - ); - } - <<\Facebook\HackTest\DataProvider('getTestCases')>> final public async function test(string $query, vec $expected_errors): Awaitable { $source = new \Graphpinator\Source\StringSource($query); diff --git a/tests/gen/AlphabetConnection.hack b/tests/gen/AlphabetConnection.hack index 83644cc..e83f126 100644 --- a/tests/gen/AlphabetConnection.hack +++ b/tests/gen/AlphabetConnection.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<719b63da1617621fd11c2eecc7a5937a>> + * @generated SignedSource<<5f8b6c004891baeb9f3ebe6ccc928ed0>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,4 +46,8 @@ final class AlphabetConnection extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/AnotherObjectShape.hack b/tests/gen/AnotherObjectShape.hack index 53fc88d..3dea947 100644 --- a/tests/gen/AnotherObjectShape.hack +++ b/tests/gen/AnotherObjectShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<1611425c7afdec8569bbf17bf71fe48f>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -37,4 +37,8 @@ final class AnotherObjectShape extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Bot.hack b/tests/gen/Bot.hack index 57cdd89..d549005 100644 --- a/tests/gen/Bot.hack +++ b/tests/gen/Bot.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<82fd88d11578d2accf0fd7734c729e03>> + * @generated SignedSource<<03be695df2735f4f7ae8ed28a01c23de>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -74,4 +74,10 @@ final class Bot extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[ + new \Directives\AnotherDirective(), + ]; + } } diff --git a/tests/gen/Concrete.hack b/tests/gen/Concrete.hack index a4e6fb0..65e3489 100644 --- a/tests/gen/Concrete.hack +++ b/tests/gen/Concrete.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<09f97d331e6676408747e845f64c5356>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -57,4 +57,8 @@ final class Concrete extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/ErrorTestObj.hack b/tests/gen/ErrorTestObj.hack index 419515a..06cecc8 100644 --- a/tests/gen/ErrorTestObj.hack +++ b/tests/gen/ErrorTestObj.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<85f7e816fb6c20b5fc75832ff1b72814>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -136,4 +136,8 @@ final class ErrorTestObj extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/FooInterface.hack b/tests/gen/FooInterface.hack index 99bc769..1ea297d 100644 --- a/tests/gen/FooInterface.hack +++ b/tests/gen/FooInterface.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -30,6 +30,10 @@ final class FooInterface extends \Slack\GraphQL\Types\InterfaceType { } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/FooObject.hack b/tests/gen/FooObject.hack index 2f1bd77..31ea553 100644 --- a/tests/gen/FooObject.hack +++ b/tests/gen/FooObject.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<0ead586fdecd01489a318f475838ee10>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -29,4 +29,8 @@ final class FooObject extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Human.hack b/tests/gen/Human.hack index d27006c..0ac48cb 100644 --- a/tests/gen/Human.hack +++ b/tests/gen/Human.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<4aff110e85a01b651fda1013e243938c>> + * @generated SignedSource<<7ea97f7c9dbc2d43a4cb74d7fd481c6f>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,15 +39,10 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { dict[], async ($parent, $args, $vars) ==> $parent->getFavoriteColor(), vec[ - new \Directives\AnotherFieldDirective(), - new \Directives\HasRole(vec[ - 'STAFF', - ]), - new \Directives\LogSampled(33.3, 'foo'), - new \Directives\TestShapeDirective(shape( - 'foo' => 1, - 'bar' => 'abc', - ), true), + new \Directives\AnotherDirective(), + new \Directives\HasRole(vec[\Directives\StaffRoleType::class]), + new \Directives\LogSampled(33.300000, "foo"), + new \Directives\TestShapeDirective(shape('foo' => 1, 'bar' => "abc"), true), ], ); case 'friends': @@ -160,4 +155,12 @@ final class Human extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[ + new \Directives\AnotherDirective(), + new \Directives\HasRole(vec[\Directives\AdminRoleType::class]), + new \Directives\LogSampled(0.000000, "bar"), + ]; + } } diff --git a/tests/gen/IIntrospectionInterfaceA.hack b/tests/gen/IIntrospectionInterfaceA.hack index c78038c..981079f 100644 --- a/tests/gen/IIntrospectionInterfaceA.hack +++ b/tests/gen/IIntrospectionInterfaceA.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<83c4ac9e30bde4ce823ffcb02f4260a4>> + * @generated SignedSource<<474eefa78f1e2d09600a88d534036421>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -33,6 +33,10 @@ final class IIntrospectionInterfaceA } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/IIntrospectionInterfaceB.hack b/tests/gen/IIntrospectionInterfaceB.hack index e02adbe..a16a827 100644 --- a/tests/gen/IIntrospectionInterfaceB.hack +++ b/tests/gen/IIntrospectionInterfaceB.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<12c9405d6c2b8a6c546a295fa510ad17>> + * @generated SignedSource<<2f12a89134a2d0e3c409bc6e8090774a>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -32,6 +32,10 @@ final class IIntrospectionInterfaceB } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/IIntrospectionInterfaceC.hack b/tests/gen/IIntrospectionInterfaceC.hack index d69c991..67d0b67 100644 --- a/tests/gen/IIntrospectionInterfaceC.hack +++ b/tests/gen/IIntrospectionInterfaceC.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<86c4146f79457335723548bb2e9aaff6>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -31,6 +31,10 @@ final class IIntrospectionInterfaceC } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/ImplementInterfaceA.hack b/tests/gen/ImplementInterfaceA.hack index 13f0944..b57184c 100644 --- a/tests/gen/ImplementInterfaceA.hack +++ b/tests/gen/ImplementInterfaceA.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<53436dffd735452beaf52476eaf137fd>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -29,4 +29,8 @@ final class ImplementInterfaceA extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/ImplementInterfaceB.hack b/tests/gen/ImplementInterfaceB.hack index c1875de..bf5e915 100644 --- a/tests/gen/ImplementInterfaceB.hack +++ b/tests/gen/ImplementInterfaceB.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<9e0b3a4ffca4c305b94da9212f5b4b6f>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -30,4 +30,8 @@ final class ImplementInterfaceB extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/ImplementInterfaceC.hack b/tests/gen/ImplementInterfaceC.hack index 7beac92..b7e331b 100644 --- a/tests/gen/ImplementInterfaceC.hack +++ b/tests/gen/ImplementInterfaceC.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<11aa39f80b2881d293b3ecfb2c300565>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -31,4 +31,8 @@ final class ImplementInterfaceC extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/InterfaceA.hack b/tests/gen/InterfaceA.hack index 6651b79..dee8eb1 100644 --- a/tests/gen/InterfaceA.hack +++ b/tests/gen/InterfaceA.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,6 +39,10 @@ final class InterfaceA extends \Slack\GraphQL\Types\InterfaceType { } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/InterfaceB.hack b/tests/gen/InterfaceB.hack index f15c546..51c59af 100644 --- a/tests/gen/InterfaceB.hack +++ b/tests/gen/InterfaceB.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<4452f961c73140a819658affb076471a>> + * @generated SignedSource<<23f46fe7d8463187b6682cfd68762302>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -48,6 +48,10 @@ final class InterfaceB extends \Slack\GraphQL\Types\InterfaceType { } } + public function getDirectives(): vec { + return vec[]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/IntrospectionTestObject.hack b/tests/gen/IntrospectionTestObject.hack index fc9df0d..b1f12a4 100644 --- a/tests/gen/IntrospectionTestObject.hack +++ b/tests/gen/IntrospectionTestObject.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<96d26624e992a9825611f6456287747c>> + * @generated SignedSource<<45e41c9745331f6573116572a8ba8efc>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -91,4 +91,8 @@ final class IntrospectionTestObject extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Mutation.hack b/tests/gen/Mutation.hack index 94fbe7e..3ef4ca0 100644 --- a/tests/gen/Mutation.hack +++ b/tests/gen/Mutation.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<3c2d153ebae73993aabf8a07ad1c4140>> + * @generated SignedSource<<97f06bebd370fbc8787f762a4cd40e0a>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -60,4 +60,8 @@ final class Mutation extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/NestedOutputShape.hack b/tests/gen/NestedOutputShape.hack index 738029c..2d5cede 100644 --- a/tests/gen/NestedOutputShape.hack +++ b/tests/gen/NestedOutputShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<4e04f7362248af9d50f125809c616da4>> + * @generated SignedSource<<0b7b74b98133923a0745b7bedee84638>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -37,4 +37,8 @@ final class NestedOutputShape extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/ObjectShape.hack b/tests/gen/ObjectShape.hack index 1a98c2b..b54cc16 100644 --- a/tests/gen/ObjectShape.hack +++ b/tests/gen/ObjectShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<61f3547801db2170a80dec294a02a9a9>> + * @generated SignedSource<<6891ec753bf59d5345f3cdfff3f16249>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -55,4 +55,8 @@ final class ObjectShape extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/OutputShape.hack b/tests/gen/OutputShape.hack index 186a406..d449ba8 100644 --- a/tests/gen/OutputShape.hack +++ b/tests/gen/OutputShape.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<10a6010662c45c2b62ad31c58ebbb25d>> + * @generated SignedSource<<7e99f57dacb997591c72087d6edf3f08>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -55,4 +55,8 @@ final class OutputShape extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/OutputTypeTestObj.hack b/tests/gen/OutputTypeTestObj.hack index 3c3b40e..eb8067e 100644 --- a/tests/gen/OutputTypeTestObj.hack +++ b/tests/gen/OutputTypeTestObj.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<26a44836f820c6e077cb276f7420989f>> + * @generated SignedSource<<09695374eb1e58934de4020b3263ef25>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -100,4 +100,8 @@ final class OutputTypeTestObj extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/PageInfo.hack b/tests/gen/PageInfo.hack index c656a31..8ee4d0b 100644 --- a/tests/gen/PageInfo.hack +++ b/tests/gen/PageInfo.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<39088bc448c50e4310ce649372f459cf>> + * @generated SignedSource<<5bfc5f3b5335acce9fe7662d1508d4a7>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -64,4 +64,8 @@ final class PageInfo extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Query.hack b/tests/gen/Query.hack index 79b44ae..612a249 100644 --- a/tests/gen/Query.hack +++ b/tests/gen/Query.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -307,4 +307,8 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Schema.hack b/tests/gen/Schema.hack index 24ca744..87adbd1 100644 --- a/tests/gen/Schema.hack +++ b/tests/gen/Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<024a37e7259d3742ec2b1d7c931d239b>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -23,6 +23,7 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'CreateUserInput' => CreateUserInput::class, 'ErrorTestObj' => ErrorTestObj::class, 'FavoriteColor' => FavoriteColor::class, + 'Float' => Types\FloatType::class, 'FooInterface' => FooInterface::class, 'FooObject' => FooObject::class, 'Human' => Human::class, diff --git a/tests/gen/StringTypeEdge.hack b/tests/gen/StringTypeEdge.hack index 26a8152..6eb625b 100644 --- a/tests/gen/StringTypeEdge.hack +++ b/tests/gen/StringTypeEdge.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<2f08369620321d56e1063d4826923e8e>> + * @generated SignedSource<<099876231b60f10f885505f29fd17d3e>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,4 +46,8 @@ final class StringTypeEdge extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/Team.hack b/tests/gen/Team.hack index a7b32b7..96e7ce6 100644 --- a/tests/gen/Team.hack +++ b/tests/gen/Team.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<28b06e9d05212da52e2b0f1c80871c2b>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -71,4 +71,8 @@ final class Team extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/User.hack b/tests/gen/User.hack index 1cfb64e..7081774 100644 --- a/tests/gen/User.hack +++ b/tests/gen/User.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -67,6 +67,12 @@ final class User extends \Slack\GraphQL\Types\InterfaceType { } } + public function getDirectives(): vec { + return vec[ + new \Directives\AnotherDirective(), + ]; + } + public async function resolveAsync( this::THackType $value, vec<\Graphpinator\Parser\Field\IHasSelectionSet> $parent_nodes, diff --git a/tests/gen/UserConnection.hack b/tests/gen/UserConnection.hack index 328e355..83b2e83 100644 --- a/tests/gen/UserConnection.hack +++ b/tests/gen/UserConnection.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<8148f902c0b3c1fad35b44af1750c389>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,4 +46,8 @@ final class UserConnection extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/UserEdge.hack b/tests/gen/UserEdge.hack index 9300645..9cf5370 100644 --- a/tests/gen/UserEdge.hack +++ b/tests/gen/UserEdge.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<8881f007a7263fc895b7e0d6b52a8956>> + * @generated SignedSource<<5665608d7c5975f884ca8afb7893bdc5>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -46,4 +46,8 @@ final class UserEdge extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__Directive.hack b/tests/gen/__Directive.hack index f94be66..af99713 100644 --- a/tests/gen/__Directive.hack +++ b/tests/gen/__Directive.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<8d1002ab1c1f4c808b79f8bf6b909a32>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -64,4 +64,8 @@ final class __Directive extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__EnumValue.hack b/tests/gen/__EnumValue.hack index 43abddb..7de6c4a 100644 --- a/tests/gen/__EnumValue.hack +++ b/tests/gen/__EnumValue.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<723b75b056d5ca1b83a331a7c8c81d82>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -64,4 +64,8 @@ final class __EnumValue extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__Field.hack b/tests/gen/__Field.hack index 79a5879..b2888b5 100644 --- a/tests/gen/__Field.hack +++ b/tests/gen/__Field.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<237fce4f25f9799eb3a313c7c981f5af>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -82,4 +82,8 @@ final class __Field extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__InputValue.hack b/tests/gen/__InputValue.hack index 28bbc86..3ac30fa 100644 --- a/tests/gen/__InputValue.hack +++ b/tests/gen/__InputValue.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<8d46941c60191c5c536e393a2ced87f2>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -64,4 +64,8 @@ final class __InputValue extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__Schema.hack b/tests/gen/__Schema.hack index 6a722a9..34dd14f 100644 --- a/tests/gen/__Schema.hack +++ b/tests/gen/__Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<3a2ace1dfaf20502d018e61b7180b689>> + * @generated SignedSource<<7043578f96e44901000cd3cee9dcfb23>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -73,4 +73,8 @@ final class __Schema extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/gen/__Type.hack b/tests/gen/__Type.hack index 9ab67f4..f37a013 100644 --- a/tests/gen/__Type.hack +++ b/tests/gen/__Type.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<7fd4a983d839245ee2f2e216f6ef0e09>> + * @generated SignedSource<<4b5c85309c11eb94cbd9e65361724b45>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -125,4 +125,8 @@ final class __Type extends \Slack\GraphQL\Types\ObjectType { return null; } } + + public function getDirectives(): vec { + return vec[]; + } } diff --git a/tests/schema.json b/tests/schema.json index b1f1807..bf7cca4 100644 --- a/tests/schema.json +++ b/tests/schema.json @@ -503,6 +503,15 @@ ], "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Float", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INTERFACE", "name": "FooInterface", From d26ec52602a84476865dabb665c7437ad7333f40 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Tue, 28 Sep 2021 15:27:14 -0700 Subject: [PATCH 09/11] remove unused function --- src/Codegen/Types.hack | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Codegen/Types.hack b/src/Codegen/Types.hack index 83faf74..649d9ae 100644 --- a/src/Codegen/Types.hack +++ b/src/Codegen/Types.hack @@ -111,10 +111,6 @@ function get_graphql_leaf_type(string $hack_type): ?string { } } -function is_float_type(string $hack_type): bool { - return $hack_type === 'HH\float' || $hack_type === '?HH\float'; -} - /** * Get the GraphQL type to output for a hack type. */ From 334a891f533f0ff2fa3d31fb910c058a4f8a4b4c Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Tue, 28 Sep 2021 15:28:23 -0700 Subject: [PATCH 10/11] simplify this PR a bit --- src/Codegen/Types.hack | 3 -- src/Types/InputOutput/Scalar/FloatType.hack | 37 --------------------- tests/gen/Schema.hack | 3 +- tests/schema.json | 9 ----- 4 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 src/Types/InputOutput/Scalar/FloatType.hack diff --git a/src/Codegen/Types.hack b/src/Codegen/Types.hack index 649d9ae..3c9e6fe 100644 --- a/src/Codegen/Types.hack +++ b/src/Codegen/Types.hack @@ -8,7 +8,6 @@ use namespace Slack\GraphQL\Types; const dict> BUILTIN_TYPES = dict[ Types\IntType::NAME => Types\IntType::class, - Types\FloatType::NAME => Types\FloatType::class, Types\StringType::NAME => Types\StringType::class, Types\BooleanType::NAME => Types\BooleanType::class, ]; @@ -92,8 +91,6 @@ function get_graphql_leaf_type(string $hack_type): ?string { switch ($hack_type) { case 'HH\int': return Types\IntType::class; - case 'HH\float': - return Types\FloatType::class; case 'HH\string': return Types\StringType::class; case 'HH\bool': diff --git a/src/Types/InputOutput/Scalar/FloatType.hack b/src/Types/InputOutput/Scalar/FloatType.hack deleted file mode 100644 index 861e344..0000000 --- a/src/Types/InputOutput/Scalar/FloatType.hack +++ /dev/null @@ -1,37 +0,0 @@ - - - -namespace Slack\GraphQL\Types; - -use namespace Graphpinator\Parser\Value; -use namespace Slack\GraphQL; - -final class FloatType extends ScalarType { - - const type THackType = float; - const string NAME = 'Float'; - - const int MIN_SAFE_VALUE = -2147483648; - const int MAX_SAFE_VALUE = 2147483647; - - <<__Override>> - public function coerceValue(mixed $value): float { - if (!$value is float) { - throw new GraphQL\UserFacingError('Expected a float, got %s', (string)$value); - } - return $value; - } - - <<__Override>> - final public function coerceNonVariableNode(Value\Value $node, dict $variable_values): float { - if (!$node is Value\FloatLiteral) { - throw new GraphQL\UserFacingError('Expected a Float literal, got %s', \get_class($node)); - } - return $node->getRawValue(); - } - - <<__Override>> - protected function serialize(float $value): float { - return $value; - } -} diff --git a/tests/gen/Schema.hack b/tests/gen/Schema.hack index d0110ef..3acb6f5 100644 --- a/tests/gen/Schema.hack +++ b/tests/gen/Schema.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<957f11aa25d3f2f93bb17e881ac469b7>> + * @generated SignedSource<<28ca3795298b957dd1a0452383e71935>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -24,7 +24,6 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'CreateUserInput' => CreateUserInput::class, 'ErrorTestObj' => ErrorTestObj::class, 'FavoriteColor' => FavoriteColor::class, - 'Float' => Types\FloatType::class, 'FooConnection' => FooConnection::class, 'FooInterface' => FooInterface::class, 'FooObject' => FooObject::class, diff --git a/tests/schema.json b/tests/schema.json index 81eba19..182566f 100644 --- a/tests/schema.json +++ b/tests/schema.json @@ -524,15 +524,6 @@ ], "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "Float", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "FooConnection", From 53746a7d23aca7a72d25935cfc3667ff9ad2896e Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Tue, 28 Sep 2021 15:38:31 -0700 Subject: [PATCH 11/11] directives for root fields --- src/Codegen/Builders/Fields/FieldBuilder.hack | 8 ++++++-- src/Codegen/Generator.hack | 7 ++++++- tests/BaseTest.hack | 2 -- tests/DirectiveContext.hack | 1 + tests/Fixtures/Playground.hack | 7 +++++-- tests/gen/Mutation.hack | 6 ++++-- tests/gen/Query.hack | 6 ++++-- 7 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index 0f27502..dfee822 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -90,8 +90,12 @@ abstract class FieldBuilder { /** * Construct a top-level GraphQL field. */ - public static function forRootField(\Slack\GraphQL\Field $field, \ReflectionMethod $rm): FieldBuilder { - return FieldBuilder::fromReflectionMethod($field, $rm, dict[], true); + public static function forRootField( + \Slack\GraphQL\Field $field, + \ReflectionMethod $rm, + dict> $directives, + ): FieldBuilder { + return FieldBuilder::fromReflectionMethod($field, $rm, $directives, true); } public static function introspectSchemaField(): FieldBuilder { diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index ac84dd7..288e5f4 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -296,7 +296,11 @@ final class Generator { $rm = new \ReflectionMethod($class->getName(), $method_name); $query_root_field = $rm->getAttributeClass(\Slack\GraphQL\QueryRootField::class); if ($query_root_field is nonnull) { - $query_fields[$query_root_field->getName()] = FieldBuilder::forRootField($query_root_field, $rm); + $query_fields[$query_root_field->getName()] = FieldBuilder::forRootField( + $query_root_field, + $rm, + $directive_finder->findDirectivesForField($rm), + ); continue; } @@ -308,6 +312,7 @@ final class Generator { $mutation_fields[$mutation_root_field->getName()] = FieldBuilder::forRootField( $mutation_root_field, $rm, + $directive_finder->findDirectivesForField($rm), ); } } diff --git a/tests/BaseTest.hack b/tests/BaseTest.hack index f320120..cdff916 100644 --- a/tests/BaseTest.hack +++ b/tests/BaseTest.hack @@ -17,7 +17,6 @@ abstract class BaseTest extends \Facebook\HackTest\HackTest { <<__Memoize>> private static async function runCodegenAsync(): Awaitable { - $start_ts = microtime(true); await GraphQL\Codegen\Generator::forPath( __DIR__.'/Fixtures', shape( @@ -38,7 +37,6 @@ abstract class BaseTest extends \Facebook\HackTest\HackTest { ), ), ); - echo "Generated fixtures in: ".(microtime(true) - $start_ts)."\n"; } private function getResolver(GraphQL\Resolver::TOptions $options = shape()): GraphQL\Resolver { diff --git a/tests/DirectiveContext.hack b/tests/DirectiveContext.hack index 195fcf9..0bb8d44 100644 --- a/tests/DirectiveContext.hack +++ b/tests/DirectiveContext.hack @@ -1,3 +1,4 @@ + use namespace HH\Lib\C; final class DirectiveContext { diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index 9a1764c..784efd6 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -172,7 +172,7 @@ abstract final class UserQueryAttributes { return new \Human(shape('id' => $id, 'name' => 'User '.$id, 'team_id' => 1, 'is_active' => true)); } - <> + <> public static async function getBot(int $id): Awaitable<\Bot> { return new \Bot(shape('id' => $id, 'name' => 'User '.$id, 'team_id' => 1, 'is_active' => true)); } @@ -222,7 +222,10 @@ abstract final class UserMutationAttributes { return new \Human(shape('id' => $id, 'name' => 'User '.$id, 'team_id' => 1, 'is_active' => true)); } - <> + << + GraphQL\MutationRootField('createUser', 'Create a new user'), + \Directives\HasRole(vec[\Directives\StaffRoleType::class]), + >> public static async function createUser(TCreateUserInput $input): Awaitable<\User> { $team_input = $input['team'] ?? null; diff --git a/tests/gen/Mutation.hack b/tests/gen/Mutation.hack index 3ef4ca0..e5c7977 100644 --- a/tests/gen/Mutation.hack +++ b/tests/gen/Mutation.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<<97f06bebd370fbc8787f762a4cd40e0a>> + * @generated SignedSource<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -39,7 +39,9 @@ final class Mutation extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserMutationAttributes::createUser( CreateUserInput::nonNullable()->coerceNamedNode('input', $args, $vars), ), - vec[], + vec[ + new \Directives\HasRole(vec[\Directives\StaffRoleType::class]), + ], ); case 'pokeUser': return new GraphQL\FieldDefinition( diff --git a/tests/gen/Query.hack b/tests/gen/Query.hack index 38e449c..b6b3a13 100644 --- a/tests/gen/Query.hack +++ b/tests/gen/Query.hack @@ -4,7 +4,7 @@ * To re-generate this file run vendor/bin/hacktest * * - * @generated SignedSource<> + * @generated SignedSource<<1fe6f2b89af7fb1bc13a159233c38301>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -176,7 +176,9 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { async ($parent, $args, $vars) ==> await \UserQueryAttributes::getBot( Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), ), - vec[], + vec[ + new \Directives\LogSampled(1.100000, "foo"), + ], ); case 'error_test': return new GraphQL\FieldDefinition(