From 1e151da5edce78069569862c7fc3dc43a538516a Mon Sep 17 00:00:00 2001 From: Isara Ritthaworn Date: Fri, 23 Feb 2024 14:52:16 -0500 Subject: [PATCH] add hackIgnoreRefs option for preventing circular references --- src/Codegen/Constraints/SchemaBuilder.php | 15 +++- tests/IgnoreRefsValidatorTest.php | 24 ++++++ .../examples/codegen/IgnoreRefsValidator.php | 74 +++++++++++++++++++ tests/examples/ignore-refs.json | 29 ++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 tests/IgnoreRefsValidatorTest.php create mode 100644 tests/examples/codegen/IgnoreRefsValidator.php create mode 100644 tests/examples/ignore-refs.json diff --git a/src/Codegen/Constraints/SchemaBuilder.php b/src/Codegen/Constraints/SchemaBuilder.php index 9356909..cd8ad84 100644 --- a/src/Codegen/Constraints/SchemaBuilder.php +++ b/src/Codegen/Constraints/SchemaBuilder.php @@ -25,6 +25,14 @@ enum TSchemaType: string { ?'default' => mixed, ?'enum' => vec, ?'hackEnum' => string, + + // Generate a mixed type instead of following the ref. + // + // Normally you cannot use circular references in JSON Schema because Hack + // does not support them. However many other languages that use JSON Schema such as + // Typescript and Kotlin do support circular references. This allows us to generate + // circular references in those languages at the cost of generating a mixed type in Hack. + ?'hackIgnoreRef' => bool, ... ); @@ -42,7 +50,12 @@ public function __construct( protected TSchema $schema, ?CodegenClass $class = null, ) { - $ref = $schema['$ref'] ?? null; + + $ref = null; + if (!($schema['hackIgnoreRef'] ?? false)) { + $ref = $schema['$ref'] ?? null; + } + $new_ctx = clone $this->ctx; // Resolve refs until we get to an actual schema diff --git a/tests/IgnoreRefsValidatorTest.php b/tests/IgnoreRefsValidatorTest.php new file mode 100644 index 0000000..b89d4c2 --- /dev/null +++ b/tests/IgnoreRefsValidatorTest.php @@ -0,0 +1,24 @@ +> + public static async function beforeFirstTestAsync(): Awaitable { + $ret = self::getBuilder('ignore-refs.json', 'IgnoreRefsValidator'); + $ret['codegen']->build(); + require_once($ret['path']); + } + + public function testInvalidEmptyInput(): void { + $validator = new IgnoreRefsValidator(dict['randomprop' => 'IanIzzy']); + $validator->validate(); + expect($validator->isValid())->toBeTrue(); + } +} diff --git a/tests/examples/codegen/IgnoreRefsValidator.php b/tests/examples/codegen/IgnoreRefsValidator.php new file mode 100644 index 0000000..6b58685 --- /dev/null +++ b/tests/examples/codegen/IgnoreRefsValidator.php @@ -0,0 +1,74 @@ +> + */ +namespace Slack\Hack\JsonSchema\Tests\Generated; +use namespace Slack\Hack\JsonSchema; +use namespace Slack\Hack\JsonSchema\Constraints; + +type TIgnoreRefsValidatorPropertiesRandomprop = mixed; + +type TIgnoreRefsValidator = shape( + ?'randomprop' => TIgnoreRefsValidatorPropertiesRandomprop, + ... +); + +final class IgnoreRefsValidatorPropertiesRandomprop { + + public static function check( + mixed $input, + string $_pointer, + ): TIgnoreRefsValidatorPropertiesRandomprop { + return $input; + } +} + +final class IgnoreRefsValidator + extends JsonSchema\BaseValidator { + + private static bool $coerce = false; + + public static function check( + mixed $input, + string $pointer, + ): TIgnoreRefsValidator { + $typed = Constraints\ObjectConstraint::check($input, $pointer, self::$coerce); + + $errors = vec[]; + $output = shape(); + + /*HHAST_IGNORE_ERROR[UnusedVariable] Some loops generated with this statement do not use their $value*/ + foreach ($typed as $key => $value) { + /* HH_IGNORE_ERROR[4051] allow dynamic access to preserve input. See comment in the codegen lib for reasoning and alternatives if needed. */ + $output[$key] = $value; + } + + if (\HH\Lib\C\contains_key($typed, 'randomprop')) { + try { + $output['randomprop'] = IgnoreRefsValidatorPropertiesRandomprop::check( + $typed['randomprop'], + JsonSchema\get_pointer($pointer, 'randomprop'), + ); + } catch (JsonSchema\InvalidFieldException $e) { + $errors = \HH\Lib\Vec\concat($errors, $e->errors); + } + } + + if (\HH\Lib\C\count($errors)) { + throw new JsonSchema\InvalidFieldException($pointer, $errors); + } + + /* HH_IGNORE_ERROR[4163] */ + return $output; + } + + <<__Override>> + protected function process(): TIgnoreRefsValidator { + return self::check($this->input, $this->pointer); + } +} diff --git a/tests/examples/ignore-refs.json b/tests/examples/ignore-refs.json new file mode 100644 index 0000000..8826a8d --- /dev/null +++ b/tests/examples/ignore-refs.json @@ -0,0 +1,29 @@ +{ + "type": "object", + "properties": { + "randomprop": { + "$ref": "#/definitions/objectRef", + "hackIgnoreRef": true + } + }, + "definitions": { + "otherRef": { + "$ref": "#/definitions/anotherRef" + }, + "anotherRef": { + "$ref": "#/definitions/otherRef" + }, + "objectRef": { + "type": "object", + "properties": { + "thing": { + "$ref": "#/definitions/objectRef2" + } + + } + }, + "objectRef2": { + "$ref": "#/definitions/objectRef" + } + } +}