Skip to content

Commit 54f3ad2

Browse files
authored
Add support for generating hack enums from json def (#81)
* wip * wip * no need to double add hack constraint * invariant. skip reflection. * remove leading slashes. * tests. * revert unneeded change. * hackfmt * invariant for slash * tabs -> 2 spaces. * typo in json * darray -> dict
1 parent de4af98 commit 54f3ad2

File tree

5 files changed

+202
-40
lines changed

5 files changed

+202
-40
lines changed

src/Codegen/Constraints/BaseBuilder.php

+43-37
Original file line numberDiff line numberDiff line change
@@ -108,56 +108,62 @@ protected function addHackEnumConstraintCheck(HackBuilder $hb): void {
108108
if (!Shapes::keyExists($schema, 'hackEnum')) {
109109
return;
110110
}
111+
$generateHackEnum = $schema['generateHackEnum'] ?? false;
112+
if (!$generateHackEnum) {
111113

112-
try {
113-
$rc = new \ReflectionClass($schema['hackEnum']);
114-
} catch (\ReflectionException $e) {
115-
throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum']));
116-
}
117-
118-
invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']);
119-
120-
$schema_type = $schema['type'] ?? null;
121-
$hack_enum_values = keyset[];
122-
foreach ($rc->getConstants() as $hack_enum_value) {
123-
if ($schema_type === TSchemaType::INTEGER_T) {
124-
$hack_enum_value = $hack_enum_value ?as int;
125-
} else {
126-
$hack_enum_value = $hack_enum_value ?as string;
114+
try {
115+
$rc = new \ReflectionClass($schema['hackEnum']);
116+
} catch (\ReflectionException $e) {
117+
throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum']));
127118
}
128-
invariant(
129-
$hack_enum_value is nonnull,
130-
"'%s' must contain only values of type %s",
131-
$rc->getName(),
132-
$schema_type === TSchemaType::INTEGER_T ? 'int' : 'string',
133-
);
134-
$hack_enum_values[] = $hack_enum_value;
135-
}
136119

137-
if (Shapes::keyExists($schema, 'enum')) {
138-
// If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of
139-
// `hackEnum` values. Any value not also in `hackEnum` can't be valid.
140-
foreach ($schema['enum'] as $enum_value) {
141-
invariant(
142-
$enum_value is string,
143-
"Enum value '%s' is not a valid value for '%s'",
144-
\print_r($enum_value, true),
145-
$rc->getName(),
146-
);
120+
invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']);
121+
122+
$schema_type = $schema['type'] ?? null;
123+
$hack_enum_values = keyset[];
124+
foreach ($rc->getConstants() as $hack_enum_value) {
125+
if ($schema_type === TSchemaType::INTEGER_T) {
126+
$hack_enum_value = $hack_enum_value ?as int;
127+
} else {
128+
$hack_enum_value = $hack_enum_value ?as string;
129+
}
147130
invariant(
148-
C\contains_key($hack_enum_values, $enum_value),
149-
"Enum value '%s' is unexpectedly not present in '%s'",
150-
\print_r($enum_value, true),
131+
$hack_enum_value is nonnull,
132+
"'%s' must contain only values of type %s",
151133
$rc->getName(),
134+
$schema_type === TSchemaType::INTEGER_T ? 'int' : 'string',
152135
);
136+
$hack_enum_values[] = $hack_enum_value;
137+
}
138+
139+
if (Shapes::keyExists($schema, 'enum')) {
140+
// If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of
141+
// `hackEnum` values. Any value not also in `hackEnum` can't be valid.
142+
foreach ($schema['enum'] as $enum_value) {
143+
invariant(
144+
$enum_value is string,
145+
"Enum value '%s' is not a valid value for '%s'",
146+
\print_r($enum_value, true),
147+
$rc->getName(),
148+
);
149+
invariant(
150+
C\contains_key($hack_enum_values, $enum_value),
151+
"Enum value '%s' is unexpectedly not present in '%s'",
152+
\print_r($enum_value, true),
153+
$rc->getName(),
154+
);
155+
}
153156
}
157+
$enum_name = Str\format('\%s::class', $rc->getName());
158+
} else {
159+
$enum_name = $schema['hackEnum'].'::class';
154160
}
155161

156162
$hb->addMultilineCall(
157163
'$typed = Constraints\HackEnumConstraint::check',
158164
vec[
159165
'$typed',
160-
Str\format('\%s::class', $rc->getName()),
166+
$enum_name,
161167
'$pointer',
162168
],
163169
);

src/Codegen/Constraints/StringBuilder.php

+28-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
?'minLength' => int,
1313
?'enum' => vec<string>,
1414
?'hackEnum' => string,
15+
?'generateHackEnum' => bool,
1516
?'pattern' => string,
1617
?'format' => string,
1718
?'sanitize' => shape(
@@ -59,8 +60,28 @@ public function build(): this {
5960
}
6061

6162
$enum = $this->getEnumCodegenProperty();
63+
$generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false;
6264
if ($enum is nonnull) {
63-
$properties[] = $enum;
65+
if ($generateHackEnum) {
66+
$enum = $this->typed_schema['enum'] ?? vec[];
67+
$factory = $this->ctx->getHackCodegenFactory();
68+
$members = \HH\Lib\Vec\map(
69+
$enum,
70+
$member ==> $factory->codegenEnumMember(Str\uppercase($member))
71+
->setValue($member, HackBuilderValues::export()),
72+
);
73+
$enumName = $this->typed_schema['hackEnum'] ?? null;
74+
invariant($enumName is string, 'hackEnum is required when generating hack enum.');
75+
invariant(!Str\contains($enumName, '\\'), 'hackEnum must not contain a slash.');
76+
$enum_obj = $factory->codegenEnum($enumName, 'string')
77+
->addMembers($members)
78+
->setIsAs('string');
79+
$this->ctx->getFile()->addEnum($enum_obj);
80+
} else {
81+
$properties[] = $enum;
82+
}
83+
} else {
84+
invariant(!$generateHackEnum, 'enum is required when generating hack enum');
6485
}
6586

6687
$coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault();
@@ -99,8 +120,9 @@ protected function getCheckMethod(): CodegenMethod {
99120
->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal())
100121
->ensureEmptyLine();
101122
}
102-
103-
$this->addEnumConstraintCheck($hb);
123+
if (!($this->typed_schema['generateHackEnum'] ?? false)) {
124+
$this->addEnumConstraintCheck($hb);
125+
}
104126

105127
$max_length = $this->typed_schema['maxLength'] ?? null;
106128
$min_length = $this->typed_schema['minLength'] ?? null;
@@ -146,6 +168,9 @@ protected function getCheckMethod(): CodegenMethod {
146168
<<__Override>>
147169
public function getType(): string {
148170
if (Shapes::keyExists($this->typed_schema, 'hackEnum')) {
171+
if ($this->typed_schema['generateHackEnum'] ?? false) {
172+
return $this->typed_schema['hackEnum'];
173+
}
149174
return Str\format('\%s', $this->typed_schema['hackEnum']);
150175
}
151176

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?hh // strict
2+
3+
namespace Slack\Hack\JsonSchema\Tests;
4+
5+
6+
use type Slack\Hack\JsonSchema\Tests\Generated\GeneratedHackEnumSchemaValidator;
7+
8+
final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase {
9+
10+
<<__Override>>
11+
public static async function beforeFirstTestAsync(): Awaitable<void> {
12+
$ret = self::getBuilder('generated-hack-enum-schema.json', 'GeneratedHackEnumSchemaValidator');
13+
$ret['codegen']->build();
14+
require_once($ret['path']);
15+
}
16+
public function testStringEnum(): void {
17+
$cases = vec[
18+
shape(
19+
'input' => dict['enum_string' => 'one'],
20+
'output' => dict['enum_string' => 'one'],
21+
'valid' => true,
22+
),
23+
shape(
24+
'input' => dict['enum_string' => 'four'],
25+
'valid' => false,
26+
),
27+
shape(
28+
'input' => dict['enum_string' => 1],
29+
'valid' => false,
30+
),
31+
];
32+
33+
$this->expectCases($cases, $input ==> new GeneratedHackEnumSchemaValidator($input));
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?hh // strict
2+
/**
3+
* This file is generated. Do not modify it manually!
4+
*
5+
* To re-generate this file run `make test`
6+
*
7+
*
8+
* @generated SignedSource<<ff65e010c6ed29ad1f95451eac4f8a17>>
9+
*/
10+
namespace Slack\Hack\JsonSchema\Tests\Generated;
11+
use namespace Slack\Hack\JsonSchema;
12+
use namespace Slack\Hack\JsonSchema\Constraints;
13+
14+
type TGeneratedHackEnumSchemaValidator = shape(
15+
?'enum_string' => myCoolTestEnum,
16+
...
17+
);
18+
19+
20+
enum myCoolTestEnum : string as string {
21+
ONE = 'one';
22+
TWO = 'two';
23+
THREE = 'three';
24+
}
25+
26+
final class GeneratedHackEnumSchemaValidatorPropertiesEnumString {
27+
28+
private static bool $coerce = false;
29+
30+
public static function check(mixed $input, string $pointer): myCoolTestEnum {
31+
$typed = Constraints\StringConstraint::check($input, $pointer, self::$coerce);
32+
33+
$typed = Constraints\HackEnumConstraint::check(
34+
$typed,
35+
myCoolTestEnum::class,
36+
$pointer,
37+
);
38+
return $typed;
39+
}
40+
}
41+
42+
final class GeneratedHackEnumSchemaValidator
43+
extends JsonSchema\BaseValidator<TGeneratedHackEnumSchemaValidator> {
44+
45+
private static bool $coerce = false;
46+
47+
public static function check(
48+
mixed $input,
49+
string $pointer,
50+
): TGeneratedHackEnumSchemaValidator {
51+
$typed = Constraints\ObjectConstraint::check($input, $pointer, self::$coerce);
52+
53+
$errors = vec[];
54+
$output = shape();
55+
56+
/*HHAST_IGNORE_ERROR[UnusedVariable] Some loops generated with this statement do not use their $value*/
57+
foreach ($typed as $key => $value) {
58+
/* HH_IGNORE_ERROR[4051] allow dynamic access to preserve input. See comment in the codegen lib for reasoning and alternatives if needed. */
59+
$output[$key] = $value;
60+
}
61+
62+
if (\HH\Lib\C\contains_key($typed, 'enum_string')) {
63+
try {
64+
$output['enum_string'] = GeneratedHackEnumSchemaValidatorPropertiesEnumString::check(
65+
$typed['enum_string'],
66+
JsonSchema\get_pointer($pointer, 'enum_string'),
67+
);
68+
} catch (JsonSchema\InvalidFieldException $e) {
69+
$errors = \HH\Lib\Vec\concat($errors, $e->errors);
70+
}
71+
}
72+
73+
if (\HH\Lib\C\count($errors)) {
74+
throw new JsonSchema\InvalidFieldException($pointer, $errors);
75+
}
76+
77+
/* HH_IGNORE_ERROR[4163] */
78+
return $output;
79+
}
80+
81+
<<__Override>>
82+
protected function process(): TGeneratedHackEnumSchemaValidator {
83+
return self::check($this->input, $this->pointer);
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"enum_string": {
5+
"type": "string",
6+
"enum": ["one", "two", "three"],
7+
"generateHackEnum": true,
8+
"hackEnum": "myCoolTestEnum"
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)