Skip to content

Commit bb7dffe

Browse files
committed
Support type inference for oneOf schemas
Support for type inference for non-shape types in oneOf constraints. Shapes are still unsupported, just as they are unsupported for anyOf. They're a fair bit of work to get right and will come later, if we deem that worthwhile.
1 parent 06164dc commit bb7dffe

7 files changed

+352
-13
lines changed

src/Codegen/Constraints/UntypedBuilder.php

+16-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
HackBuilderKeys,
1212
HackBuilderValues,
1313
};
14+
use type Slack\Hack\JsonSchema\Sentinal;
1415

1516
type TUntypedSchema = shape(
1617
?'anyOf' => vec<TSchema>,
@@ -141,24 +142,35 @@ private function generateNotChecks(vec<TSchema> $schemas, HackBuilder $hb): void
141142

142143
}
143144

144-
// TODO: Determine Lowest Upper Bound for oneOf constraint.
145145
private function generateOneOfChecks(vec<TSchema> $schemas, HackBuilder $hb): void {
146146
$constraints = vec[];
147+
$types = vec[];
147148
foreach ($schemas as $index => $schema) {
148149
$schema_builder =
149150
new SchemaBuilder($this->ctx, $this->generateClassName($this->suffix, 'oneOf', (string)$index), $schema);
150151
$schema_builder->build();
151152
$constraints[] = "{$schema_builder->getClassName()}::check<>";
153+
$types[] = $schema_builder->getTypeInfo();
152154
}
153155

156+
$type_info = Typing\TypeSystem::union($types);
157+
// For now, keep upcasting nonnull to mixed.
158+
// This is a temporary cludge to reduce the amount of code changed by generating unions.
159+
// TODO: Stop doing the above.
160+
if ($type_info is Typing\ConcreteType && $type_info->getConcreteTypeName() === Typing\ConcreteTypeName::NONNULL) {
161+
$type_info = Typing\TypeSystem::mixed();
162+
}
163+
$this->type_info = $type_info;
164+
154165
$hb
155166
->addAssignment('$constraints', $constraints, HackBuilderValues::vec(HackBuilderValues::literal()))
156167
->ensureEmptyLine();
157168

158169
$hb
159170
->addAssignment('$passed_any', false, HackBuilderValues::export())
160171
->addAssignment('$passed_multi', false, HackBuilderValues::export())
161-
->addAssignment('$output', null, HackBuilderValues::export())
172+
// Use `sentinal` rather than `null` so that it's obvious when we failed to match any constraint.
173+
->addAssignment('$output', Str\format('new \%s()', Sentinal::class), HackBuilderValues::literal())
162174
->startForeachLoop('$constraints', null, '$constraint')
163175
->startTryBlock()
164176
->addMultilineCall('$output = $constraint', vec['$input', '$pointer'])
@@ -181,7 +193,7 @@ private function generateOneOfChecks(vec<TSchema> $schemas, HackBuilder $hb): vo
181193
);
182194

