From e3ae0a359caee94186b2fb44973f153d61b39b5a Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Mon, 18 Dec 2023 10:00:34 -0800 Subject: [PATCH 01/12] wip --- src/Codegen/Constraints/BaseBuilder.php | 352 +++++++++--------- src/Codegen/Constraints/StringBuilder.php | 301 +++++++-------- .../GeneratedHackEnumSchemaValidatorTest.php | 16 + tests/StringSchemaValidatorTest.php | 3 +- 4 files changed, 352 insertions(+), 320 deletions(-) create mode 100644 tests/GeneratedHackEnumSchemaValidatorTest.php diff --git a/src/Codegen/Constraints/BaseBuilder.php b/src/Codegen/Constraints/BaseBuilder.php index 7e29a22..ea66c49 100644 --- a/src/Codegen/Constraints/BaseBuilder.php +++ b/src/Codegen/Constraints/BaseBuilder.php @@ -3,175 +3,191 @@ namespace Slack\Hack\JsonSchema\Codegen; use namespace HH\Lib\{C, Str}; -use type Facebook\HackCodegen\{CodegenClass, CodegenMethod, CodegenProperty, HackBuilder, HackBuilderValues}; +use type Facebook\HackCodegen\{ + CodegenClass, + // CodegenEnum, + CodegenEnumMember, + CodegenMethod, + CodegenProperty, + HackBuilder, + HackBuilderValues, +}; <<__ConsistentConstruct>> abstract class BaseBuilder implements IBuilder { - use Factory; - - protected T $typed_schema; - protected static string $schema_name = ''; - - public function __construct( - protected Context $ctx, - protected string $suffix, - protected TSchema $schema, - protected ?CodegenClass $class = null, - ) { - $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); - } - - /** - * - * Return a string which will get rendered as a literal value describing the - * output type of the schema. For example, the `StringBuilder` would return: - * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. - */ - abstract public function getType(): string; - - public function isArrayKeyType(): bool { - $type = $this->getType(); - if ($type === 'string' || $type === 'int') { - return true; - } - - $schema = type_assert_type($this->typed_schema, TSchema::class); - return Shapes::keyExists($schema, 'hackEnum'); - } - - /** - * - * Main method for building the class for the schema and appending it to the - * file. - */ - abstract public function build(): this; - - protected function getHackBuilder(): HackBuilder { - return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); - } - - public function getClassName(): string { - return $this->generateClassName($this->ctx->getClassName(), $this->suffix); - } - - protected function codegenClass(): CodegenClass { - if ($this->class) { - return $this->class; - } - - return $this->ctx - ->getHackCodegenFactory() - ->codegenClass($this->getClassName()) - ->setIsFinal(true); - } - - protected function codegenProperty(string $name): CodegenProperty { - return $this->ctx - ->getHackCodegenFactory() - ->codegenProperty($name) - ->setIsStatic(true) - ->setPrivate(); - } - - protected function codegenCheckMethod(): CodegenMethod { - return $this->ctx - ->getHackCodegenFactory() - ->codegenMethod('check') - ->setPublic() - ->setIsStatic(true); - } - - protected function getEnumCodegenProperty(): ?CodegenProperty { - $property = null; - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - $enum = $schema['enum'] ?? null; - if ($enum is nonnull) { - $hb = $this->getHackBuilder() - ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); - - $property = $this->codegenProperty('enum') - ->setType('vec') - ->setValue($hb->getCode(), HackBuilderValues::literal()); - } - return $property; - } - - protected function addEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - if (($schema['enum'] ?? null) is nonnull) { - $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); - } - } - - protected function addHackEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_type($this->typed_schema, TSchema::class); - if (!Shapes::keyExists($schema, 'hackEnum')) { - return; - } - - try { - $rc = new \ReflectionClass($schema['hackEnum']); - } catch (\ReflectionException $e) { - throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); - } - - invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); - - $schema_type = $schema['type'] ?? null; - $hack_enum_values = keyset[]; - foreach ($rc->getConstants() as $hack_enum_value) { - if ($schema_type === TSchemaType::INTEGER_T) { - $hack_enum_value = $hack_enum_value ?as int; - } else { - $hack_enum_value = $hack_enum_value ?as string; - } - invariant( - $hack_enum_value is nonnull, - "'%s' must contain only values of type %s", - $rc->getName(), - $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', - ); - $hack_enum_values[] = $hack_enum_value; - } - - if (Shapes::keyExists($schema, 'enum')) { - // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of - // `hackEnum` values. Any value not also in `hackEnum` can't be valid. - foreach ($schema['enum'] as $enum_value) { - invariant( - $enum_value is string, - "Enum value '%s' is not a valid value for '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - invariant( - C\contains_key($hack_enum_values, $enum_value), - "Enum value '%s' is unexpectedly not present in '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - } - } - - $hb->addMultilineCall( - '$typed = Constraints\HackEnumConstraint::check', - vec[ - '$typed', - Str\format('\%s::class', $rc->getName()), - '$pointer', - ], - ); - } - - public function addBuilderClass(CodegenClass $class): void { - if ($this->class) { - return; - } - - $this->ctx->getFile()->addClass($class); - } - - public function setSuffix(string $suffix): void { - $this->suffix = $suffix; - } + use Factory; + + protected T $typed_schema; + protected static string $schema_name = ''; + + public function __construct( + protected Context $ctx, + protected string $suffix, + protected TSchema $schema, + protected ?CodegenClass $class = null, + ) { + $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); + } + + /** + * + * Return a string which will get rendered as a literal value describing the + * output type of the schema. For example, the `StringBuilder` would return: + * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. + */ + abstract public function getType(): string; + + public function isArrayKeyType(): bool { + $type = $this->getType(); + if ($type === 'string' || $type === 'int') { + return true; + } + + $schema = type_assert_type($this->typed_schema, TSchema::class); + return Shapes::keyExists($schema, 'hackEnum'); + } + + /** + * + * Main method for building the class for the schema and appending it to the + * file. + */ + abstract public function build(): this; + + protected function getHackBuilder(): HackBuilder { + return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); + } + + public function getClassName(): string { + return $this->generateClassName($this->ctx->getClassName(), $this->suffix); + } + + protected function codegenClass(): CodegenClass { + if ($this->class) { + return $this->class; + } + + return $this->ctx + ->getHackCodegenFactory() + ->codegenClass($this->getClassName()) + ->setIsFinal(true); + } + + protected function codegenProperty(string $name): CodegenProperty { + return $this->ctx + ->getHackCodegenFactory() + ->codegenProperty($name) + ->setIsStatic(true) + ->setPrivate(); + } + + protected function codegenCheckMethod(): CodegenMethod { + return $this->ctx + ->getHackCodegenFactory() + ->codegenMethod('check') + ->setPublic() + ->setIsStatic(true); + } + + protected function getEnumCodegenProperty(): ?CodegenProperty { + $property = null; + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + $enum = $schema['enum'] ?? null; + if ($enum is nonnull) { + $should_create_hack_enum = $schema['generateHackEnum'] ?? false; + if ($should_create_hack_enum) { + $factory = $this->ctx->getHackCodegenFactory() + $members = Vec\map($enum, $val ==> $factory->codegenEnumMember($val)); + $enum_obj = $factory->codegenEnum('enum', 'string')->addMembers($members); + $this->codegenProperty('enum')->setType('enum')->setPublic()->setValue($enum_obj->getCode(), HackBuilderValues::literal()); + } else { + $hb = $this->getHackBuilder() + ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); + + $property = $this->codegenProperty('enum') + ->setType('vec') + ->setValue($hb->getCode(), HackBuilderValues::literal()); + } + } + return $property; + } + + protected function addEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + if (($schema['enum'] ?? null) is nonnull) { + $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); + } + } + + protected function addHackEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_type($this->typed_schema, TSchema::class); + if (!Shapes::keyExists($schema, 'hackEnum')) { + return; + } + + try { + $rc = new \ReflectionClass($schema['hackEnum']); + } catch (\ReflectionException $e) { + throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); + } + + invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); + + $schema_type = $schema['type'] ?? null; + $hack_enum_values = keyset[]; + foreach ($rc->getConstants() as $hack_enum_value) { + if ($schema_type === TSchemaType::INTEGER_T) { + $hack_enum_value = $hack_enum_value ?as int; + } else { + $hack_enum_value = $hack_enum_value ?as string; + } + invariant( + $hack_enum_value is nonnull, + "'%s' must contain only values of type %s", + $rc->getName(), + $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', + ); + $hack_enum_values[] = $hack_enum_value; + } + + if (Shapes::keyExists($schema, 'enum')) { + // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of + // `hackEnum` values. Any value not also in `hackEnum` can't be valid. + foreach ($schema['enum'] as $enum_value) { + invariant( + $enum_value is string, + "Enum value '%s' is not a valid value for '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + invariant( + C\contains_key($hack_enum_values, $enum_value), + "Enum value '%s' is unexpectedly not present in '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + } + } + + $hb->addMultilineCall( + '$typed = Constraints\HackEnumConstraint::check', + vec[ + '$typed', + Str\format('\%s::class', $rc->getName()), + '$pointer', + ], + ); + } + + public function addBuilderClass(CodegenClass $class): void { + if ($this->class) { + return; + } + + $this->ctx->getFile()->addClass($class); + } + + public function setSuffix(string $suffix): void { + $this->suffix = $suffix; + } } diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index d69183a..d1bc9f1 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -7,157 +7,158 @@ use type Facebook\HackCodegen\{CodegenMethod, HackBuilderValues}; type TStringSchema = shape( - 'type' => TSchemaType, - ?'maxLength' => int, - ?'minLength' => int, - ?'enum' => vec, - ?'hackEnum' => string, - ?'pattern' => string, - ?'format' => string, - ?'sanitize' => shape( - 'multiline' => bool, - ), - ?'coerce' => bool, - ... + 'type' => TSchemaType, + ?'maxLength' => int, + ?'minLength' => int, + ?'enum' => vec, + ?'hackEnum' => string, + ?'generateHackEnum' => bool, + ?'pattern' => string, + ?'format' => string, + ?'sanitize' => shape( + 'multiline' => bool, + ), + ?'coerce' => bool, + ... ); class StringBuilder extends BaseBuilder { - protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; - - <<__Override>> - public function build(): this { - $class = $this->codegenClass() - ->addMethod($this->getCheckMethod()); - - $properties = vec[]; - $max_length = $this->typed_schema['maxLength'] ?? null; - if ($max_length is nonnull) { - $properties[] = $this->codegenProperty('maxLength') - ->setType('int') - ->setValue($max_length, HackBuilderValues::export()); - } - - $min_length = $this->typed_schema['minLength'] ?? null; - if ($min_length is nonnull) { - $properties[] = $this->codegenProperty('minLength') - ->setType('int') - ->setValue($min_length, HackBuilderValues::export()); - } - - $pattern = $this->typed_schema['pattern'] ?? null; - if ($pattern is nonnull) { - $properties[] = $this->codegenProperty('pattern') - ->setType('string') - ->setValue($pattern, HackBuilderValues::export()); - } - - $format = $this->typed_schema['format'] ?? null; - if ($format is nonnull) { - $properties[] = $this->codegenProperty('format') - ->setType('string') - ->setValue($format, HackBuilderValues::export()); - } - - $enum = $this->getEnumCodegenProperty(); - if ($enum is nonnull) { - $properties[] = $enum; - } - - $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); - $properties[] = $this->codegenProperty('coerce') - ->setType('bool') - ->setValue($coerce, HackBuilderValues::export()); - - $class->addProperties($properties); - $this->addBuilderClass($class); - - return $this; - } - - protected function getCheckMethod(): CodegenMethod { - $hb = $this->getHackBuilder() - ->addAssignment( - '$typed', - 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', - HackBuilderValues::literal(), - ) - ->ensureEmptyLine(); - - $sanitize = $this->typed_schema['sanitize'] ?? null; - $sanitize_string = $this->ctx->getSanitizeStringConfig(); - if ($sanitize is nonnull && $sanitize_string === null) { - throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); - } else if ($sanitize is nonnull && $sanitize_string is nonnull) { - if ($sanitize['multiline']) { - $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); - } else { - $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); - } - - $hb - ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) - ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) - ->ensureEmptyLine(); - } - - $this->addEnumConstraintCheck($hb); - - $max_length = $this->typed_schema['maxLength'] ?? null; - $min_length = $this->typed_schema['minLength'] ?? null; - $pattern = $this->typed_schema['pattern'] ?? null; - $format = $this->typed_schema['format'] ?? null; - - if ($pattern is nonnull) { - $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); - } - - if ($format is nonnull) { - $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); - } - - if ($max_length is nonnull || $min_length is nonnull) { - $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); - } - - if ($max_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMaxLengthConstraint::check', - vec['$length', 'self::$maxLength', '$pointer'], - ); - } - - if ($min_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMinLengthConstraint::check', - vec['$length', 'self::$minLength', '$pointer'], - ); - } - - $this->addHackEnumConstraintCheck($hb); - - $hb->addReturn('$typed', HackBuilderValues::literal()); - - return $this->codegenCheckMethod() - ->addParameters(vec['mixed $input', 'string $pointer']) - ->setBody($hb->getCode()) - ->setReturnType($this->getType()); - } - - <<__Override>> - public function getType(): string { - if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { - return Str\format('\%s', $this->typed_schema['hackEnum']); - } - - return 'string'; - } - - <<__Override>> - public function getTypeInfo(): Typing\Type { - if ($this->getType() === 'string') { - return Typing\TypeSystem::string(); - } - // TODO: Type resolution for hackEnum - return Typing\TypeSystem::nonnull(); - } + protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; + + <<__Override>> + public function build(): this { + $class = $this->codegenClass() + ->addMethod($this->getCheckMethod()); + + $properties = vec[]; + $max_length = $this->typed_schema['maxLength'] ?? null; + if ($max_length is nonnull) { + $properties[] = $this->codegenProperty('maxLength') + ->setType('int') + ->setValue($max_length, HackBuilderValues::export()); + } + + $min_length = $this->typed_schema['minLength'] ?? null; + if ($min_length is nonnull) { + $properties[] = $this->codegenProperty('minLength') + ->setType('int') + ->setValue($min_length, HackBuilderValues::export()); + } + + $pattern = $this->typed_schema['pattern'] ?? null; + if ($pattern is nonnull) { + $properties[] = $this->codegenProperty('pattern') + ->setType('string') + ->setValue($pattern, HackBuilderValues::export()); + } + + $format = $this->typed_schema['format'] ?? null; + if ($format is nonnull) { + $properties[] = $this->codegenProperty('format') + ->setType('string') + ->setValue($format, HackBuilderValues::export()); + } + + $enum = $this->getEnumCodegenProperty(); + if ($enum is nonnull) { + $properties[] = $enum; + } + + $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); + $properties[] = $this->codegenProperty('coerce') + ->setType('bool') + ->setValue($coerce, HackBuilderValues::export()); + + $class->addProperties($properties); + $this->addBuilderClass($class); + + return $this; + } + + protected function getCheckMethod(): CodegenMethod { + $hb = $this->getHackBuilder() + ->addAssignment( + '$typed', + 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', + HackBuilderValues::literal(), + ) + ->ensureEmptyLine(); + + $sanitize = $this->typed_schema['sanitize'] ?? null; + $sanitize_string = $this->ctx->getSanitizeStringConfig(); + if ($sanitize is nonnull && $sanitize_string === null) { + throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); + } else if ($sanitize is nonnull && $sanitize_string is nonnull) { + if ($sanitize['multiline']) { + $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); + } else { + $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); + } + + $hb + ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) + ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) + ->ensureEmptyLine(); + } + + $this->addEnumConstraintCheck($hb); + + $max_length = $this->typed_schema['maxLength'] ?? null; + $min_length = $this->typed_schema['minLength'] ?? null; + $pattern = $this->typed_schema['pattern'] ?? null; + $format = $this->typed_schema['format'] ?? null; + + if ($pattern is nonnull) { + $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); + } + + if ($format is nonnull) { + $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); + } + + if ($max_length is nonnull || $min_length is nonnull) { + $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); + } + + if ($max_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMaxLengthConstraint::check', + vec['$length', 'self::$maxLength', '$pointer'], + ); + } + + if ($min_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMinLengthConstraint::check', + vec['$length', 'self::$minLength', '$pointer'], + ); + } + + $this->addHackEnumConstraintCheck($hb); + + $hb->addReturn('$typed', HackBuilderValues::literal()); + + return $this->codegenCheckMethod() + ->addParameters(vec['mixed $input', 'string $pointer']) + ->setBody($hb->getCode()) + ->setReturnType($this->getType()); + } + + <<__Override>> + public function getType(): string { + if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { + return Str\format('\%s', $this->typed_schema['hackEnum']); + } + + return 'string'; + } + + <<__Override>> + public function getTypeInfo(): Typing\Type { + if ($this->getType() === 'string') { + return Typing\TypeSystem::string(); + } + // TODO: Type resolution for hackEnum + return Typing\TypeSystem::nonnull(); + } } diff --git a/tests/GeneratedHackEnumSchemaValidatorTest.php b/tests/GeneratedHackEnumSchemaValidatorTest.php new file mode 100644 index 0000000..7f4b72d --- /dev/null +++ b/tests/GeneratedHackEnumSchemaValidatorTest.php @@ -0,0 +1,16 @@ +> + public static async function beforeFirstTestAsync(): Awaitable { + $ret = self::getBuilder('generated-hack-enum-schema.json', 'EnumSchemaValidator'); + $ret['codegen']->build(); + require_once($ret['path']); + } +} diff --git a/tests/StringSchemaValidatorTest.php b/tests/StringSchemaValidatorTest.php index 6de2246..d5830b4 100644 --- a/tests/StringSchemaValidatorTest.php +++ b/tests/StringSchemaValidatorTest.php @@ -140,6 +140,5 @@ public function testHackEnum(): void { ]; $this->expectCases($cases, $input ==> new StringSchemaValidator($input)); - } - + } } From cd54e99a408e004c8172183b5831212ad7f8a5be Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Mon, 18 Dec 2023 13:19:00 -0800 Subject: [PATCH 02/12] wip --- src/Codegen/Constraints/BaseBuilder.php | 352 +++++++++--------- src/Codegen/Constraints/StringBuilder.php | 36 +- .../examples/generated-hack-enum-schema.json | 12 + 3 files changed, 211 insertions(+), 189 deletions(-) create mode 100644 tests/examples/generated-hack-enum-schema.json diff --git a/src/Codegen/Constraints/BaseBuilder.php b/src/Codegen/Constraints/BaseBuilder.php index ea66c49..7e29a22 100644 --- a/src/Codegen/Constraints/BaseBuilder.php +++ b/src/Codegen/Constraints/BaseBuilder.php @@ -3,191 +3,175 @@ namespace Slack\Hack\JsonSchema\Codegen; use namespace HH\Lib\{C, Str}; -use type Facebook\HackCodegen\{ - CodegenClass, - // CodegenEnum, - CodegenEnumMember, - CodegenMethod, - CodegenProperty, - HackBuilder, - HackBuilderValues, -}; +use type Facebook\HackCodegen\{CodegenClass, CodegenMethod, CodegenProperty, HackBuilder, HackBuilderValues}; <<__ConsistentConstruct>> abstract class BaseBuilder implements IBuilder { - use Factory; - - protected T $typed_schema; - protected static string $schema_name = ''; - - public function __construct( - protected Context $ctx, - protected string $suffix, - protected TSchema $schema, - protected ?CodegenClass $class = null, - ) { - $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); - } - - /** - * - * Return a string which will get rendered as a literal value describing the - * output type of the schema. For example, the `StringBuilder` would return: - * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. - */ - abstract public function getType(): string; - - public function isArrayKeyType(): bool { - $type = $this->getType(); - if ($type === 'string' || $type === 'int') { - return true; - } - - $schema = type_assert_type($this->typed_schema, TSchema::class); - return Shapes::keyExists($schema, 'hackEnum'); - } - - /** - * - * Main method for building the class for the schema and appending it to the - * file. - */ - abstract public function build(): this; - - protected function getHackBuilder(): HackBuilder { - return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); - } - - public function getClassName(): string { - return $this->generateClassName($this->ctx->getClassName(), $this->suffix); - } - - protected function codegenClass(): CodegenClass { - if ($this->class) { - return $this->class; - } - - return $this->ctx - ->getHackCodegenFactory() - ->codegenClass($this->getClassName()) - ->setIsFinal(true); - } - - protected function codegenProperty(string $name): CodegenProperty { - return $this->ctx - ->getHackCodegenFactory() - ->codegenProperty($name) - ->setIsStatic(true) - ->setPrivate(); - } - - protected function codegenCheckMethod(): CodegenMethod { - return $this->ctx - ->getHackCodegenFactory() - ->codegenMethod('check') - ->setPublic() - ->setIsStatic(true); - } - - protected function getEnumCodegenProperty(): ?CodegenProperty { - $property = null; - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - $enum = $schema['enum'] ?? null; - if ($enum is nonnull) { - $should_create_hack_enum = $schema['generateHackEnum'] ?? false; - if ($should_create_hack_enum) { - $factory = $this->ctx->getHackCodegenFactory() - $members = Vec\map($enum, $val ==> $factory->codegenEnumMember($val)); - $enum_obj = $factory->codegenEnum('enum', 'string')->addMembers($members); - $this->codegenProperty('enum')->setType('enum')->setPublic()->setValue($enum_obj->getCode(), HackBuilderValues::literal()); - } else { - $hb = $this->getHackBuilder() - ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); - - $property = $this->codegenProperty('enum') - ->setType('vec') - ->setValue($hb->getCode(), HackBuilderValues::literal()); - } - } - return $property; - } - - protected function addEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - if (($schema['enum'] ?? null) is nonnull) { - $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); - } - } - - protected function addHackEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_type($this->typed_schema, TSchema::class); - if (!Shapes::keyExists($schema, 'hackEnum')) { - return; - } - - try { - $rc = new \ReflectionClass($schema['hackEnum']); - } catch (\ReflectionException $e) { - throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); - } - - invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); - - $schema_type = $schema['type'] ?? null; - $hack_enum_values = keyset[]; - foreach ($rc->getConstants() as $hack_enum_value) { - if ($schema_type === TSchemaType::INTEGER_T) { - $hack_enum_value = $hack_enum_value ?as int; - } else { - $hack_enum_value = $hack_enum_value ?as string; - } - invariant( - $hack_enum_value is nonnull, - "'%s' must contain only values of type %s", - $rc->getName(), - $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', - ); - $hack_enum_values[] = $hack_enum_value; - } - - if (Shapes::keyExists($schema, 'enum')) { - // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of - // `hackEnum` values. Any value not also in `hackEnum` can't be valid. - foreach ($schema['enum'] as $enum_value) { - invariant( - $enum_value is string, - "Enum value '%s' is not a valid value for '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - invariant( - C\contains_key($hack_enum_values, $enum_value), - "Enum value '%s' is unexpectedly not present in '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - } - } - - $hb->addMultilineCall( - '$typed = Constraints\HackEnumConstraint::check', - vec[ - '$typed', - Str\format('\%s::class', $rc->getName()), - '$pointer', - ], - ); - } - - public function addBuilderClass(CodegenClass $class): void { - if ($this->class) { - return; - } - - $this->ctx->getFile()->addClass($class); - } - - public function setSuffix(string $suffix): void { - $this->suffix = $suffix; - } + use Factory; + + protected T $typed_schema; + protected static string $schema_name = ''; + + public function __construct( + protected Context $ctx, + protected string $suffix, + protected TSchema $schema, + protected ?CodegenClass $class = null, + ) { + $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); + } + + /** + * + * Return a string which will get rendered as a literal value describing the + * output type of the schema. For example, the `StringBuilder` would return: + * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. + */ + abstract public function getType(): string; + + public function isArrayKeyType(): bool { + $type = $this->getType(); + if ($type === 'string' || $type === 'int') { + return true; + } + + $schema = type_assert_type($this->typed_schema, TSchema::class); + return Shapes::keyExists($schema, 'hackEnum'); + } + + /** + * + * Main method for building the class for the schema and appending it to the + * file. + */ + abstract public function build(): this; + + protected function getHackBuilder(): HackBuilder { + return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); + } + + public function getClassName(): string { + return $this->generateClassName($this->ctx->getClassName(), $this->suffix); + } + + protected function codegenClass(): CodegenClass { + if ($this->class) { + return $this->class; + } + + return $this->ctx + ->getHackCodegenFactory() + ->codegenClass($this->getClassName()) + ->setIsFinal(true); + } + + protected function codegenProperty(string $name): CodegenProperty { + return $this->ctx + ->getHackCodegenFactory() + ->codegenProperty($name) + ->setIsStatic(true) + ->setPrivate(); + } + + protected function codegenCheckMethod(): CodegenMethod { + return $this->ctx + ->getHackCodegenFactory() + ->codegenMethod('check') + ->setPublic() + ->setIsStatic(true); + } + + protected function getEnumCodegenProperty(): ?CodegenProperty { + $property = null; + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + $enum = $schema['enum'] ?? null; + if ($enum is nonnull) { + $hb = $this->getHackBuilder() + ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); + + $property = $this->codegenProperty('enum') + ->setType('vec') + ->setValue($hb->getCode(), HackBuilderValues::literal()); + } + return $property; + } + + protected function addEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + if (($schema['enum'] ?? null) is nonnull) { + $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); + } + } + + protected function addHackEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_type($this->typed_schema, TSchema::class); + if (!Shapes::keyExists($schema, 'hackEnum')) { + return; + } + + try { + $rc = new \ReflectionClass($schema['hackEnum']); + } catch (\ReflectionException $e) { + throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); + } + + invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); + + $schema_type = $schema['type'] ?? null; + $hack_enum_values = keyset[]; + foreach ($rc->getConstants() as $hack_enum_value) { + if ($schema_type === TSchemaType::INTEGER_T) { + $hack_enum_value = $hack_enum_value ?as int; + } else { + $hack_enum_value = $hack_enum_value ?as string; + } + invariant( + $hack_enum_value is nonnull, + "'%s' must contain only values of type %s", + $rc->getName(), + $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', + ); + $hack_enum_values[] = $hack_enum_value; + } + + if (Shapes::keyExists($schema, 'enum')) { + // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of + // `hackEnum` values. Any value not also in `hackEnum` can't be valid. + foreach ($schema['enum'] as $enum_value) { + invariant( + $enum_value is string, + "Enum value '%s' is not a valid value for '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + invariant( + C\contains_key($hack_enum_values, $enum_value), + "Enum value '%s' is unexpectedly not present in '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + } + } + + $hb->addMultilineCall( + '$typed = Constraints\HackEnumConstraint::check', + vec[ + '$typed', + Str\format('\%s::class', $rc->getName()), + '$pointer', + ], + ); + } + + public function addBuilderClass(CodegenClass $class): void { + if ($this->class) { + return; + } + + $this->ctx->getFile()->addClass($class); + } + + public function setSuffix(string $suffix): void { + $this->suffix = $suffix; + } } diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index d1bc9f1..107f7f4 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -61,7 +61,24 @@ public function build(): this { $enum = $this->getEnumCodegenProperty(); if ($enum is nonnull) { - $properties[] = $enum; + $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; + if ($generateHackEnum) { + $enum = $this->typed_schema['enum'] ?? vec[]; + $factory = $this->ctx->getHackCodegenFactory(); + $members = Vec\map( + $enum, + $member ==> $factory->codegenEnumMember(Str\uppercase($member)) + ->setValue($member, HackBuilderValues::export()), + ); + $enumName = $this->typed_schema['hackEnum'] ?? null; + invariant($enumName is string, 'hackEnum is required when generating hack enum.'); + $enum_obj = $factory->codegenEnum($enumName, 'string') + ->addMembers($members) + ->setIsAs('string'); + $this->ctx->getFile()->addEnum($enum_obj); + } else { + $properties[] = $enum; + } } $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); @@ -100,8 +117,11 @@ protected function getCheckMethod(): CodegenMethod { ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) ->ensureEmptyLine(); } - - $this->addEnumConstraintCheck($hb); + if ($this->typed_schema['generateHackEnum'] ?? false) { + $this->addHackEnumConstraintCheck($hb); + } else { + $this->addEnumConstraintCheck($hb); + } $max_length = $this->typed_schema['maxLength'] ?? null; $min_length = $this->typed_schema['minLength'] ?? null; @@ -109,11 +129,17 @@ protected function getCheckMethod(): CodegenMethod { $format = $this->typed_schema['format'] ?? null; if ($pattern is nonnull) { - $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); + $hb->addMultilineCall( + 'Constraints\StringPatternConstraint::check', + vec['$typed', 'self::$pattern', '$pointer'], + ); } if ($format is nonnull) { - $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); + $hb->addMultilineCall( + 'Constraints\StringFormatConstraint::check', + vec['$typed', 'self::$format', '$pointer'], + ); } if ($max_length is nonnull || $min_length is nonnull) { diff --git a/tests/examples/generated-hack-enum-schema.json b/tests/examples/generated-hack-enum-schema.json new file mode 100644 index 0000000..75221d2 --- /dev/null +++ b/tests/examples/generated-hack-enum-schema.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "enum_string": { + "type": "string", + "enum": ["one", "two", "three"], + "generateHackEnum": true, + "hackEnum": "myCoolTestEnum" + } + } + } + \ No newline at end of file From 0010d7c0046204f544c66dd0f7cb56dc320c1cfd Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Mon, 18 Dec 2023 13:56:29 -0800 Subject: [PATCH 03/12] no need to double add hack constraint --- src/Codegen/Constraints/StringBuilder.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index 107f7f4..fc11d2e 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -117,9 +117,7 @@ protected function getCheckMethod(): CodegenMethod { ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) ->ensureEmptyLine(); } - if ($this->typed_schema['generateHackEnum'] ?? false) { - $this->addHackEnumConstraintCheck($hb); - } else { + if (!($this->typed_schema['generateHackEnum'] ?? false)) { $this->addEnumConstraintCheck($hb); } From b19a901b73f32292cf200d437700c486c01cbb07 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Mon, 18 Dec 2023 16:15:31 -0800 Subject: [PATCH 04/12] invariant. skip reflection. --- src/Codegen/Constraints/BaseBuilder.php | 340 +++++++++++----------- src/Codegen/Constraints/StringBuilder.php | 6 +- 2 files changed, 177 insertions(+), 169 deletions(-) diff --git a/src/Codegen/Constraints/BaseBuilder.php b/src/Codegen/Constraints/BaseBuilder.php index 7e29a22..da9c73f 100644 --- a/src/Codegen/Constraints/BaseBuilder.php +++ b/src/Codegen/Constraints/BaseBuilder.php @@ -7,171 +7,177 @@ <<__ConsistentConstruct>> abstract class BaseBuilder implements IBuilder { - use Factory; - - protected T $typed_schema; - protected static string $schema_name = ''; - - public function __construct( - protected Context $ctx, - protected string $suffix, - protected TSchema $schema, - protected ?CodegenClass $class = null, - ) { - $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); - } - - /** - * - * Return a string which will get rendered as a literal value describing the - * output type of the schema. For example, the `StringBuilder` would return: - * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. - */ - abstract public function getType(): string; - - public function isArrayKeyType(): bool { - $type = $this->getType(); - if ($type === 'string' || $type === 'int') { - return true; - } - - $schema = type_assert_type($this->typed_schema, TSchema::class); - return Shapes::keyExists($schema, 'hackEnum'); - } - - /** - * - * Main method for building the class for the schema and appending it to the - * file. - */ - abstract public function build(): this; - - protected function getHackBuilder(): HackBuilder { - return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); - } - - public function getClassName(): string { - return $this->generateClassName($this->ctx->getClassName(), $this->suffix); - } - - protected function codegenClass(): CodegenClass { - if ($this->class) { - return $this->class; - } - - return $this->ctx - ->getHackCodegenFactory() - ->codegenClass($this->getClassName()) - ->setIsFinal(true); - } - - protected function codegenProperty(string $name): CodegenProperty { - return $this->ctx - ->getHackCodegenFactory() - ->codegenProperty($name) - ->setIsStatic(true) - ->setPrivate(); - } - - protected function codegenCheckMethod(): CodegenMethod { - return $this->ctx - ->getHackCodegenFactory() - ->codegenMethod('check') - ->setPublic() - ->setIsStatic(true); - } - - protected function getEnumCodegenProperty(): ?CodegenProperty { - $property = null; - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - $enum = $schema['enum'] ?? null; - if ($enum is nonnull) { - $hb = $this->getHackBuilder() - ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); - - $property = $this->codegenProperty('enum') - ->setType('vec') - ->setValue($hb->getCode(), HackBuilderValues::literal()); - } - return $property; - } - - protected function addEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - if (($schema['enum'] ?? null) is nonnull) { - $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); - } - } - - protected function addHackEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_type($this->typed_schema, TSchema::class); - if (!Shapes::keyExists($schema, 'hackEnum')) { - return; - } - - try { - $rc = new \ReflectionClass($schema['hackEnum']); - } catch (\ReflectionException $e) { - throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); - } - - invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); - - $schema_type = $schema['type'] ?? null; - $hack_enum_values = keyset[]; - foreach ($rc->getConstants() as $hack_enum_value) { - if ($schema_type === TSchemaType::INTEGER_T) { - $hack_enum_value = $hack_enum_value ?as int; - } else { - $hack_enum_value = $hack_enum_value ?as string; - } - invariant( - $hack_enum_value is nonnull, - "'%s' must contain only values of type %s", - $rc->getName(), - $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', - ); - $hack_enum_values[] = $hack_enum_value; - } - - if (Shapes::keyExists($schema, 'enum')) { - // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of - // `hackEnum` values. Any value not also in `hackEnum` can't be valid. - foreach ($schema['enum'] as $enum_value) { - invariant( - $enum_value is string, - "Enum value '%s' is not a valid value for '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - invariant( - C\contains_key($hack_enum_values, $enum_value), - "Enum value '%s' is unexpectedly not present in '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - } - } - - $hb->addMultilineCall( - '$typed = Constraints\HackEnumConstraint::check', - vec[ - '$typed', - Str\format('\%s::class', $rc->getName()), - '$pointer', - ], - ); - } - - public function addBuilderClass(CodegenClass $class): void { - if ($this->class) { - return; - } - - $this->ctx->getFile()->addClass($class); - } - - public function setSuffix(string $suffix): void { - $this->suffix = $suffix; - } + use Factory; + + protected T $typed_schema; + protected static string $schema_name = ''; + + public function __construct( + protected Context $ctx, + protected string $suffix, + protected TSchema $schema, + protected ?CodegenClass $class = null, + ) { + $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); + } + + /** + * + * Return a string which will get rendered as a literal value describing the + * output type of the schema. For example, the `StringBuilder` would return: + * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. + */ + abstract public function getType(): string; + + public function isArrayKeyType(): bool { + $type = $this->getType(); + if ($type === 'string' || $type === 'int') { + return true; + } + + $schema = type_assert_type($this->typed_schema, TSchema::class); + return Shapes::keyExists($schema, 'hackEnum'); + } + + /** + * + * Main method for building the class for the schema and appending it to the + * file. + */ + abstract public function build(): this; + + protected function getHackBuilder(): HackBuilder { + return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); + } + + public function getClassName(): string { + return $this->generateClassName($this->ctx->getClassName(), $this->suffix); + } + + protected function codegenClass(): CodegenClass { + if ($this->class) { + return $this->class; + } + + return $this->ctx + ->getHackCodegenFactory() + ->codegenClass($this->getClassName()) + ->setIsFinal(true); + } + + protected function codegenProperty(string $name): CodegenProperty { + return $this->ctx + ->getHackCodegenFactory() + ->codegenProperty($name) + ->setIsStatic(true) + ->setPrivate(); + } + + protected function codegenCheckMethod(): CodegenMethod { + return $this->ctx + ->getHackCodegenFactory() + ->codegenMethod('check') + ->setPublic() + ->setIsStatic(true); + } + + protected function getEnumCodegenProperty(): ?CodegenProperty { + $property = null; + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + $enum = $schema['enum'] ?? null; + if ($enum is nonnull) { + $hb = $this->getHackBuilder() + ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); + + $property = $this->codegenProperty('enum') + ->setType('vec') + ->setValue($hb->getCode(), HackBuilderValues::literal()); + } + return $property; + } + + protected function addEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + if (($schema['enum'] ?? null) is nonnull) { + $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); + } + } + + protected function addHackEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_type($this->typed_schema, TSchema::class); + if (!Shapes::keyExists($schema, 'hackEnum')) { + return; + } + $generateHackEnum = $schema['generateHackEnum'] ?? false; + if (!$generateHackEnum) { + + try { + $rc = new \ReflectionClass($schema['hackEnum']); + } catch (\ReflectionException $e) { + throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); + } + + invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); + + $schema_type = $schema['type'] ?? null; + $hack_enum_values = keyset[]; + foreach ($rc->getConstants() as $hack_enum_value) { + if ($schema_type === TSchemaType::INTEGER_T) { + $hack_enum_value = $hack_enum_value ?as int; + } else { + $hack_enum_value = $hack_enum_value ?as string; + } + invariant( + $hack_enum_value is nonnull, + "'%s' must contain only values of type %s", + $rc->getName(), + $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', + ); + $hack_enum_values[] = $hack_enum_value; + } + + if (Shapes::keyExists($schema, 'enum')) { + // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of + // `hackEnum` values. Any value not also in `hackEnum` can't be valid. + foreach ($schema['enum'] as $enum_value) { + invariant( + $enum_value is string, + "Enum value '%s' is not a valid value for '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + invariant( + C\contains_key($hack_enum_values, $enum_value), + "Enum value '%s' is unexpectedly not present in '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + } + } + $enum_name = $rc->getName(); + } else { + $enum_name = $schema['hackEnum']; + } + + $hb->addMultilineCall( + '$typed = Constraints\HackEnumConstraint::check', + vec[ + '$typed', + Str\format('\%s::class', $enum_name), + '$pointer', + ], + ); + } + + public function addBuilderClass(CodegenClass $class): void { + if ($this->class) { + return; + } + + $this->ctx->getFile()->addClass($class); + } + + public function setSuffix(string $suffix): void { + $this->suffix = $suffix; + } } diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index fc11d2e..831ff97 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -60,12 +60,12 @@ public function build(): this { } $enum = $this->getEnumCodegenProperty(); + $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; if ($enum is nonnull) { - $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; if ($generateHackEnum) { $enum = $this->typed_schema['enum'] ?? vec[]; $factory = $this->ctx->getHackCodegenFactory(); - $members = Vec\map( + $members = \HH\Lib\Vec\map( $enum, $member ==> $factory->codegenEnumMember(Str\uppercase($member)) ->setValue($member, HackBuilderValues::export()), @@ -79,6 +79,8 @@ public function build(): this { } else { $properties[] = $enum; } + } else { + invariant(!$generateHackEnum, 'enum is required when generating hack enum'); } $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); From fcd58cfce8168d402b72f0e97b2afa6109e3ab4b Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Mon, 18 Dec 2023 16:46:44 -0800 Subject: [PATCH 05/12] remove leading slashes. --- src/Codegen/Constraints/BaseBuilder.php | 6 +- src/Codegen/Constraints/StringBuilder.php | 3 + .../GeneratedHackEnumSchemaValidatorTest.php | 2 +- .../GeneratedHackEnumSchemaValidator.php | 85 +++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 tests/examples/codegen/GeneratedHackEnumSchemaValidator.php diff --git a/src/Codegen/Constraints/BaseBuilder.php b/src/Codegen/Constraints/BaseBuilder.php index da9c73f..c04fc03 100644 --- a/src/Codegen/Constraints/BaseBuilder.php +++ b/src/Codegen/Constraints/BaseBuilder.php @@ -154,16 +154,16 @@ protected function addHackEnumConstraintCheck(HackBuilder $hb): void { ); } } - $enum_name = $rc->getName(); + $enum_name = Str\format('\%s::class', $rc->getName()); } else { - $enum_name = $schema['hackEnum']; + $enum_name = $schema['hackEnum'].'::class'; } $hb->addMultilineCall( '$typed = Constraints\HackEnumConstraint::check', vec[ '$typed', - Str\format('\%s::class', $enum_name), + $enum_name, '$pointer', ], ); diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index 831ff97..4ee5556 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -173,6 +173,9 @@ protected function getCheckMethod(): CodegenMethod { <<__Override>> public function getType(): string { if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { + if ($this->typed_schema['generateHackEnum'] ?? false) { + return $this->typed_schema['hackEnum']; + } return Str\format('\%s', $this->typed_schema['hackEnum']); } diff --git a/tests/GeneratedHackEnumSchemaValidatorTest.php b/tests/GeneratedHackEnumSchemaValidatorTest.php index 7f4b72d..4eff196 100644 --- a/tests/GeneratedHackEnumSchemaValidatorTest.php +++ b/tests/GeneratedHackEnumSchemaValidatorTest.php @@ -9,7 +9,7 @@ final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase { <<__Override>> public static async function beforeFirstTestAsync(): Awaitable { - $ret = self::getBuilder('generated-hack-enum-schema.json', 'EnumSchemaValidator'); + $ret = self::getBuilder('generated-hack-enum-schema.json', 'GeneratedHackEnumSchemaValidator'); $ret['codegen']->build(); require_once($ret['path']); } diff --git a/tests/examples/codegen/GeneratedHackEnumSchemaValidator.php b/tests/examples/codegen/GeneratedHackEnumSchemaValidator.php new file mode 100644 index 0000000..e33d603 --- /dev/null +++ b/tests/examples/codegen/GeneratedHackEnumSchemaValidator.php @@ -0,0 +1,85 @@ +> + */ +namespace Slack\Hack\JsonSchema\Tests\Generated; +use namespace Slack\Hack\JsonSchema; +use namespace Slack\Hack\JsonSchema\Constraints; + +type TGeneratedHackEnumSchemaValidator = shape( + ?'enum_string' => myCoolTestEnum, + ... +); + + +enum myCoolTestEnum : string as string { + ONE = 'one'; + TWO = 'two'; + THREE = 'three'; +} + +final class GeneratedHackEnumSchemaValidatorPropertiesEnumString { + + private static bool $coerce = false; + + public static function check(mixed $input, string $pointer): myCoolTestEnum { + $typed = Constraints\StringConstraint::check($input, $pointer, self::$coerce); + + $typed = Constraints\HackEnumConstraint::check( + $typed, + myCoolTestEnum::class, + $pointer, + ); + return $typed; + } +} + +final class GeneratedHackEnumSchemaValidator + extends JsonSchema\BaseValidator { + + private static bool $coerce = false; + + public static function check( + mixed $input, + string $pointer, + ): TGeneratedHackEnumSchemaValidator { + $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, 'enum_string')) { + try { + $output['enum_string'] = GeneratedHackEnumSchemaValidatorPropertiesEnumString::check( + $typed['enum_string'], + JsonSchema\get_pointer($pointer, 'enum_string'), + ); + } 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(): TGeneratedHackEnumSchemaValidator { + return self::check($this->input, $this->pointer); + } +} From a8602e6ccd859d56c61dc9cd9cdf9ec2d82ccdf0 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Wed, 3 Jan 2024 13:59:19 -0800 Subject: [PATCH 06/12] tests. --- .../GeneratedHackEnumSchemaValidatorTest.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/GeneratedHackEnumSchemaValidatorTest.php b/tests/GeneratedHackEnumSchemaValidatorTest.php index 4eff196..a5126f6 100644 --- a/tests/GeneratedHackEnumSchemaValidatorTest.php +++ b/tests/GeneratedHackEnumSchemaValidatorTest.php @@ -3,7 +3,7 @@ namespace Slack\Hack\JsonSchema\Tests; -use type Slack\Hack\JsonSchema\Tests\Generated\EnumSchemaValidator; +use type Slack\Hack\JsonSchema\Tests\Generated\GeneratedHackEnumSchemaValidator; final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase { @@ -13,4 +13,23 @@ final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase { $ret['codegen']->build(); require_once($ret['path']); } + public function testStringEnum(): void { + $cases = vec[ + shape( + 'input' => darray['enum_string' => 'one'], + 'output' => darray['enum_string' => 'one'], + 'valid' => true, + ), + shape( + 'input' => darray['enum_string' => 'four'], + 'valid' => false, + ), + shape( + 'input' => darray['enum_string' => 1], + 'valid' => false, + ), + ]; + + $this->expectCases($cases, $input ==> new GeneratedHackEnumSchemaValidator($input)); + } } From 63ea1415319df945016851a1deeef796f77c138a Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Wed, 3 Jan 2024 14:00:45 -0800 Subject: [PATCH 07/12] revert unneeded change. --- tests/StringSchemaValidatorTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/StringSchemaValidatorTest.php b/tests/StringSchemaValidatorTest.php index d5830b4..6de2246 100644 --- a/tests/StringSchemaValidatorTest.php +++ b/tests/StringSchemaValidatorTest.php @@ -140,5 +140,6 @@ public function testHackEnum(): void { ]; $this->expectCases($cases, $input ==> new StringSchemaValidator($input)); - } + } + } From 6ec04310acd6a70b6d3de35ae8c15694af233638 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Thu, 4 Jan 2024 13:43:56 -0800 Subject: [PATCH 08/12] hackfmt --- src/Codegen/Constraints/BaseBuilder.php | 346 ++++++++--------- src/Codegen/Constraints/StringBuilder.php | 354 +++++++++--------- .../GeneratedHackEnumSchemaValidatorTest.php | 48 +-- 3 files changed, 371 insertions(+), 377 deletions(-) diff --git a/src/Codegen/Constraints/BaseBuilder.php b/src/Codegen/Constraints/BaseBuilder.php index c04fc03..e0068ca 100644 --- a/src/Codegen/Constraints/BaseBuilder.php +++ b/src/Codegen/Constraints/BaseBuilder.php @@ -7,177 +7,177 @@ <<__ConsistentConstruct>> abstract class BaseBuilder implements IBuilder { - use Factory; - - protected T $typed_schema; - protected static string $schema_name = ''; - - public function __construct( - protected Context $ctx, - protected string $suffix, - protected TSchema $schema, - protected ?CodegenClass $class = null, - ) { - $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); - } - - /** - * - * Return a string which will get rendered as a literal value describing the - * output type of the schema. For example, the `StringBuilder` would return: - * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. - */ - abstract public function getType(): string; - - public function isArrayKeyType(): bool { - $type = $this->getType(); - if ($type === 'string' || $type === 'int') { - return true; - } - - $schema = type_assert_type($this->typed_schema, TSchema::class); - return Shapes::keyExists($schema, 'hackEnum'); - } - - /** - * - * Main method for building the class for the schema and appending it to the - * file. - */ - abstract public function build(): this; - - protected function getHackBuilder(): HackBuilder { - return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); - } - - public function getClassName(): string { - return $this->generateClassName($this->ctx->getClassName(), $this->suffix); - } - - protected function codegenClass(): CodegenClass { - if ($this->class) { - return $this->class; - } - - return $this->ctx - ->getHackCodegenFactory() - ->codegenClass($this->getClassName()) - ->setIsFinal(true); - } - - protected function codegenProperty(string $name): CodegenProperty { - return $this->ctx - ->getHackCodegenFactory() - ->codegenProperty($name) - ->setIsStatic(true) - ->setPrivate(); - } - - protected function codegenCheckMethod(): CodegenMethod { - return $this->ctx - ->getHackCodegenFactory() - ->codegenMethod('check') - ->setPublic() - ->setIsStatic(true); - } - - protected function getEnumCodegenProperty(): ?CodegenProperty { - $property = null; - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - $enum = $schema['enum'] ?? null; - if ($enum is nonnull) { - $hb = $this->getHackBuilder() - ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); - - $property = $this->codegenProperty('enum') - ->setType('vec') - ->setValue($hb->getCode(), HackBuilderValues::literal()); - } - return $property; - } - - protected function addEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); - if (($schema['enum'] ?? null) is nonnull) { - $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); - } - } - - protected function addHackEnumConstraintCheck(HackBuilder $hb): void { - $schema = type_assert_type($this->typed_schema, TSchema::class); - if (!Shapes::keyExists($schema, 'hackEnum')) { - return; - } - $generateHackEnum = $schema['generateHackEnum'] ?? false; - if (!$generateHackEnum) { - - try { - $rc = new \ReflectionClass($schema['hackEnum']); - } catch (\ReflectionException $e) { - throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); - } - - invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); - - $schema_type = $schema['type'] ?? null; - $hack_enum_values = keyset[]; - foreach ($rc->getConstants() as $hack_enum_value) { - if ($schema_type === TSchemaType::INTEGER_T) { - $hack_enum_value = $hack_enum_value ?as int; - } else { - $hack_enum_value = $hack_enum_value ?as string; - } - invariant( - $hack_enum_value is nonnull, - "'%s' must contain only values of type %s", - $rc->getName(), - $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', - ); - $hack_enum_values[] = $hack_enum_value; - } - - if (Shapes::keyExists($schema, 'enum')) { - // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of - // `hackEnum` values. Any value not also in `hackEnum` can't be valid. - foreach ($schema['enum'] as $enum_value) { - invariant( - $enum_value is string, - "Enum value '%s' is not a valid value for '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - invariant( - C\contains_key($hack_enum_values, $enum_value), - "Enum value '%s' is unexpectedly not present in '%s'", - \print_r($enum_value, true), - $rc->getName(), - ); - } - } - $enum_name = Str\format('\%s::class', $rc->getName()); - } else { - $enum_name = $schema['hackEnum'].'::class'; - } - - $hb->addMultilineCall( - '$typed = Constraints\HackEnumConstraint::check', - vec[ - '$typed', - $enum_name, - '$pointer', - ], - ); - } - - public function addBuilderClass(CodegenClass $class): void { - if ($this->class) { - return; - } - - $this->ctx->getFile()->addClass($class); - } - - public function setSuffix(string $suffix): void { - $this->suffix = $suffix; - } + use Factory; + + protected T $typed_schema; + protected static string $schema_name = ''; + + public function __construct( + protected Context $ctx, + protected string $suffix, + protected TSchema $schema, + protected ?CodegenClass $class = null, + ) { + $this->typed_schema = type_assert_shape($this->schema, static::$schema_name); + } + + /** + * + * Return a string which will get rendered as a literal value describing the + * output type of the schema. For example, the `StringBuilder` would return: + * `return 'string';`, the `NumberBuilder` would return: `return 'num';`. + */ + abstract public function getType(): string; + + public function isArrayKeyType(): bool { + $type = $this->getType(); + if ($type === 'string' || $type === 'int') { + return true; + } + + $schema = type_assert_type($this->typed_schema, TSchema::class); + return Shapes::keyExists($schema, 'hackEnum'); + } + + /** + * + * Main method for building the class for the schema and appending it to the + * file. + */ + abstract public function build(): this; + + protected function getHackBuilder(): HackBuilder { + return new HackBuilder($this->ctx->getHackCodegenFactory()->getConfig()); + } + + public function getClassName(): string { + return $this->generateClassName($this->ctx->getClassName(), $this->suffix); + } + + protected function codegenClass(): CodegenClass { + if ($this->class) { + return $this->class; + } + + return $this->ctx + ->getHackCodegenFactory() + ->codegenClass($this->getClassName()) + ->setIsFinal(true); + } + + protected function codegenProperty(string $name): CodegenProperty { + return $this->ctx + ->getHackCodegenFactory() + ->codegenProperty($name) + ->setIsStatic(true) + ->setPrivate(); + } + + protected function codegenCheckMethod(): CodegenMethod { + return $this->ctx + ->getHackCodegenFactory() + ->codegenMethod('check') + ->setPublic() + ->setIsStatic(true); + } + + protected function getEnumCodegenProperty(): ?CodegenProperty { + $property = null; + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + $enum = $schema['enum'] ?? null; + if ($enum is nonnull) { + $hb = $this->getHackBuilder() + ->addValue($enum, HackBuilderValues::vec(HackBuilderValues::export())); + + $property = $this->codegenProperty('enum') + ->setType('vec') + ->setValue($hb->getCode(), HackBuilderValues::literal()); + } + return $property; + } + + protected function addEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_shape($this->typed_schema, 'Slack\Hack\JsonSchema\Codegen\TSchema'); + if (($schema['enum'] ?? null) is nonnull) { + $hb->addMultilineCall('Constraints\EnumConstraint::check', vec['$typed', 'self::$enum', '$pointer']); + } + } + + protected function addHackEnumConstraintCheck(HackBuilder $hb): void { + $schema = type_assert_type($this->typed_schema, TSchema::class); + if (!Shapes::keyExists($schema, 'hackEnum')) { + return; + } + $generateHackEnum = $schema['generateHackEnum'] ?? false; + if (!$generateHackEnum) { + + try { + $rc = new \ReflectionClass($schema['hackEnum']); + } catch (\ReflectionException $e) { + throw new \Exception(Str\format("Hack enum '%s' does not exist", $schema['hackEnum'])); + } + + invariant($rc->isEnum(), "'%s' is not an enum", $schema['hackEnum']); + + $schema_type = $schema['type'] ?? null; + $hack_enum_values = keyset[]; + foreach ($rc->getConstants() as $hack_enum_value) { + if ($schema_type === TSchemaType::INTEGER_T) { + $hack_enum_value = $hack_enum_value ?as int; + } else { + $hack_enum_value = $hack_enum_value ?as string; + } + invariant( + $hack_enum_value is nonnull, + "'%s' must contain only values of type %s", + $rc->getName(), + $schema_type === TSchemaType::INTEGER_T ? 'int' : 'string', + ); + $hack_enum_values[] = $hack_enum_value; + } + + if (Shapes::keyExists($schema, 'enum')) { + // If both `enum` and `hackEnum` are specified, assert that `enum` is a subset of + // `hackEnum` values. Any value not also in `hackEnum` can't be valid. + foreach ($schema['enum'] as $enum_value) { + invariant( + $enum_value is string, + "Enum value '%s' is not a valid value for '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + invariant( + C\contains_key($hack_enum_values, $enum_value), + "Enum value '%s' is unexpectedly not present in '%s'", + \print_r($enum_value, true), + $rc->getName(), + ); + } + } + $enum_name = Str\format('\%s::class', $rc->getName()); + } else { + $enum_name = $schema['hackEnum'].'::class'; + } + + $hb->addMultilineCall( + '$typed = Constraints\HackEnumConstraint::check', + vec[ + '$typed', + $enum_name, + '$pointer', + ], + ); + } + + public function addBuilderClass(CodegenClass $class): void { + if ($this->class) { + return; + } + + $this->ctx->getFile()->addClass($class); + } + + public function setSuffix(string $suffix): void { + $this->suffix = $suffix; + } } diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index 4ee5556..8b8631d 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -7,187 +7,181 @@ use type Facebook\HackCodegen\{CodegenMethod, HackBuilderValues}; type TStringSchema = shape( - 'type' => TSchemaType, - ?'maxLength' => int, - ?'minLength' => int, - ?'enum' => vec, - ?'hackEnum' => string, - ?'generateHackEnum' => bool, - ?'pattern' => string, - ?'format' => string, - ?'sanitize' => shape( - 'multiline' => bool, - ), - ?'coerce' => bool, - ... + 'type' => TSchemaType, + ?'maxLength' => int, + ?'minLength' => int, + ?'enum' => vec, + ?'hackEnum' => string, + ?'generateHackEnum' => bool, + ?'pattern' => string, + ?'format' => string, + ?'sanitize' => shape( + 'multiline' => bool, + ), + ?'coerce' => bool, + ... ); class StringBuilder extends BaseBuilder { - protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; - - <<__Override>> - public function build(): this { - $class = $this->codegenClass() - ->addMethod($this->getCheckMethod()); - - $properties = vec[]; - $max_length = $this->typed_schema['maxLength'] ?? null; - if ($max_length is nonnull) { - $properties[] = $this->codegenProperty('maxLength') - ->setType('int') - ->setValue($max_length, HackBuilderValues::export()); - } - - $min_length = $this->typed_schema['minLength'] ?? null; - if ($min_length is nonnull) { - $properties[] = $this->codegenProperty('minLength') - ->setType('int') - ->setValue($min_length, HackBuilderValues::export()); - } - - $pattern = $this->typed_schema['pattern'] ?? null; - if ($pattern is nonnull) { - $properties[] = $this->codegenProperty('pattern') - ->setType('string') - ->setValue($pattern, HackBuilderValues::export()); - } - - $format = $this->typed_schema['format'] ?? null; - if ($format is nonnull) { - $properties[] = $this->codegenProperty('format') - ->setType('string') - ->setValue($format, HackBuilderValues::export()); - } - - $enum = $this->getEnumCodegenProperty(); - $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; - if ($enum is nonnull) { - if ($generateHackEnum) { - $enum = $this->typed_schema['enum'] ?? vec[]; - $factory = $this->ctx->getHackCodegenFactory(); - $members = \HH\Lib\Vec\map( - $enum, - $member ==> $factory->codegenEnumMember(Str\uppercase($member)) - ->setValue($member, HackBuilderValues::export()), - ); - $enumName = $this->typed_schema['hackEnum'] ?? null; - invariant($enumName is string, 'hackEnum is required when generating hack enum.'); - $enum_obj = $factory->codegenEnum($enumName, 'string') - ->addMembers($members) - ->setIsAs('string'); - $this->ctx->getFile()->addEnum($enum_obj); - } else { - $properties[] = $enum; - } - } else { - invariant(!$generateHackEnum, 'enum is required when generating hack enum'); - } - - $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); - $properties[] = $this->codegenProperty('coerce') - ->setType('bool') - ->setValue($coerce, HackBuilderValues::export()); - - $class->addProperties($properties); - $this->addBuilderClass($class); - - return $this; - } - - protected function getCheckMethod(): CodegenMethod { - $hb = $this->getHackBuilder() - ->addAssignment( - '$typed', - 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', - HackBuilderValues::literal(), - ) - ->ensureEmptyLine(); - - $sanitize = $this->typed_schema['sanitize'] ?? null; - $sanitize_string = $this->ctx->getSanitizeStringConfig(); - if ($sanitize is nonnull && $sanitize_string === null) { - throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); - } else if ($sanitize is nonnull && $sanitize_string is nonnull) { - if ($sanitize['multiline']) { - $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); - } else { - $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); - } - - $hb - ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) - ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) - ->ensureEmptyLine(); - } - if (!($this->typed_schema['generateHackEnum'] ?? false)) { - $this->addEnumConstraintCheck($hb); - } - - $max_length = $this->typed_schema['maxLength'] ?? null; - $min_length = $this->typed_schema['minLength'] ?? null; - $pattern = $this->typed_schema['pattern'] ?? null; - $format = $this->typed_schema['format'] ?? null; - - if ($pattern is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringPatternConstraint::check', - vec['$typed', 'self::$pattern', '$pointer'], - ); - } - - if ($format is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringFormatConstraint::check', - vec['$typed', 'self::$format', '$pointer'], - ); - } - - if ($max_length is nonnull || $min_length is nonnull) { - $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); - } - - if ($max_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMaxLengthConstraint::check', - vec['$length', 'self::$maxLength', '$pointer'], - ); - } - - if ($min_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMinLengthConstraint::check', - vec['$length', 'self::$minLength', '$pointer'], - ); - } - - $this->addHackEnumConstraintCheck($hb); - - $hb->addReturn('$typed', HackBuilderValues::literal()); - - return $this->codegenCheckMethod() - ->addParameters(vec['mixed $input', 'string $pointer']) - ->setBody($hb->getCode()) - ->setReturnType($this->getType()); - } - - <<__Override>> - public function getType(): string { - if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { - if ($this->typed_schema['generateHackEnum'] ?? false) { - return $this->typed_schema['hackEnum']; - } - return Str\format('\%s', $this->typed_schema['hackEnum']); - } - - return 'string'; - } - - <<__Override>> - public function getTypeInfo(): Typing\Type { - if ($this->getType() === 'string') { - return Typing\TypeSystem::string(); - } - // TODO: Type resolution for hackEnum - return Typing\TypeSystem::nonnull(); - } + protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; + + <<__Override>> + public function build(): this { + $class = $this->codegenClass() + ->addMethod($this->getCheckMethod()); + + $properties = vec[]; + $max_length = $this->typed_schema['maxLength'] ?? null; + if ($max_length is nonnull) { + $properties[] = $this->codegenProperty('maxLength') + ->setType('int') + ->setValue($max_length, HackBuilderValues::export()); + } + + $min_length = $this->typed_schema['minLength'] ?? null; + if ($min_length is nonnull) { + $properties[] = $this->codegenProperty('minLength') + ->setType('int') + ->setValue($min_length, HackBuilderValues::export()); + } + + $pattern = $this->typed_schema['pattern'] ?? null; + if ($pattern is nonnull) { + $properties[] = $this->codegenProperty('pattern') + ->setType('string') + ->setValue($pattern, HackBuilderValues::export()); + } + + $format = $this->typed_schema['format'] ?? null; + if ($format is nonnull) { + $properties[] = $this->codegenProperty('format') + ->setType('string') + ->setValue($format, HackBuilderValues::export()); + } + + $enum = $this->getEnumCodegenProperty(); + $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; + if ($enum is nonnull) { + if ($generateHackEnum) { + $enum = $this->typed_schema['enum'] ?? vec[]; + $factory = $this->ctx->getHackCodegenFactory(); + $members = \HH\Lib\Vec\map( + $enum, + $member ==> $factory->codegenEnumMember(Str\uppercase($member)) + ->setValue($member, HackBuilderValues::export()), + ); + $enumName = $this->typed_schema['hackEnum'] ?? null; + invariant($enumName is string, 'hackEnum is required when generating hack enum.'); + $enum_obj = $factory->codegenEnum($enumName, 'string') + ->addMembers($members) + ->setIsAs('string'); + $this->ctx->getFile()->addEnum($enum_obj); + } else { + $properties[] = $enum; + } + } else { + invariant(!$generateHackEnum, 'enum is required when generating hack enum'); + } + + $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); + $properties[] = $this->codegenProperty('coerce') + ->setType('bool') + ->setValue($coerce, HackBuilderValues::export()); + + $class->addProperties($properties); + $this->addBuilderClass($class); + + return $this; + } + + protected function getCheckMethod(): CodegenMethod { + $hb = $this->getHackBuilder() + ->addAssignment( + '$typed', + 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', + HackBuilderValues::literal(), + ) + ->ensureEmptyLine(); + + $sanitize = $this->typed_schema['sanitize'] ?? null; + $sanitize_string = $this->ctx->getSanitizeStringConfig(); + if ($sanitize is nonnull && $sanitize_string === null) { + throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); + } else if ($sanitize is nonnull && $sanitize_string is nonnull) { + if ($sanitize['multiline']) { + $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); + } else { + $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); + } + + $hb + ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) + ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) + ->ensureEmptyLine(); + } + if (!($this->typed_schema['generateHackEnum'] ?? false)) { + $this->addEnumConstraintCheck($hb); + } + + $max_length = $this->typed_schema['maxLength'] ?? null; + $min_length = $this->typed_schema['minLength'] ?? null; + $pattern = $this->typed_schema['pattern'] ?? null; + $format = $this->typed_schema['format'] ?? null; + + if ($pattern is nonnull) { + $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); + } + + if ($format is nonnull) { + $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); + } + + if ($max_length is nonnull || $min_length is nonnull) { + $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); + } + + if ($max_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMaxLengthConstraint::check', + vec['$length', 'self::$maxLength', '$pointer'], + ); + } + + if ($min_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMinLengthConstraint::check', + vec['$length', 'self::$minLength', '$pointer'], + ); + } + + $this->addHackEnumConstraintCheck($hb); + + $hb->addReturn('$typed', HackBuilderValues::literal()); + + return $this->codegenCheckMethod() + ->addParameters(vec['mixed $input', 'string $pointer']) + ->setBody($hb->getCode()) + ->setReturnType($this->getType()); + } + + <<__Override>> + public function getType(): string { + if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { + if ($this->typed_schema['generateHackEnum'] ?? false) { + return $this->typed_schema['hackEnum']; + } + return Str\format('\%s', $this->typed_schema['hackEnum']); + } + + return 'string'; + } + + <<__Override>> + public function getTypeInfo(): Typing\Type { + if ($this->getType() === 'string') { + return Typing\TypeSystem::string(); + } + // TODO: Type resolution for hackEnum + return Typing\TypeSystem::nonnull(); + } } diff --git a/tests/GeneratedHackEnumSchemaValidatorTest.php b/tests/GeneratedHackEnumSchemaValidatorTest.php index a5126f6..aeeea44 100644 --- a/tests/GeneratedHackEnumSchemaValidatorTest.php +++ b/tests/GeneratedHackEnumSchemaValidatorTest.php @@ -7,29 +7,29 @@ final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase { - <<__Override>> - public static async function beforeFirstTestAsync(): Awaitable { - $ret = self::getBuilder('generated-hack-enum-schema.json', 'GeneratedHackEnumSchemaValidator'); - $ret['codegen']->build(); - require_once($ret['path']); - } - public function testStringEnum(): void { - $cases = vec[ - shape( - 'input' => darray['enum_string' => 'one'], - 'output' => darray['enum_string' => 'one'], - 'valid' => true, - ), - shape( - 'input' => darray['enum_string' => 'four'], - 'valid' => false, - ), - shape( - 'input' => darray['enum_string' => 1], - 'valid' => false, - ), - ]; + <<__Override>> + public static async function beforeFirstTestAsync(): Awaitable { + $ret = self::getBuilder('generated-hack-enum-schema.json', 'GeneratedHackEnumSchemaValidator'); + $ret['codegen']->build(); + require_once($ret['path']); + } + public function testStringEnum(): void { + $cases = vec[ + shape( + 'input' => darray['enum_string' => 'one'], + 'output' => darray['enum_string' => 'one'], + 'valid' => true, + ), + shape( + 'input' => darray['enum_string' => 'four'], + 'valid' => false, + ), + shape( + 'input' => darray['enum_string' => 1], + 'valid' => false, + ), + ]; - $this->expectCases($cases, $input ==> new GeneratedHackEnumSchemaValidator($input)); - } + $this->expectCases($cases, $input ==> new GeneratedHackEnumSchemaValidator($input)); + } } From 4691ad1910e7f3a70185eaf03350620e0b22b07a Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Thu, 4 Jan 2024 13:50:02 -0800 Subject: [PATCH 09/12] invariant for slash --- src/Codegen/Constraints/StringBuilder.php | 349 +++++++++++----------- 1 file changed, 175 insertions(+), 174 deletions(-) diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index 8b8631d..1a1dc6d 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -7,181 +7,182 @@ use type Facebook\HackCodegen\{CodegenMethod, HackBuilderValues}; type TStringSchema = shape( - 'type' => TSchemaType, - ?'maxLength' => int, - ?'minLength' => int, - ?'enum' => vec, - ?'hackEnum' => string, - ?'generateHackEnum' => bool, - ?'pattern' => string, - ?'format' => string, - ?'sanitize' => shape( - 'multiline' => bool, - ), - ?'coerce' => bool, - ... + 'type' => TSchemaType, + ?'maxLength' => int, + ?'minLength' => int, + ?'enum' => vec, + ?'hackEnum' => string, + ?'generateHackEnum' => bool, + ?'pattern' => string, + ?'format' => string, + ?'sanitize' => shape( + 'multiline' => bool, + ), + ?'coerce' => bool, + ... ); class StringBuilder extends BaseBuilder { - protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; - - <<__Override>> - public function build(): this { - $class = $this->codegenClass() - ->addMethod($this->getCheckMethod()); - - $properties = vec[]; - $max_length = $this->typed_schema['maxLength'] ?? null; - if ($max_length is nonnull) { - $properties[] = $this->codegenProperty('maxLength') - ->setType('int') - ->setValue($max_length, HackBuilderValues::export()); - } - - $min_length = $this->typed_schema['minLength'] ?? null; - if ($min_length is nonnull) { - $properties[] = $this->codegenProperty('minLength') - ->setType('int') - ->setValue($min_length, HackBuilderValues::export()); - } - - $pattern = $this->typed_schema['pattern'] ?? null; - if ($pattern is nonnull) { - $properties[] = $this->codegenProperty('pattern') - ->setType('string') - ->setValue($pattern, HackBuilderValues::export()); - } - - $format = $this->typed_schema['format'] ?? null; - if ($format is nonnull) { - $properties[] = $this->codegenProperty('format') - ->setType('string') - ->setValue($format, HackBuilderValues::export()); - } - - $enum = $this->getEnumCodegenProperty(); - $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; - if ($enum is nonnull) { - if ($generateHackEnum) { - $enum = $this->typed_schema['enum'] ?? vec[]; - $factory = $this->ctx->getHackCodegenFactory(); - $members = \HH\Lib\Vec\map( - $enum, - $member ==> $factory->codegenEnumMember(Str\uppercase($member)) - ->setValue($member, HackBuilderValues::export()), - ); - $enumName = $this->typed_schema['hackEnum'] ?? null; - invariant($enumName is string, 'hackEnum is required when generating hack enum.'); - $enum_obj = $factory->codegenEnum($enumName, 'string') - ->addMembers($members) - ->setIsAs('string'); - $this->ctx->getFile()->addEnum($enum_obj); - } else { - $properties[] = $enum; - } - } else { - invariant(!$generateHackEnum, 'enum is required when generating hack enum'); - } - - $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); - $properties[] = $this->codegenProperty('coerce') - ->setType('bool') - ->setValue($coerce, HackBuilderValues::export()); - - $class->addProperties($properties); - $this->addBuilderClass($class); - - return $this; - } - - protected function getCheckMethod(): CodegenMethod { - $hb = $this->getHackBuilder() - ->addAssignment( - '$typed', - 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', - HackBuilderValues::literal(), - ) - ->ensureEmptyLine(); - - $sanitize = $this->typed_schema['sanitize'] ?? null; - $sanitize_string = $this->ctx->getSanitizeStringConfig(); - if ($sanitize is nonnull && $sanitize_string === null) { - throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); - } else if ($sanitize is nonnull && $sanitize_string is nonnull) { - if ($sanitize['multiline']) { - $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); - } else { - $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); - } - - $hb - ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) - ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) - ->ensureEmptyLine(); - } - if (!($this->typed_schema['generateHackEnum'] ?? false)) { - $this->addEnumConstraintCheck($hb); - } - - $max_length = $this->typed_schema['maxLength'] ?? null; - $min_length = $this->typed_schema['minLength'] ?? null; - $pattern = $this->typed_schema['pattern'] ?? null; - $format = $this->typed_schema['format'] ?? null; - - if ($pattern is nonnull) { - $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); - } - - if ($format is nonnull) { - $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); - } - - if ($max_length is nonnull || $min_length is nonnull) { - $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); - } - - if ($max_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMaxLengthConstraint::check', - vec['$length', 'self::$maxLength', '$pointer'], - ); - } - - if ($min_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMinLengthConstraint::check', - vec['$length', 'self::$minLength', '$pointer'], - ); - } - - $this->addHackEnumConstraintCheck($hb); - - $hb->addReturn('$typed', HackBuilderValues::literal()); - - return $this->codegenCheckMethod() - ->addParameters(vec['mixed $input', 'string $pointer']) - ->setBody($hb->getCode()) - ->setReturnType($this->getType()); - } - - <<__Override>> - public function getType(): string { - if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { - if ($this->typed_schema['generateHackEnum'] ?? false) { - return $this->typed_schema['hackEnum']; - } - return Str\format('\%s', $this->typed_schema['hackEnum']); - } - - return 'string'; - } - - <<__Override>> - public function getTypeInfo(): Typing\Type { - if ($this->getType() === 'string') { - return Typing\TypeSystem::string(); - } - // TODO: Type resolution for hackEnum - return Typing\TypeSystem::nonnull(); - } + protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; + + <<__Override>> + public function build(): this { + $class = $this->codegenClass() + ->addMethod($this->getCheckMethod()); + + $properties = vec[]; + $max_length = $this->typed_schema['maxLength'] ?? null; + if ($max_length is nonnull) { + $properties[] = $this->codegenProperty('maxLength') + ->setType('int') + ->setValue($max_length, HackBuilderValues::export()); + } + + $min_length = $this->typed_schema['minLength'] ?? null; + if ($min_length is nonnull) { + $properties[] = $this->codegenProperty('minLength') + ->setType('int') + ->setValue($min_length, HackBuilderValues::export()); + } + + $pattern = $this->typed_schema['pattern'] ?? null; + if ($pattern is nonnull) { + $properties[] = $this->codegenProperty('pattern') + ->setType('string') + ->setValue($pattern, HackBuilderValues::export()); + } + + $format = $this->typed_schema['format'] ?? null; + if ($format is nonnull) { + $properties[] = $this->codegenProperty('format') + ->setType('string') + ->setValue($format, HackBuilderValues::export()); + } + + $enum = $this->getEnumCodegenProperty(); + $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; + if ($enum is nonnull) { + if ($generateHackEnum) { + $enum = $this->typed_schema['enum'] ?? vec[]; + $factory = $this->ctx->getHackCodegenFactory(); + $members = \HH\Lib\Vec\map( + $enum, + $member ==> $factory->codegenEnumMember(Str\uppercase($member)) + ->setValue($member, HackBuilderValues::export()), + ); + $enumName = $this->typed_schema['hackEnum'] ?? null; + invariant($enumName is string, 'hackEnum is required when generating hack enum.'); + invariant(!Str\contains($enumName, '\\'), 'hackEnum must not contain a slash.'); + $enum_obj = $factory->codegenEnum($enumName, 'string') + ->addMembers($members) + ->setIsAs('string'); + $this->ctx->getFile()->addEnum($enum_obj); + } else { + $properties[] = $enum; + } + } else { + invariant(!$generateHackEnum, 'enum is required when generating hack enum'); + } + + $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); + $properties[] = $this->codegenProperty('coerce') + ->setType('bool') + ->setValue($coerce, HackBuilderValues::export()); + + $class->addProperties($properties); + $this->addBuilderClass($class); + + return $this; + } + + protected function getCheckMethod(): CodegenMethod { + $hb = $this->getHackBuilder() + ->addAssignment( + '$typed', + 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', + HackBuilderValues::literal(), + ) + ->ensureEmptyLine(); + + $sanitize = $this->typed_schema['sanitize'] ?? null; + $sanitize_string = $this->ctx->getSanitizeStringConfig(); + if ($sanitize is nonnull && $sanitize_string === null) { + throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); + } else if ($sanitize is nonnull && $sanitize_string is nonnull) { + if ($sanitize['multiline']) { + $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); + } else { + $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); + } + + $hb + ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) + ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) + ->ensureEmptyLine(); + } + if (!($this->typed_schema['generateHackEnum'] ?? false)) { + $this->addEnumConstraintCheck($hb); + } + + $max_length = $this->typed_schema['maxLength'] ?? null; + $min_length = $this->typed_schema['minLength'] ?? null; + $pattern = $this->typed_schema['pattern'] ?? null; + $format = $this->typed_schema['format'] ?? null; + + if ($pattern is nonnull) { + $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); + } + + if ($format is nonnull) { + $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); + } + + if ($max_length is nonnull || $min_length is nonnull) { + $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); + } + + if ($max_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMaxLengthConstraint::check', + vec['$length', 'self::$maxLength', '$pointer'], + ); + } + + if ($min_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMinLengthConstraint::check', + vec['$length', 'self::$minLength', '$pointer'], + ); + } + + $this->addHackEnumConstraintCheck($hb); + + $hb->addReturn('$typed', HackBuilderValues::literal()); + + return $this->codegenCheckMethod() + ->addParameters(vec['mixed $input', 'string $pointer']) + ->setBody($hb->getCode()) + ->setReturnType($this->getType()); + } + + <<__Override>> + public function getType(): string { + if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { + if ($this->typed_schema['generateHackEnum'] ?? false) { + return $this->typed_schema['hackEnum']; + } + return Str\format('\%s', $this->typed_schema['hackEnum']); + } + + return 'string'; + } + + <<__Override>> + public function getTypeInfo(): Typing\Type { + if ($this->getType() === 'string') { + return Typing\TypeSystem::string(); + } + // TODO: Type resolution for hackEnum + return Typing\TypeSystem::nonnull(); + } } From a06beb84c12f18624933c9d85fd414ec072822b6 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Thu, 4 Jan 2024 14:19:50 -0800 Subject: [PATCH 10/12] tabs -> 2 spaces. --- src/Codegen/Constraints/StringBuilder.php | 350 +++++++++--------- .../examples/generated-hack-enum-schema.json | 18 +- 2 files changed, 184 insertions(+), 184 deletions(-) diff --git a/src/Codegen/Constraints/StringBuilder.php b/src/Codegen/Constraints/StringBuilder.php index 1a1dc6d..558eb57 100644 --- a/src/Codegen/Constraints/StringBuilder.php +++ b/src/Codegen/Constraints/StringBuilder.php @@ -7,182 +7,182 @@ use type Facebook\HackCodegen\{CodegenMethod, HackBuilderValues}; type TStringSchema = shape( - 'type' => TSchemaType, - ?'maxLength' => int, - ?'minLength' => int, - ?'enum' => vec, - ?'hackEnum' => string, - ?'generateHackEnum' => bool, - ?'pattern' => string, - ?'format' => string, - ?'sanitize' => shape( - 'multiline' => bool, - ), - ?'coerce' => bool, - ... + 'type' => TSchemaType, + ?'maxLength' => int, + ?'minLength' => int, + ?'enum' => vec, + ?'hackEnum' => string, + ?'generateHackEnum' => bool, + ?'pattern' => string, + ?'format' => string, + ?'sanitize' => shape( + 'multiline' => bool, + ), + ?'coerce' => bool, + ... ); class StringBuilder extends BaseBuilder { - protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; - - <<__Override>> - public function build(): this { - $class = $this->codegenClass() - ->addMethod($this->getCheckMethod()); - - $properties = vec[]; - $max_length = $this->typed_schema['maxLength'] ?? null; - if ($max_length is nonnull) { - $properties[] = $this->codegenProperty('maxLength') - ->setType('int') - ->setValue($max_length, HackBuilderValues::export()); - } - - $min_length = $this->typed_schema['minLength'] ?? null; - if ($min_length is nonnull) { - $properties[] = $this->codegenProperty('minLength') - ->setType('int') - ->setValue($min_length, HackBuilderValues::export()); - } - - $pattern = $this->typed_schema['pattern'] ?? null; - if ($pattern is nonnull) { - $properties[] = $this->codegenProperty('pattern') - ->setType('string') - ->setValue($pattern, HackBuilderValues::export()); - } - - $format = $this->typed_schema['format'] ?? null; - if ($format is nonnull) { - $properties[] = $this->codegenProperty('format') - ->setType('string') - ->setValue($format, HackBuilderValues::export()); - } - - $enum = $this->getEnumCodegenProperty(); - $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; - if ($enum is nonnull) { - if ($generateHackEnum) { - $enum = $this->typed_schema['enum'] ?? vec[]; - $factory = $this->ctx->getHackCodegenFactory(); - $members = \HH\Lib\Vec\map( - $enum, - $member ==> $factory->codegenEnumMember(Str\uppercase($member)) - ->setValue($member, HackBuilderValues::export()), - ); - $enumName = $this->typed_schema['hackEnum'] ?? null; - invariant($enumName is string, 'hackEnum is required when generating hack enum.'); - invariant(!Str\contains($enumName, '\\'), 'hackEnum must not contain a slash.'); - $enum_obj = $factory->codegenEnum($enumName, 'string') - ->addMembers($members) - ->setIsAs('string'); - $this->ctx->getFile()->addEnum($enum_obj); - } else { - $properties[] = $enum; - } - } else { - invariant(!$generateHackEnum, 'enum is required when generating hack enum'); - } - - $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); - $properties[] = $this->codegenProperty('coerce') - ->setType('bool') - ->setValue($coerce, HackBuilderValues::export()); - - $class->addProperties($properties); - $this->addBuilderClass($class); - - return $this; - } - - protected function getCheckMethod(): CodegenMethod { - $hb = $this->getHackBuilder() - ->addAssignment( - '$typed', - 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', - HackBuilderValues::literal(), - ) - ->ensureEmptyLine(); - - $sanitize = $this->typed_schema['sanitize'] ?? null; - $sanitize_string = $this->ctx->getSanitizeStringConfig(); - if ($sanitize is nonnull && $sanitize_string === null) { - throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); - } else if ($sanitize is nonnull && $sanitize_string is nonnull) { - if ($sanitize['multiline']) { - $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); - } else { - $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); - } - - $hb - ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) - ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) - ->ensureEmptyLine(); - } - if (!($this->typed_schema['generateHackEnum'] ?? false)) { - $this->addEnumConstraintCheck($hb); - } - - $max_length = $this->typed_schema['maxLength'] ?? null; - $min_length = $this->typed_schema['minLength'] ?? null; - $pattern = $this->typed_schema['pattern'] ?? null; - $format = $this->typed_schema['format'] ?? null; - - if ($pattern is nonnull) { - $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); - } - - if ($format is nonnull) { - $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); - } - - if ($max_length is nonnull || $min_length is nonnull) { - $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); - } - - if ($max_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMaxLengthConstraint::check', - vec['$length', 'self::$maxLength', '$pointer'], - ); - } - - if ($min_length is nonnull) { - $hb->addMultilineCall( - 'Constraints\StringMinLengthConstraint::check', - vec['$length', 'self::$minLength', '$pointer'], - ); - } - - $this->addHackEnumConstraintCheck($hb); - - $hb->addReturn('$typed', HackBuilderValues::literal()); - - return $this->codegenCheckMethod() - ->addParameters(vec['mixed $input', 'string $pointer']) - ->setBody($hb->getCode()) - ->setReturnType($this->getType()); - } - - <<__Override>> - public function getType(): string { - if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { - if ($this->typed_schema['generateHackEnum'] ?? false) { - return $this->typed_schema['hackEnum']; - } - return Str\format('\%s', $this->typed_schema['hackEnum']); - } - - return 'string'; - } - - <<__Override>> - public function getTypeInfo(): Typing\Type { - if ($this->getType() === 'string') { - return Typing\TypeSystem::string(); - } - // TODO: Type resolution for hackEnum - return Typing\TypeSystem::nonnull(); - } + protected static string $schema_name = 'Slack\Hack\JsonSchema\Codegen\TStringSchema'; + + <<__Override>> + public function build(): this { + $class = $this->codegenClass() + ->addMethod($this->getCheckMethod()); + + $properties = vec[]; + $max_length = $this->typed_schema['maxLength'] ?? null; + if ($max_length is nonnull) { + $properties[] = $this->codegenProperty('maxLength') + ->setType('int') + ->setValue($max_length, HackBuilderValues::export()); + } + + $min_length = $this->typed_schema['minLength'] ?? null; + if ($min_length is nonnull) { + $properties[] = $this->codegenProperty('minLength') + ->setType('int') + ->setValue($min_length, HackBuilderValues::export()); + } + + $pattern = $this->typed_schema['pattern'] ?? null; + if ($pattern is nonnull) { + $properties[] = $this->codegenProperty('pattern') + ->setType('string') + ->setValue($pattern, HackBuilderValues::export()); + } + + $format = $this->typed_schema['format'] ?? null; + if ($format is nonnull) { + $properties[] = $this->codegenProperty('format') + ->setType('string') + ->setValue($format, HackBuilderValues::export()); + } + + $enum = $this->getEnumCodegenProperty(); + $generateHackEnum = $this->typed_schema['generateHackEnum'] ?? false; + if ($enum is nonnull) { + if ($generateHackEnum) { + $enum = $this->typed_schema['enum'] ?? vec[]; + $factory = $this->ctx->getHackCodegenFactory(); + $members = \HH\Lib\Vec\map( + $enum, + $member ==> $factory->codegenEnumMember(Str\uppercase($member)) + ->setValue($member, HackBuilderValues::export()), + ); + $enumName = $this->typed_schema['hackEnum'] ?? null; + invariant($enumName is string, 'hackEnum is required when generating hack enum.'); + invariant(!Str\contains($enumName, '\\'), 'hackEnum must not contain a slash.'); + $enum_obj = $factory->codegenEnum($enumName, 'string') + ->addMembers($members) + ->setIsAs('string'); + $this->ctx->getFile()->addEnum($enum_obj); + } else { + $properties[] = $enum; + } + } else { + invariant(!$generateHackEnum, 'enum is required when generating hack enum'); + } + + $coerce = $this->typed_schema['coerce'] ?? $this->ctx->getCoerceDefault(); + $properties[] = $this->codegenProperty('coerce') + ->setType('bool') + ->setValue($coerce, HackBuilderValues::export()); + + $class->addProperties($properties); + $this->addBuilderClass($class); + + return $this; + } + + protected function getCheckMethod(): CodegenMethod { + $hb = $this->getHackBuilder() + ->addAssignment( + '$typed', + 'Constraints\StringConstraint::check($input, $pointer, self::$coerce)', + HackBuilderValues::literal(), + ) + ->ensureEmptyLine(); + + $sanitize = $this->typed_schema['sanitize'] ?? null; + $sanitize_string = $this->ctx->getSanitizeStringConfig(); + if ($sanitize is nonnull && $sanitize_string === null) { + throw new \Exception('Specified `sanitize` on a string without providing sanitization functions.'); + } else if ($sanitize is nonnull && $sanitize_string is nonnull) { + if ($sanitize['multiline']) { + $sanitization_func = get_function_name_from_function($sanitize_string['multiline']); + } else { + $sanitization_func = get_function_name_from_function($sanitize_string['uniline']); + } + + $hb + ->addAssignment('$sanitize_string', "\\$sanitization_func<>", HackBuilderValues::literal()) + ->addAssignment('$typed', '$sanitize_string($typed)', HackBuilderValues::literal()) + ->ensureEmptyLine(); + } + if (!($this->typed_schema['generateHackEnum'] ?? false)) { + $this->addEnumConstraintCheck($hb); + } + + $max_length = $this->typed_schema['maxLength'] ?? null; + $min_length = $this->typed_schema['minLength'] ?? null; + $pattern = $this->typed_schema['pattern'] ?? null; + $format = $this->typed_schema['format'] ?? null; + + if ($pattern is nonnull) { + $hb->addMultilineCall('Constraints\StringPatternConstraint::check', vec['$typed', 'self::$pattern', '$pointer']); + } + + if ($format is nonnull) { + $hb->addMultilineCall('Constraints\StringFormatConstraint::check', vec['$typed', 'self::$format', '$pointer']); + } + + if ($max_length is nonnull || $min_length is nonnull) { + $hb->addAssignment('$length', '\mb_strlen($typed)', HackBuilderValues::literal()); + } + + if ($max_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMaxLengthConstraint::check', + vec['$length', 'self::$maxLength', '$pointer'], + ); + } + + if ($min_length is nonnull) { + $hb->addMultilineCall( + 'Constraints\StringMinLengthConstraint::check', + vec['$length', 'self::$minLength', '$pointer'], + ); + } + + $this->addHackEnumConstraintCheck($hb); + + $hb->addReturn('$typed', HackBuilderValues::literal()); + + return $this->codegenCheckMethod() + ->addParameters(vec['mixed $input', 'string $pointer']) + ->setBody($hb->getCode()) + ->setReturnType($this->getType()); + } + + <<__Override>> + public function getType(): string { + if (Shapes::keyExists($this->typed_schema, 'hackEnum')) { + if ($this->typed_schema['generateHackEnum'] ?? false) { + return $this->typed_schema['hackEnum']; + } + return Str\format('\%s', $this->typed_schema['hackEnum']); + } + + return 'string'; + } + + <<__Override>> + public function getTypeInfo(): Typing\Type { + if ($this->getType() === 'string') { + return Typing\TypeSystem::string(); + } + // TODO: Type resolution for hackEnum + return Typing\TypeSystem::nonnull(); + } } diff --git a/tests/examples/generated-hack-enum-schema.json b/tests/examples/generated-hack-enum-schema.json index 75221d2..5ab12de 100644 --- a/tests/examples/generated-hack-enum-schema.json +++ b/tests/examples/generated-hack-enum-schema.json @@ -1,12 +1,12 @@ { - "type": "object", - "properties": { - "enum_string": { - "type": "string", - "enum": ["one", "two", "three"], - "generateHackEnum": true, - "hackEnum": "myCoolTestEnum" - } - } + "type": "object", + "properties": { + "enum_string": { + "type": "string", + "enum": ["one", "two", "three"], + "generateHackEnum": true, + "hackEnum": "myCoolTestEnum" + } + } } \ No newline at end of file From 4415f1475229ac0a31a7ac27db9fa98ee5912b95 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Thu, 4 Jan 2024 14:23:07 -0800 Subject: [PATCH 11/12] typo in json --- tests/examples/generated-hack-enum-schema.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/examples/generated-hack-enum-schema.json b/tests/examples/generated-hack-enum-schema.json index 5ab12de..284da07 100644 --- a/tests/examples/generated-hack-enum-schema.json +++ b/tests/examples/generated-hack-enum-schema.json @@ -8,5 +8,4 @@ "hackEnum": "myCoolTestEnum" } } - } - \ No newline at end of file +} From 10c04d8368f7d144deebeda97b145cbcd53cfa74 Mon Sep 17 00:00:00 2001 From: Steven Cobb Date: Thu, 4 Jan 2024 15:17:53 -0800 Subject: [PATCH 12/12] darray -> dict --- tests/GeneratedHackEnumSchemaValidatorTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/GeneratedHackEnumSchemaValidatorTest.php b/tests/GeneratedHackEnumSchemaValidatorTest.php index aeeea44..64f686b 100644 --- a/tests/GeneratedHackEnumSchemaValidatorTest.php +++ b/tests/GeneratedHackEnumSchemaValidatorTest.php @@ -16,16 +16,16 @@ final class GeneratedHackEnumSchemaValidatorTest extends BaseCodegenTestCase { public function testStringEnum(): void { $cases = vec[ shape( - 'input' => darray['enum_string' => 'one'], - 'output' => darray['enum_string' => 'one'], + 'input' => dict['enum_string' => 'one'], + 'output' => dict['enum_string' => 'one'], 'valid' => true, ), shape( - 'input' => darray['enum_string' => 'four'], + 'input' => dict['enum_string' => 'four'], 'valid' => false, ), shape( - 'input' => darray['enum_string' => 1], + 'input' => dict['enum_string' => 1], 'valid' => false, ), ];