From cbcb639efed7dbc8685d1bd62699947d33a967f4 Mon Sep 17 00:00:00 2001 From: Ian Hoffman Date: Fri, 22 Apr 2022 17:15:39 -0700 Subject: [PATCH] Support custom input types --- src/Codegen/Builders/CompositeBuilder.hack | 3 +- src/Codegen/Builders/Fields/FieldBuilder.hack | 40 +++++---- .../IntrospectionSchemaFieldBuilder.hack | 13 +-- .../Fields/IntrospectionTypeFieldBuilder.hack | 23 +++--- .../Builders/Fields/MethodFieldBuilder.hack | 2 +- src/Codegen/Builders/InputObjectBuilder.hack | 20 +++-- src/Codegen/Builders/InterfaceBuilder.hack | 3 +- src/Codegen/Builders/ObjectBuilder.hack | 81 ++++++++++++------- src/Codegen/Builders/TypeBuilder.hack | 2 +- src/Codegen/Context.hack | 11 +++ src/Codegen/FieldResolver.hack | 4 +- src/Codegen/Generator.hack | 45 ++++++++--- src/Codegen/Types.hack | 14 +++- src/Request.hack | 3 + src/Response.hack | 3 + tests/FixtureTest.hack | 4 + tests/Fixtures/ChannelInputType.hack | 41 ++++++++++ tests/Fixtures/Playground.hack | 8 +- tests/Fixtures/UserIdInputType.hack | 43 ++++++++++ tests/Validation/BaseValidationTest.hack | 4 + tests/gen/Query.hack | 21 ++++- tests/gen/Schema.hack | 4 +- tests/schema.json | 45 ++++++++++- 23 files changed, 343 insertions(+), 94 deletions(-) create mode 100644 src/Codegen/Context.hack create mode 100644 tests/Fixtures/ChannelInputType.hack create mode 100644 tests/Fixtures/UserIdInputType.hack diff --git a/src/Codegen/Builders/CompositeBuilder.hack b/src/Codegen/Builders/CompositeBuilder.hack index d92ab8b..b299734 100644 --- a/src/Codegen/Builders/CompositeBuilder.hack +++ b/src/Codegen/Builders/CompositeBuilder.hack @@ -21,11 +21,12 @@ use type Facebook\HackCodegen\{ abstract class CompositeBuilder extends OutputTypeBuilder<\Slack\GraphQL\__Private\CompositeType> { public function __construct( + Context $ctx, \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, protected vec $fields, ) { - parent::__construct($type_info, $hack_type); + parent::__construct($ctx, $type_info, $hack_type); } public function build(HackCodegenFactory $cg): CodegenClass { diff --git a/src/Codegen/Builders/Fields/FieldBuilder.hack b/src/Codegen/Builders/Fields/FieldBuilder.hack index 7a3ca5e..992d80e 100644 --- a/src/Codegen/Builders/Fields/FieldBuilder.hack +++ b/src/Codegen/Builders/Fields/FieldBuilder.hack @@ -23,12 +23,13 @@ abstract class FieldBuilder { // Constructors - public function __construct(protected this::TField $data) {} + public function __construct(protected Context $ctx, protected this::TField $data) {} /** * Construct a GraphQL field from a Hack method. */ public static function fromReflectionMethod( + Context $ctx, \Slack\GraphQL\Field $field, \ReflectionMethod $rm, bool $is_root_field = false, @@ -61,36 +62,43 @@ abstract class FieldBuilder { } if (returns_connection_type($rm)) { - return new ConnectionFieldBuilder($data); + return new ConnectionFieldBuilder($ctx, $data); } else { - return new MethodFieldBuilder($data); + return new MethodFieldBuilder($ctx, $data); } } /** * Construct a GraphQL field from a shape field. */ - public static function fromShapeField(string $name, TypeStructure $ts): FieldBuilder { - return new ShapeFieldBuilder(shape( - 'name' => $name, - 'output_type' => output_type(type_structure_to_type_alias($ts), false), - 'is_optional' => Shapes::idx($ts, 'optional_shape_field') ?? false, - )); + public static function fromShapeField(Context $ctx, string $name, TypeStructure $ts): FieldBuilder { + return new ShapeFieldBuilder( + $ctx, + shape( + 'name' => $name, + 'output_type' => output_type(type_structure_to_type_alias($ts), false), + 'is_optional' => Shapes::idx($ts, 'optional_shape_field') ?? false, + ), + ); } /** * Construct a top-level GraphQL field. */ - public static function forRootField(\Slack\GraphQL\Field $field, \ReflectionMethod $rm): FieldBuilder { - return FieldBuilder::fromReflectionMethod($field, $rm, true); + public static function forRootField( + Context $ctx, + \Slack\GraphQL\Field $field, + \ReflectionMethod $rm, + ): FieldBuilder { + return FieldBuilder::fromReflectionMethod($ctx, $field, $rm, true); } - public static function introspectSchemaField(): FieldBuilder { - return new IntrospectSchemaFieldBuilder(); + public static function introspectSchemaField(Context $ctx): FieldBuilder { + return new IntrospectSchemaFieldBuilder($ctx); } - public static function introspectTypeField(): FieldBuilder { - return new IntrospectTypeFieldBuilder(); + public static function introspectTypeField(Context $ctx): FieldBuilder { + return new IntrospectTypeFieldBuilder($ctx); } // Codegen @@ -141,7 +149,7 @@ abstract class FieldBuilder { $hb->addLinef("'name' => %s,", $argument_name); - $type = input_type($param['type']); + $type = input_type($param['type'], $this->ctx->getCustomTypes()); $hb->addLinef("'type' => %s,", $type); $default_value = $param['default_value'] ?? null; diff --git a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack index 9d93f49..aadd40e 100644 --- a/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionSchemaFieldBuilder.hack @@ -11,11 +11,14 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder { ... ); - public function __construct() { - parent::__construct(shape( - 'name' => '__schema', - 'output_type' => shape('type' => '__Schema::nullableOutput()'), - )); + public function __construct(Context $ctx) { + parent::__construct( + $ctx, + shape( + 'name' => '__schema', + 'output_type' => shape('type' => '__Schema::nullableOutput()'), + ), + ); } <<__Override>> diff --git a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack index d10ac4c..e01246e 100644 --- a/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/IntrospectionTypeFieldBuilder.hack @@ -6,16 +6,19 @@ use type Facebook\HackCodegen\{HackBuilder}; final class IntrospectTypeFieldBuilder extends MethodFieldBuilder { - public function __construct() { - parent::__construct(shape( - 'name' => '__type', - 'method_name' => '_', - 'output_type' => shape('type' => '__Type::nullableOutput()'), - 'root_field_for_type' => 'Schema', - 'parameters' => vec[ - shape('name' => 'name', 'type' => 'HH\string', 'is_optional' => false), - ], - )); + public function __construct(Context $ctx) { + parent::__construct( + $ctx, + shape( + 'name' => '__type', + 'method_name' => '_', + 'output_type' => shape('type' => '__Type::nullableOutput()'), + 'root_field_for_type' => 'Schema', + 'parameters' => vec[ + shape('name' => 'name', 'type' => 'HH\string', 'is_optional' => false), + ], + ), + ); } <<__Override>> protected function generateResolverBody(HackBuilder $hb): void { diff --git a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack index a0289cf..27a1e43 100644 --- a/src/Codegen/Builders/Fields/MethodFieldBuilder.hack +++ b/src/Codegen/Builders/Fields/MethodFieldBuilder.hack @@ -80,7 +80,7 @@ class MethodFieldBuilder extends FieldBuilder { final protected function getArgumentInvocationString(Parameter $param): string { return Str\format( '%s->coerce%sNamedNode(%s, $args, $vars%s)', - input_type($param['type']), + input_type($param['type'], $this->ctx->getCustomTypes()), $param['is_optional'] ? 'Optional' : '', \var_export($param['name'], true), Shapes::keyExists($param, 'default_value') ? ', '.$param['default_value'] : '', diff --git a/src/Codegen/Builders/InputObjectBuilder.hack b/src/Codegen/Builders/InputObjectBuilder.hack index bd3edbb..64bec3b 100644 --- a/src/Codegen/Builders/InputObjectBuilder.hack +++ b/src/Codegen/Builders/InputObjectBuilder.hack @@ -11,8 +11,12 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType const classname<\Slack\GraphQL\Types\InputObjectType> SUPERCLASS = \Slack\GraphQL\Types\InputObjectType::class; - public function __construct(\Slack\GraphQL\InputObjectType $type_info, private \ReflectionTypeAlias $type_alias) { - parent::__construct($type_info, $type_alias->getName()); + public function __construct( + Context $ctx, + \Slack\GraphQL\InputObjectType $type_info, + private \ReflectionTypeAlias $type_alias, + ) { + parent::__construct($ctx, $type_info, $type_alias->getName()); } public function build(HackCodegenFactory $cg): CodegenClass { @@ -36,7 +40,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType ->addParameter('KeyedContainer $fields') ->setReturnType('this::THackType') ->setBody( - self::getFieldCoercionMethodBody( + $this->getFieldCoercionMethodBody( $cg, $ts['fields'], $field_name ==> Str\format('C\\contains_key($fields, %s)', $field_name), @@ -49,7 +53,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType ->addParameter('dict $vars') ->setReturnType('this::THackType') ->setBody( - self::getFieldCoercionMethodBody( + $this->getFieldCoercionMethodBody( $cg, $ts['fields'], $field_name ==> Str\format('$this->hasValue(%s, $fields, $vars)', $field_name), @@ -61,7 +65,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType ->addParameter('KeyedContainer $fields') ->setReturnType('this::THackType') ->setBody( - self::getFieldCoercionMethodBody( + $this->getFieldCoercionMethodBody( $cg, $ts['fields'], $field_name ==> Str\format('C\\contains_key($fields, %s)', $field_name), @@ -75,7 +79,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType /** * Shared logic for coerceFieldValues(), coerceFieldNodes() and assertValidFieldValues() */ - private static function getFieldCoercionMethodBody( + private function getFieldCoercionMethodBody( HackCodegenFactory $cg, KeyedContainer> $fields, (function(string): string) $get_if_condition, @@ -94,7 +98,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType ); $name_literal = \var_export($field_name, true); - $type = input_type(type_structure_to_type_alias($field_ts)); + $type = input_type(type_structure_to_type_alias($field_ts), $this->ctx->getCustomTypes()); if ($is_optional) { $hb->startIfBlock($get_if_condition($name_literal)); @@ -127,7 +131,7 @@ class InputObjectBuilder extends InputTypeBuilder<\Slack\GraphQL\InputObjectType $hb->addLine('return shape(')->indent(); $hb->addLinef("'name' => %s,", \var_export($field_name, true)); - $type = input_type(type_structure_to_type_alias($field_ts)); + $type = input_type(type_structure_to_type_alias($field_ts), $this->ctx->getCustomTypes()); $hb->addLinef("'type' => %s,", $type); // TODO: description, defaultValue $hb->unindent()->addLine(');')->unindent(); diff --git a/src/Codegen/Builders/InterfaceBuilder.hack b/src/Codegen/Builders/InterfaceBuilder.hack index 5becea0..50ede55 100644 --- a/src/Codegen/Builders/InterfaceBuilder.hack +++ b/src/Codegen/Builders/InterfaceBuilder.hack @@ -17,12 +17,13 @@ final class InterfaceBuilder extends CompositeBuilder { <<__Override>> public function __construct( + Context $ctx, \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, vec $fields, private dict $hack_class_to_graphql_object, ) { - parent::__construct($type_info, $hack_type, $fields); + parent::__construct($ctx, $type_info, $hack_type, $fields); } <<__Override>> diff --git a/src/Codegen/Builders/ObjectBuilder.hack b/src/Codegen/Builders/ObjectBuilder.hack index f20ab2f..beba846 100644 --- a/src/Codegen/Builders/ObjectBuilder.hack +++ b/src/Codegen/Builders/ObjectBuilder.hack @@ -17,12 +17,13 @@ class ObjectBuilder extends CompositeBuilder { <<__Override>> public function __construct( + Context $ctx, \Slack\GraphQL\__Private\CompositeType $type_info, string $hack_type, vec $fields, private dict $hack_class_to_graphql_interface, ) { - parent::__construct($type_info, $hack_type, $fields); + parent::__construct($ctx, $type_info, $hack_type, $fields); } <<__Override>> @@ -45,6 +46,7 @@ class ObjectBuilder extends CompositeBuilder { } public static function fromTypeAlias( + Context $ctx, \Slack\GraphQL\ObjectType $type_info, \ReflectionTypeAlias $type_alias, ): ObjectBuilder { @@ -55,39 +57,48 @@ class ObjectBuilder extends CompositeBuilder { TypeStructureKind::getNames()[$ts['kind']], ); return new ObjectBuilder( + $ctx, $type_info, $type_alias->getName(), - Vec\map_with_key($ts['fields'], ($name, $ts) ==> FieldBuilder::fromShapeField($name, $ts)), + Vec\map_with_key($ts['fields'], ($name, $ts) ==> FieldBuilder::fromShapeField($ctx, $name, $ts)), dict[], // Objects generated from shapes cannot implement interfaces ); } public static function forConnection( + Context $ctx, string $name, \Slack\GraphQL\ObjectType $object_type, string $edge_name, vec $additional_fields, ): ObjectBuilder { return new ObjectBuilder( + $ctx, $object_type, $name, // hack type Vec\concat( vec[ // fields - new MethodFieldBuilder(shape( - 'name' => 'edges', - 'method_name' => 'getEdges', - 'output_type' => shape( - 'type' => $edge_name.'::nonNullable()->nullableOutputListOf()', - 'needs_await' => true, + new MethodFieldBuilder( + $ctx, + shape( + 'name' => 'edges', + 'method_name' => 'getEdges', + 'output_type' => shape( + 'type' => $edge_name.'::nonNullable()->nullableOutputListOf()', + 'needs_await' => true, + ), + 'parameters' => vec[], ), - 'parameters' => vec[], - )), - new MethodFieldBuilder(shape( - 'name' => 'pageInfo', - 'method_name' => 'getPageInfo', - 'output_type' => shape('type' => 'PageInfo::nullableOutput()', 'needs_await' => true), - 'parameters' => vec[], - )), + ), + new MethodFieldBuilder( + $ctx, + shape( + 'name' => 'pageInfo', + 'method_name' => 'getPageInfo', + 'output_type' => shape('type' => 'PageInfo::nullableOutput()', 'needs_await' => true), + 'parameters' => vec[], + ), + ), ], $additional_fields, ), @@ -96,24 +107,36 @@ class ObjectBuilder extends CompositeBuilder { } // TODO: It should be possible to create user-defined edges which contain additional fields. - public static function forEdge(string $gql_type, string $hack_type, string $output_type): ObjectBuilder { + public static function forEdge( + Context $ctx, + string $gql_type, + string $hack_type, + string $output_type, + ): ObjectBuilder { $name = $gql_type.'Edge'; return new ObjectBuilder( + $ctx, new \Slack\GraphQL\ObjectType($name, $gql_type.' Edge'), // TODO: Description 'Slack\GraphQL\Pagination\Edge<'.$hack_type.'>', // hack type vec[ // fields - new MethodFieldBuilder(shape( - 'name' => 'node', - 'method_name' => 'getNode', - 'output_type' => shape('type' => $output_type.'::nullableOutput()'), - 'parameters' => vec[], - )), - new MethodFieldBuilder(shape( - 'name' => 'cursor', - 'method_name' => 'getCursor', - 'output_type' => shape('type' => 'Types\StringType::nullableOutput()'), - 'parameters' => vec[], - )), + new MethodFieldBuilder( + $ctx, + shape( + 'name' => 'node', + 'method_name' => 'getNode', + 'output_type' => shape('type' => $output_type.'::nullableOutput()'), + 'parameters' => vec[], + ), + ), + new MethodFieldBuilder( + $ctx, + shape( + 'name' => 'cursor', + 'method_name' => 'getCursor', + 'output_type' => shape('type' => 'Types\StringType::nullableOutput()'), + 'parameters' => vec[], + ), + ), ], dict[], ); diff --git a/src/Codegen/Builders/TypeBuilder.hack b/src/Codegen/Builders/TypeBuilder.hack index a84b40b..cff7c0b 100644 --- a/src/Codegen/Builders/TypeBuilder.hack +++ b/src/Codegen/Builders/TypeBuilder.hack @@ -24,7 +24,7 @@ abstract class TypeBuilder { abstract const classname<\Slack\GraphQL\Types\BaseType> SUPERCLASS; - public function __construct(protected T $type_info, protected string $hack_type) {} + public function __construct(protected Context $ctx, protected T $type_info, protected string $hack_type) {} final public function getGraphQLType(): string { return $this->type_info->getType(); diff --git a/src/Codegen/Context.hack b/src/Codegen/Context.hack new file mode 100644 index 0000000..945c04b --- /dev/null +++ b/src/Codegen/Context.hack @@ -0,0 +1,11 @@ + + +namespace Slack\GraphQL\Codegen; + +final class Context { + public function __construct(private dict> $custom_types) {} + + public function getCustomTypes(): dict> { + return $this->custom_types; + } +} diff --git a/src/Codegen/FieldResolver.hack b/src/Codegen/FieldResolver.hack index d322030..3261ec1 100644 --- a/src/Codegen/FieldResolver.hack +++ b/src/Codegen/FieldResolver.hack @@ -14,7 +14,7 @@ final class FieldResolver { private dict $scanned_classes; private dict> $resolved_fields = dict[]; - public function __construct(vec $classes) { + public function __construct(private Context $ctx, vec $classes) { $this->scanned_classes = Dict\from_values($classes, $class ==> $class->getName()); } @@ -77,7 +77,7 @@ 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); + $fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($this->ctx, $graphql_field, $rm); } return $fields; diff --git a/src/Codegen/Generator.hack b/src/Codegen/Generator.hack index 50a155e..57279a2 100644 --- a/src/Codegen/Generator.hack +++ b/src/Codegen/Generator.hack @@ -1,6 +1,5 @@ - namespace Slack\GraphQL\Codegen; use namespace Slack\GraphQL\Types; @@ -26,15 +25,18 @@ function hb(HackCodegenFactory $cg): HackBuilder { final class Generator { private HackCodegenFactory $cg; private bool $has_mutations = false; + private Context $ctx; const type TGeneratorConfig = shape( 'output_directory' => string, 'namespace' => string, ?'codegen_config' => IHackCodegenConfig, + ?'custom_types' => dict>, ); private function __construct(private MultiParser $parser, private self::TGeneratorConfig $config) { $this->cg = new HackCodegenFactory($config['codegen_config'] ?? new HackCodegenConfig()); + $this->ctx = new Context($config['custom_types'] ?? dict[]); } private static async function init( @@ -75,6 +77,10 @@ final class Generator { } } + foreach ($this->config['custom_types'] ?? dict[] as $_ => $type) { + $types = self::add($types, $type::NAME, Str\format('\%s', $type)); + } + $this->generateFile($this->generateSchemaType($this->cg, $types)); } @@ -195,14 +201,14 @@ final class Generator { private async function collectObjects(): Awaitable> { $objects = vec[]; $query_fields = dict[ - '__schema' => FieldBuilder::introspectSchemaField(), - '__type' => FieldBuilder::introspectTypeField(), + '__schema' => FieldBuilder::introspectSchemaField($this->ctx), + '__type' => FieldBuilder::introspectTypeField($this->ctx), ]; $mutation_fields = dict[]; $classish_objects = $this->parser->getClassishObjects(); - $field_resolver = new FieldResolver($classish_objects); + $field_resolver = new FieldResolver($this->ctx, $classish_objects); $class_fields = $field_resolver->resolveFields(); $hack_class_to_graphql_interface = dict[]; @@ -229,12 +235,12 @@ final class Generator { $rt = new \ReflectionTypeAlias($type->getName()); $graphql_input = $rt->getAttributeClass(\Slack\GraphQL\InputObjectType::class); if ($graphql_input is nonnull) { - $objects[] = new InputObjectBuilder($graphql_input, $rt); + $objects[] = new InputObjectBuilder($this->ctx, $graphql_input, $rt); } $graphql_output = $rt->getAttributeClass(\Slack\GraphQL\ObjectType::class); if ($graphql_output is nonnull) { - $objects[] = ObjectBuilder::fromTypeAlias($graphql_output, $rt); + $objects[] = ObjectBuilder::fromTypeAlias($this->ctx, $graphql_output, $rt); } } @@ -269,14 +275,20 @@ final class Generator { ); if ($graphql_interface is nonnull) { $objects[] = new InterfaceBuilder( + $this->ctx, $graphql_interface, $rc->getName(), $fields, $hack_class_to_graphql_object, ); } else if ($graphql_object is nonnull) { - $objects[] = - new ObjectBuilder($graphql_object, $rc->getName(), $fields, $hack_class_to_graphql_interface); + $objects[] = new ObjectBuilder( + $this->ctx, + $graphql_object, + $rc->getName(), + $fields, + $hack_class_to_graphql_interface, + ); } } @@ -289,7 +301,8 @@ 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($this->ctx, $query_root_field, $rm); continue; } @@ -299,7 +312,7 @@ final class Generator { $this->has_mutations = true; $mutation_fields[$mutation_root_field->getName()] = - FieldBuilder::forRootField($mutation_root_field, $rm); + FieldBuilder::forRootField($this->ctx, $mutation_root_field, $rm); } } } @@ -309,10 +322,11 @@ final class Generator { $rc = new \ReflectionClass($enum->getName()); $enum_type = $rc->getAttributeClass(\Slack\GraphQL\EnumType::class); if ($enum_type is null) continue; - $objects[] = new EnumBuilder($enum_type, $enum->getName()); + $objects[] = new EnumBuilder($this->ctx, $enum_type, $enum->getName()); } $objects[] = new ObjectBuilder( + $this->ctx, new \Slack\GraphQL\ObjectType('Query', 'Query'), 'Slack\\GraphQL\\Root', vec(Dict\sort_by_key($query_fields)), @@ -321,6 +335,7 @@ final class Generator { if (!C\is_empty($mutation_fields)) { $objects[] = new ObjectBuilder( + $this->ctx, new \Slack\GraphQL\ObjectType('Mutation', 'Mutation'), 'Slack\\GraphQL\\Root', vec(Dict\sort_by_key($mutation_fields)), @@ -349,12 +364,18 @@ final class Generator { ); return vec[ ObjectBuilder::forConnection( + $this->ctx, $class->getName(), $object_type, $type_info['gql_type'].'Edge', $additional_fields, ), - ObjectBuilder::forEdge($type_info['gql_type'], $type_info['hack_type'], $type_info['output_type']), + ObjectBuilder::forEdge( + $this->ctx, + $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 dd0b4ed..d2cbf8a 100644 --- a/src/Codegen/Types.hack +++ b/src/Codegen/Types.hack @@ -18,9 +18,11 @@ const dict> BUILTIN_TYPES = dict[ * ?int -> IntInputType::nullable() * ?vec -> IntInputType::nonNullable()->nullableListOf() */ -function input_type(string $hack_type): string { +function input_type(string $hack_type, dict> $custom_types): string { list($unwrapped, $suffix) = unwrap_type(IO::INPUT, $hack_type); - $class = get_graphql_leaf_type($unwrapped) ?? get_input_object_type($unwrapped); + $class = get_graphql_leaf_type($unwrapped) ?? + get_input_object_type($unwrapped) ?? + get_custom_type($unwrapped, $custom_types); if ($class is null) { throw new \Error( 'GraphQL\Field argument types must be scalar or be enums/input objects annnotated with a GraphQL '. @@ -31,6 +33,14 @@ function input_type(string $hack_type): string { return Str\strip_prefix($class, 'Slack\\GraphQL\\').$suffix; } +function get_custom_type(string $hack_type, dict> $custom_types): ?string { + $custom_type = $custom_types[$hack_type] ?? null; + if ($custom_type) { + return Str\format('\%s', $custom_type); + } + return $custom_type; +} + /** * Same but for output types. By default, we ignore the nullability of the Hack type and force all GraphQL field return * types to be nullable, so that we can return `null` as the field value in the GraphQL response in case of exceptions. diff --git a/src/Request.hack b/src/Request.hack index 734c465..f44bf9b 100644 --- a/src/Request.hack +++ b/src/Request.hack @@ -1,3 +1,6 @@ + + + namespace Slack\GraphQL; use namespace Slack\GraphQL; diff --git a/src/Response.hack b/src/Response.hack index 9a2b53a..f1fc463 100644 --- a/src/Response.hack +++ b/src/Response.hack @@ -1,3 +1,6 @@ + + + namespace Slack\GraphQL; use namespace HH\Lib\Vec; diff --git a/tests/FixtureTest.hack b/tests/FixtureTest.hack index 3cb9aa6..ea5e8e0 100644 --- a/tests/FixtureTest.hack +++ b/tests/FixtureTest.hack @@ -23,6 +23,10 @@ abstract class FixtureTest extends \Facebook\HackTest\HackTest { shape( 'output_directory' => __DIR__.'/gen', 'namespace' => 'Slack\GraphQL\Test\Generated', + 'custom_types' => dict[ + Channel::class => ChannelInputType::class, + user_id_t::class => UserIdInputType::class, + ], ), ); } diff --git a/tests/Fixtures/ChannelInputType.hack b/tests/Fixtures/ChannelInputType.hack new file mode 100644 index 0000000..182b3e1 --- /dev/null +++ b/tests/Fixtures/ChannelInputType.hack @@ -0,0 +1,41 @@ + +use namespace Graphpinator\Parser\Value; +use namespace Slack\GraphQL; + +final class Channel { + public function __construct(private string $input) {} +} + +final class ChannelInputType extends GraphQL\Types\NamedType { + use GraphQL\Types\TNamedInputType; + use GraphQL\Types\TNonNullableType; + + const string NAME = 'ChannelID'; + const type THackType = Channel; + + <<__Override>> + final public function assertValidVariableValue(mixed $value): Channel { + return $value as Channel; + } + + <<__Override>> + public function coerceValue(mixed $value): Channel { + if (!$value is string) { + throw new GraphQL\UserFacingError('Expected a ChannelID, got %s', (string)$value); + } + return new Channel($value); + } + + <<__Override>> + final public function coerceNonVariableNode(Value\Value $node, dict $variable_values): Channel { + if (!$node is Value\StringLiteral) { + throw new GraphQL\UserFacingError('Expected an ChannelID literal, got %s', \get_class($node)); + } + return new Channel($node->getRawValue()); + } + + <<__Override>> + final public function getKind(): GraphQL\Introspection\__TypeKind { + return GraphQL\Introspection\__TypeKind::SCALAR; + } +} diff --git a/tests/Fixtures/Playground.hack b/tests/Fixtures/Playground.hack index e5c95b0..11b7765 100644 --- a/tests/Fixtures/Playground.hack +++ b/tests/Fixtures/Playground.hack @@ -167,7 +167,8 @@ abstract final class UserQueryAttributes { } <> - public static async function getUser(int $id): Awaitable<\User> { + public static async function getUser(user_id_t $id): Awaitable<\User> { + $id = user_id_to_int($id); return new \Human(shape('id' => $id, 'name' => 'User '.$id, 'team_id' => 1, 'is_active' => true)); } @@ -243,6 +244,11 @@ abstract final class UserMutationAttributes { 'roles' => $input['roles'] ?? vec[], )); } + + <> + public static function mutateChannel(Channel $channel): bool { + return true; + } } diff --git a/tests/Fixtures/UserIdInputType.hack b/tests/Fixtures/UserIdInputType.hack new file mode 100644 index 0000000..a0f9ec1 --- /dev/null +++ b/tests/Fixtures/UserIdInputType.hack @@ -0,0 +1,43 @@ + +use namespace Graphpinator\Parser\Value; +use namespace Slack\GraphQL; + +newtype user_id_t = int; + +function user_id_to_int(user_id_t $user_id): int { + return $user_id; +} + +final class UserIdInputType extends GraphQL\Types\NamedType { + use GraphQL\Types\TNamedInputType; + use GraphQL\Types\TNonNullableType; + + const string NAME = 'UserID'; + const type THackType = user_id_t; + + <<__Override>> + final public function assertValidVariableValue(mixed $value): user_id_t { + return $value as user_id_t; + } + + <<__Override>> + public function coerceValue(mixed $value): user_id_t { + if (!$value is int) { + throw new GraphQL\UserFacingError('Expected a UserId, got %s', (string)$value); + } + return $value; + } + + <<__Override>> + final public function coerceNonVariableNode(Value\Value $node, dict $variable_values): user_id_t { + if (!$node is Value\IntLiteral) { + throw new GraphQL\UserFacingError('Expected a UserId literal, got %s', \get_class($node)); + } + return $node->getRawValue(); + } + + <<__Override>> + final public function getKind(): GraphQL\Introspection\__TypeKind { + return GraphQL\Introspection\__TypeKind::SCALAR; + } +} diff --git a/tests/Validation/BaseValidationTest.hack b/tests/Validation/BaseValidationTest.hack index e96a01a..b4e7265 100644 --- a/tests/Validation/BaseValidationTest.hack +++ b/tests/Validation/BaseValidationTest.hack @@ -24,6 +24,10 @@ abstract class BaseValidationTest extends \Facebook\HackTest\HackTest { shape( 'output_directory' => __DIR__.'/../gen', 'namespace' => 'Slack\GraphQL\Test\Generated', + 'custom_types' => dict[ + Channel::class => ChannelInputType::class, + user_id_t::class => UserIdInputType::class, + ], ), ); } diff --git a/tests/gen/Query.hack b/tests/gen/Query.hack index 894f2c0..9bfba15 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<<483338310fc52d714e54c4e4673f1a44>> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -31,6 +31,7 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { 'human', 'introspection_test', 'list_arg_test', + 'mutateChannel', 'nested_list_sum', 'optional_field_test', 'output_type_test', @@ -263,6 +264,20 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { Types\IntType::nonNullable()->nullableInputListOf()->nonNullableInputListOf()->nullableInputListOf()->coerceNamedNode('arg', $args, $vars), ), ); + case 'mutateChannel': + return new GraphQL\FieldDefinition( + 'mutateChannel', + Types\BooleanType::nullableOutput(), + dict[ + 'channel' => shape( + 'name' => 'channel', + 'type' => \ChannelInputType::nonNullable(), + ), + ], + async ($parent, $args, $vars) ==> \UserMutationAttributes::mutateChannel( + \ChannelInputType::nonNullable()->coerceNamedNode('channel', $args, $vars), + ), + ); case 'nested_list_sum': return new GraphQL\FieldDefinition( 'nested_list_sum', @@ -319,11 +334,11 @@ final class Query extends \Slack\GraphQL\Types\ObjectType { dict[ 'id' => shape( 'name' => 'id', - 'type' => Types\IntType::nonNullable(), + 'type' => \UserIdInputType::nonNullable(), ), ], async ($parent, $args, $vars) ==> await \UserQueryAttributes::getUser( - Types\IntType::nonNullable()->coerceNamedNode('id', $args, $vars), + \UserIdInputType::nonNullable()->coerceNamedNode('id', $args, $vars), ), ); case 'viewer': diff --git a/tests/gen/Schema.hack b/tests/gen/Schema.hack index bb03384..0424cdc 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<> */ namespace Slack\GraphQL\Test\Generated; use namespace Slack\GraphQL; @@ -19,6 +19,7 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'Baz' => Baz::class, 'Boolean' => Types\BooleanType::class, 'Bot' => Bot::class, + 'ChannelID' => \ChannelInputType::class, 'Concrete' => Concrete::class, 'CreateTeamInput' => CreateTeamInput::class, 'CreateUserInput' => CreateUserInput::class, @@ -56,6 +57,7 @@ final class Schema extends \Slack\GraphQL\BaseSchema { 'User' => User::class, 'UserConnection' => UserConnection::class, 'UserEdge' => UserEdge::class, + 'UserID' => \UserIdInputType::class, '__Directive' => __Directive::class, '__EnumValue' => __EnumValue::class, '__Field' => __Field::class, diff --git a/tests/schema.json b/tests/schema.json index 4d49bcb..5a9705d 100644 --- a/tests/schema.json +++ b/tests/schema.json @@ -198,6 +198,15 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "ChannelID", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Concrete", @@ -2045,6 +2054,31 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "mutateChannel", + "args": [ + { + "name": "channel", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ChannelID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nested_list_sum", "args": [ @@ -2157,7 +2191,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "UserID", "ofType": null } }, @@ -2485,6 +2519,15 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "UserID", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "__Directive",