Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Codegen/Builders/CompositeBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@ 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<FieldBuilder> $fields,
protected dict<string, vec<string>> $directives,
) {
parent::__construct($type_info, $hack_type);
}

public function build(HackCodegenFactory $cg): CodegenClass {
return parent::build($cg)
->addMethod($this->generateGetFieldDefinition($cg))
->addMethod($this->generateGetDirectives($cg))
->addConstant($this->generateFieldNamesConstant($cg, $this->getFieldNames()));
}

Expand Down Expand Up @@ -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<GraphQL\ObjectDirective>')
->setBody($hb->getCode());
}

final public function getFieldNames(): keyset<string> {
return Keyset\map($this->fields, $field ==> $field->getName())
|> Keyset\filter($$, $name ==> !Str\starts_with($name, '__'));
Expand Down
25 changes: 25 additions & 0 deletions src/Codegen/Builders/DirectivesBuilder.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@



namespace Slack\GraphQL\Codegen;

use namespace HH\Lib\Str;
use type Facebook\HackCodegen\HackBuilder;

trait DirectivesBuilder {
protected dict<string, vec<string>> $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;
}
}
22 changes: 19 additions & 3 deletions src/Codegen/Builders/Fields/FieldBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ 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,
'output_type' => shape('type' => string, ?'needs_await' => bool),
'directives' => dict<string, vec<string>>,
...
);

Expand All @@ -23,14 +25,19 @@ abstract class FieldBuilder {

// Constructors

public function __construct(protected this::TField $data) {}
protected dict<string, vec<string>> $directives;

public function __construct(protected this::TField $data) {
$this->directives = $data['directives'];
}

/**
* Construct a GraphQL field from a Hack method.
*/
public static function fromReflectionMethod(
\Slack\GraphQL\Field $field,
\ReflectionMethod $rm,
dict<string, vec<string>> $directives,
bool $is_root_field = false,
): FieldBuilder {
$data = shape(
Expand All @@ -54,6 +61,7 @@ abstract class FieldBuilder {
return $data;
},
),
'directives' => $directives,
);

if ($is_root_field) {
Expand All @@ -75,14 +83,19 @@ 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' => dict[],
));
}