183195
$hb
184-
->startIfBlock('$passed_multi || !$passed_any')
196+
->startIfBlockf('$passed_multi || !$passed_any || $output is \%s', Sentinal::class)
185197
->addAssignment(
186198
'$error',
187199
$error,
@@ -452,7 +464,7 @@ private function getOptimizedAnyOfTypes(vec<SchemaBuilder> $schema_builders): ?T
452464

453465
if ($property_type === TSchemaType::STRING_T && C\contains($required, $property_name)) {
454466
$typed_property_schema =
455-
type_assert_shape($property_schema, 'Slack\Hack\JsonSchema\Codegen\TStringSchema');
467+
type_assert_type($property_schema, \Slack\Hack\JsonSchema\Codegen\TStringSchema::class);
456468

457469
$enum = $typed_property_schema['enum'] ?? null;
458470
if ($enum is nonnull && C\count($enum) === 1) {

src/Sentinal.hack

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Slack\Hack\JsonSchema;
2+
3+
/**
4+
* Represents an unset value.
5+
*/
6+
final class Sentinal {}

tests/DiscardAddititionalPropertiesValidatorTest.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ final class DiscardAddititionalPropertiesValidatorTest extends BaseCodegenTestCa
1010

1111
<<__Override>>
1212
public static async function beforeFirstTestAsync(): Awaitable<void> {
13-
$ret = self::getBuilder(
14-
'discard-additional-properties-schema.json',
15-
'DiscardAddititionalPropertiesValidator',
16-
);
13+
$ret = self::getBuilder('discard-additional-properties-schema.json', 'DiscardAddititionalPropertiesValidator');
1714
$ret['codegen']->build();
1815
require_once($ret['path']);
1916
}

tests/UntypedSchemaValidatorTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,36 @@ public function testAnyOfOptimizedEnumCoercionInvalid(): void {
202202
expect($error['pointer'] ?? null)->toBeSame('/any_of_optimized_enum');
203203
}
204204

205+
public function testOneOfNullableString(): void {
206+
$this->expectCases(
207+
vec[
208+
shape('input' => 'foo', 'output' => dict['one_of_nullable_string' => 'foo'], 'valid' => true),
209+
shape('input' => null, 'output' => dict['one_of_nullable_string' => null], 'valid' => true),
210+
shape('input' => true, 'valid' => false),
211+
],
212+
$input ==> new UntypedSchemaValidator(dict['one_of_nullable_string' => $input]),
213+
);
214+
}
215+
216+
public function testOneOfStringAndInt(): void {
217+
$this->expectCases(
218+
vec[
219+
shape('input' => 'foo', 'output' => dict['one_of_string_and_int' => 'foo'], 'valid' => true),
220+
shape('input' => 3, 'output' => dict['one_of_string_and_int' => 3], 'valid' => true),
221+
shape('input' => true, 'valid' => false),
222+
],
223+
$input ==> new UntypedSchemaValidator(dict['one_of_string_and_int' => $input]),
224+
);
225+
}
226+
227+
public function testOneOfStringAndVec(): void {
228+
$this->expectCases(
229+
vec[
230+
shape('input' => 'foo', 'output' => dict['one_of_string_and_vec' => 'foo'], 'valid' => true),
231+
shape('input' => vec[3], 'output' => dict['one_of_string_and_vec' => vec[3]], 'valid' => true),
232+
shape('input' => 3, 'valid' => false),
233+
],
234+
$input ==> new UntypedSchemaValidator(dict['one_of_string_and_vec' => $input]),
235+
);
236+
}
205237
}

tests/examples/codegen/AddressSchemaValidator.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* To re-generate this file run `make test`
66
*
77
*
8-
* @generated SignedSource<<f55b7686ddc5695a203819aec8bb8742>>
8+
* @generated SignedSource<<884da88463b70fac29468fab117c4e1b>>
99
*/
1010
namespace Slack\Hack\JsonSchema\Tests\Generated;
1111
use namespace Slack\Hack\JsonSchema;
@@ -23,7 +23,7 @@
2323

2424
type TAddressSchemaValidatorPropertiesLongitude = mixed;
2525

26-
type TAddressSchemaValidatorPropertiesLatitude = mixed;
26+
type TAddressSchemaValidatorPropertiesLatitude = num;
2727

2828
type TAddressSchemaValidator = shape(
2929
?'post-office-box' => string,
@@ -425,7 +425,7 @@ public static function check(
425425

426426
$passed_any = false;
427427
$passed_multi = false;
428-
$output = null;
428+
$output = new \Slack\Hack\JsonSchema\Sentinal();
429429
foreach ($constraints as $constraint) {
430430
try {
431431
$output = $constraint($input, $pointer);
@@ -438,7 +438,7 @@ public static function check(
438438
}
439439
}
440440

441-
if ($passed_multi || !$passed_any) {
441+
if ($passed_multi || !$passed_any || $output is \Slack\Hack\JsonSchema\Sentinal) {
442442
$error = shape(
443443
'code' => JsonSchema\FieldErrorCode::FAILED_CONSTRAINT,
444444
'constraint' => shape(

0 commit comments

Comments
 (0)