/**
* 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(
\Slack\GraphQL\Field $field,
\ReflectionMethod $rm,
dict<string, vec<string>> $directives,
): FieldBuilder {
return FieldBuilder::fromReflectionMethod($field, $rm, $directives, true);
}

public static function introspectSchemaField(): FieldBuilder {
Expand Down Expand Up @@ -126,6 +139,9 @@ abstract class FieldBuilder {
$this->generateResolverBody($hb);
$hb->addLine(',');

// Field directives
$hb = $this->buildDirectives($hb)->addLine(',');

// End of new GraphQL\FieldDefinition(
$hb->unindent()->addLine(');');
$hb->unindent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ final class IntrospectSchemaFieldBuilder extends FieldBuilder {
const type TField = shape(
'name' => string,
'output_type' => shape('type' => string, ?'needs_await' => bool),
'directives' => dict<string, vec<string>>,
...
);

public function __construct() {
parent::__construct(shape(
'name' => '__schema',
'output_type' => shape('type' => '__Schema::nullableOutput()'),
'directives' => dict[],
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class IntrospectTypeFieldBuilder extends MethodFieldBuilder {
'parameters' => vec[
shape('name' => 'name', 'type' => 'HH\string', 'is_optional' => false),
],
'directives' => dict[],
));
}
<<__Override>>
Expand Down
1 change: 1 addition & 0 deletions src/Codegen/Builders/Fields/MethodFieldBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class MethodFieldBuilder extends FieldBuilder {
'method_name' => string,
'output_type' => shape('type' => string, ?'needs_await' => bool),
'parameters' => vec<Parameter>,
'directives' => dict<string, vec<string>>,
?'root_field_for_type' => string,
?'is_static' => bool,
);
Expand Down
1 change: 1 addition & 0 deletions src/Codegen/Builders/Fields/ShapeFieldBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ final class ShapeFieldBuilder extends FieldBuilder {
'name' => string,
'output_type' => shape('type' => string, ?'needs_await' => bool),
'is_optional' => bool,
'directives' => dict<string, vec<string>>,
);

<<__Override>>
Expand Down
3 changes: 2 additions & 1 deletion src/Codegen/Builders/InterfaceBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ final class InterfaceBuilder extends CompositeBuilder {
\Slack\GraphQL\__Private\CompositeType $type_info,
string $hack_type,
vec<FieldBuilder> $fields,
dict<string, vec<string>> $directives,
private dict<string, string> $hack_class_to_graphql_object,
) {
parent::__construct($type_info, $hack_type, $fields);
parent::__construct($type_info, $hack_type, $fields, $directives);
}

<<__Override>>
Expand Down
24 changes: 18 additions & 6 deletions src/Codegen/Builders/ObjectBuilder.hack
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ class ObjectBuilder extends CompositeBuilder {
\Slack\GraphQL\__Private\CompositeType $type_info,
string $hack_type,
vec<FieldBuilder> $fields,
dict<string, vec<string>> $directives,
private dict<string, string> $hack_class_to_graphql_interface,
) {
parent::__construct($type_info, $hack_type, $fields);
parent::__construct($type_info, $hack_type, $fields, $directives);
}

<<__Override>>
Expand All @@ -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')
Expand All @@ -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<string, vec<string>> $directives,
): ObjectBuilder {
// Remove namespace to generate a sane GQL name
// This means that connections in different namespaces can collide with each other;
// we could eventually fix that by merging the namespace and GQL name when
Expand All @@ -81,14 +87,17 @@ class ObjectBuilder extends CompositeBuilder {
'needs_await' => true,
),
'parameters' => vec[],
'directives' => dict[],
)),
new MethodFieldBuilder(shape(
'name' => 'pageInfo',
'method_name' => 'getPageInfo',
'output_type' => shape('type' => 'PageInfo::nullableOutput()', 'needs_await' => true),
'parameters' => vec[],
'directives' => dict[],
)),
],
$directives,
dict[], // Connections do not implement any interfaces
);
}
Expand All @@ -105,14 +114,17 @@ class ObjectBuilder extends CompositeBuilder {
'method_name' => 'getNode',
'output_type' => shape('type' => $output_type.'::nullableOutput()'),
'parameters' => vec[],
'directives' => dict[],
)),
new MethodFieldBuilder(shape(
'name' => 'cursor',
'method_name' => 'getCursor',
'output_type' => shape('type' => 'Types\StringType::nullableOutput()'),
'parameters' => vec[],
'directives' => dict[],
)),
],
dict[], // If we support custom edges, we'll want to support adding directives to them
dict[],
);
}
Expand Down
61 changes: 61 additions & 0 deletions src/Codegen/DirectivesFinder.hack
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@



namespace Slack\GraphQL\Codegen;

use namespace HH\Lib\{C, Dict, Str, Vec};

final class DirectivesFinder {
public function __construct(
private shape(
?'fields' => keyset<classname<\Slack\GraphQL\FieldDirective>>,
?'objects' => keyset<classname<\Slack\GraphQL\ObjectDirective>>,
) $directives,
private dict<string, string> $hack_class_to_graphql_interface,
) {}

public function findDirectivesForField(\ReflectionMethod $rm): dict<string, vec<string>> {
return self::findDirectives(
$this->directives['fields'] ?? keyset[],
$directive_type ==> $rm->getAttributeClass($directive_type),
);
}

public function findDirectivesForObject(\ReflectionClass $rc): dict<string, vec<string>> {
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<string, vec<string>> {
$rc = new \ReflectionClass($object_name);
return self::findDirectives(
$this->directives['objects'] ?? keyset[],
$directive_type ==> $rc->getAttributeClass($directive_type),
);
}

private static function findDirectives<T as \Slack\GraphQL\Directive>(
keyset<classname<T>> $custom_directive_types,
(function(classname<T>): ?T) $getter,
): dict<string, vec<string>> {
$directives = dict[];

foreach ($custom_directive_types as $directive_type) {
$directive = $getter($directive_type);
if ($directive is nonnull) {
$rc = new \ReflectionClass($directive);
$directives[$rc->getName()] = $directive->formatArgs();
}
}

return $directives;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's easy to miss, but this is what the generated code will look like when we add it to each field: https://github.com/mwildehahn/hack-graphql/pull/123/files#diff-bc6416c0810e5e09cdcf817fbe7fc23de76a1884523575597eb7a6b4bc6b9861R41-R51

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend always generating some of the code as part of the PR so that people can see and review the generated code as if it were human-written. Generally speaking it's best to hold generated code to the same high standards as human-written code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally. Do you mean that I should include generated code beyond the code this PR includes currently?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I meant that I could not identify by looking at your link that it was a link to lower in this PR. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I'd recommend reviewing this whole file: https://github.com/mwildehahn/hack-graphql/pull/123/files#diff-bc6416c0810e5e09cdcf817fbe7fc23de76a1884523575597eb7a6b4bc6b9861

It contains generated code for object and field-level directives.

}
}
9 changes: 7 additions & 2 deletions src/Codegen/FieldResolver.hack
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ final class FieldResolver {
private dict<string, DefinitionFinder\ScannedClassish> $scanned_classes;
private dict<string, dict<string, FieldBuilder>> $resolved_fields = dict[];

public function __construct(vec<DefinitionFinder\ScannedClassish> $classes) {
public function __construct(
vec<DefinitionFinder\ScannedClassish> $classes,
private DirectivesFinder $directives_finder,
) {
$this->scanned_classes = Dict\from_values($classes, $class ==> $class->getName());
}

Expand Down Expand Up @@ -69,7 +72,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 = $this->directives_finder->findDirectivesForField($rm);

$fields[$graphql_field->getName()] = FieldBuilder::fromReflectionMethod($graphql_field, $rm, $directives);
}

return $fields;
Expand Down
Loading