From 44756915a34b05b958f49ca5bd88dfbd992c5f3a Mon Sep 17 00:00:00 2001 From: Riley Aven Date: Fri, 30 Aug 2024 17:26:26 -0400 Subject: [PATCH] merge rule type customization --- config/rules-to-schema.php | 34 ++-------- src/Facades/LaravelRulesToSchema.php | 2 + src/LaravelRulesToSchema.php | 31 +++++++++ src/Parsers/CustomRuleSchemaParser.php | 57 ++++++++++++++++ src/Parsers/ExcludedParser.php | 3 +- src/Parsers/RuleHasJsonSchemaParser.php | 39 ----------- src/Parsers/TypeParser.php | 13 ++-- src/ParsesNormalizedRuleset.php | 7 +- .../can_register_custom_parsers.snap | 11 +++ .../can_register_custom_rule_schemas.snap | 11 +++ .../custom_rule_can_have_custom_schema.snap | 30 +++++++++ tests/ConfigurationTest.php | 67 ++++++++++++------- tests/Fixtures/TestCustomParser.php | 29 ++++++++ 13 files changed, 232 insertions(+), 102 deletions(-) create mode 100644 src/Parsers/CustomRuleSchemaParser.php delete mode 100644 src/Parsers/RuleHasJsonSchemaParser.php create mode 100644 tests/.pest/snapshots/ConfigurationTest/can_register_custom_parsers.snap create mode 100644 tests/.pest/snapshots/ConfigurationTest/can_register_custom_rule_schemas.snap create mode 100644 tests/Fixtures/TestCustomParser.php diff --git a/config/rules-to-schema.php b/config/rules-to-schema.php index 824a56d..0c27ae7 100644 --- a/config/rules-to-schema.php +++ b/config/rules-to-schema.php @@ -1,14 +1,13 @@ [ - 'string' => [ - ...LaravelRuleType::string(), - ], - 'integer' => [ - ...LaravelRuleType::integer(), - ], - 'number' => [ - ...LaravelRuleType::number(), - ], - 'boolean' => [ - ...LaravelRuleType::boolean(), - ], - 'nullable' => [ - ...LaravelRuleType::nullable(), - ], - 'array' => [ - ...LaravelRuleType::array(), - ], - 'exclude' => [ - ...LaravelRuleType::exclude(), - ], + CustomRuleSchemaParser::class, ], /* @@ -67,5 +39,7 @@ */ 'custom_rule_schemas' => [ // \CustomPackage\CustomRule::class => \Support\CustomRuleSchemaDefinition::class, + // \CustomPackage\CustomRule::class => 'string', + // \CustomPackage\CustomRule::class => ['null', 'string'], ], ]; diff --git a/src/Facades/LaravelRulesToSchema.php b/src/Facades/LaravelRulesToSchema.php index 064a7f0..7563e96 100644 --- a/src/Facades/LaravelRulesToSchema.php +++ b/src/Facades/LaravelRulesToSchema.php @@ -7,6 +7,8 @@ /** * @method static FluentSchema parse(array $rules) Parse Rules + * @method static void registerParser(string $parser) Register a rule parser + * @method static void registerCustomRuleSchema(string $rule, mixed $type) Register a schema for a custom rule * * @see \LaravelRulesToSchema\LaravelRulesToSchema */ diff --git a/src/LaravelRulesToSchema.php b/src/LaravelRulesToSchema.php index 06e9ef5..7c674fd 100755 --- a/src/LaravelRulesToSchema.php +++ b/src/LaravelRulesToSchema.php @@ -8,6 +8,10 @@ class LaravelRulesToSchema { use ParsesNormalizedRuleset; + protected static array $additionalParsers = []; + + protected static array $additionalCustomSchemas = []; + public function parse(array $rules): FluentSchema { $normalizedRules = (new ValidationRuleNormalizer($rules))->getRules(); @@ -28,4 +32,31 @@ public function parse(array $rules): FluentSchema return $schema; } + + public function getParsers(): array + { + return array_merge( + config('rules-to-schema.parsers'), + self::$additionalParsers, + ); + } + + public function registerParser(string $parser): void + { + self::$additionalParsers[] = $parser; + } + + public function getCustomRuleSchemas(): array + { + + return array_merge( + config('rules-to-schema.custom_rule_schemas'), + self::$additionalCustomSchemas, + ); + } + + public function registerCustomRuleSchema(string $rule, mixed $type): void + { + self::$additionalCustomSchemas[$rule] = $type; + } } diff --git a/src/Parsers/CustomRuleSchemaParser.php b/src/Parsers/CustomRuleSchemaParser.php new file mode 100644 index 0000000..9def840 --- /dev/null +++ b/src/Parsers/CustomRuleSchemaParser.php @@ -0,0 +1,57 @@ +toJsonSchema($attribute); + } elseif (array_key_exists($ruleName, LaravelRulesToSchema::getCustomRuleSchemas())) { + $typehint = LaravelRulesToSchema::getCustomRuleSchemas()[$ruleName]; + + if (is_string($typehint)) { + if (class_exists($typehint)) { + $instance = app($typehint); + + if (! $instance instanceof HasJsonSchema) { + throw new Exception('Custom rule schemas must implement '.HasJsonSchema::class); + } + + return $instance->toJsonSchema($attribute); + } else { + $schema->type()->fromString($typehint); + } + } elseif ($typehint instanceof JsonSchemaType) { + $schema->type()->fromString($typehint->value); + } elseif (is_array($typehint)) { + foreach ($typehint as $type) { + if ($type instanceof JsonSchemaType) { + $schema->type()->fromString($type->value); + } else { + $schema->type()->fromString($type); + } + } + } + } + } + + return $schema; + } +} diff --git a/src/Parsers/ExcludedParser.php b/src/Parsers/ExcludedParser.php index 865ebb0..981efc0 100644 --- a/src/Parsers/ExcludedParser.php +++ b/src/Parsers/ExcludedParser.php @@ -4,6 +4,7 @@ use FluentJsonSchema\FluentSchema; use LaravelRulesToSchema\Contracts\RuleParser; +use LaravelRulesToSchema\LaravelRuleType; class ExcludedParser implements RuleParser { @@ -11,7 +12,7 @@ public function __invoke(string $attribute, FluentSchema $schema, array $validat { foreach ($validationRules as $ruleArgs) { [$rule, $args] = $ruleArgs; - if (is_string($rule) && in_array($rule, config('rules-to-schema.rule_type_map.exclude'))) { + if (is_string($rule) && in_array($rule, LaravelRuleType::exclude())) { return null; } } diff --git a/src/Parsers/RuleHasJsonSchemaParser.php b/src/Parsers/RuleHasJsonSchemaParser.php deleted file mode 100644 index b3a21ed..0000000 --- a/src/Parsers/RuleHasJsonSchemaParser.php +++ /dev/null @@ -1,39 +0,0 @@ -toJsonSchema($attribute); - } elseif (array_key_exists($ruleName, config('rules-to-schema.custom_rule_schemas'))) { - $schemaClass = config('rules-to-schema.custom_rule_schemas')[$ruleName]; - - $instance = app($schemaClass); - - if (! $instance instanceof HasJsonSchema) { - throw new Exception('Custom rule schemas must implement '.HasJsonSchema::class); - } - - return $instance->toJsonSchema($attribute); - } - } - - return $schema; - } -} diff --git a/src/Parsers/TypeParser.php b/src/Parsers/TypeParser.php index 2a52c0b..6490399 100644 --- a/src/Parsers/TypeParser.php +++ b/src/Parsers/TypeParser.php @@ -6,6 +6,7 @@ use Illuminate\Validation\Rules\Enum as EnumRule; use Illuminate\Validation\Rules\In as InRule; use LaravelRulesToSchema\Contracts\RuleParser; +use LaravelRulesToSchema\LaravelRuleType; use ReflectionClass; class TypeParser implements RuleParser @@ -17,25 +18,25 @@ public function __invoke(string $attribute, FluentSchema $schema, array $validat $ruleName = is_object($rule) ? get_class($rule) : $rule; - if (in_array($ruleName, config('rules-to-schema.rule_type_map.string'))) { + if (in_array($ruleName, LaravelRuleType::string())) { $schema->type()->string(); } - if (in_array($ruleName, config('rules-to-schema.rule_type_map.integer'))) { + if (in_array($ruleName, LaravelRuleType::integer())) { $schema->type()->integer(); } - if (in_array($ruleName, config('rules-to-schema.rule_type_map.number'))) { + if (in_array($ruleName, LaravelRuleType::number())) { $schema->type()->number(); } - if (in_array($ruleName, config('rules-to-schema.rule_type_map.boolean'))) { + if (in_array($ruleName, LaravelRuleType::boolean())) { $schema->type()->boolean(); } - if (in_array($ruleName, config('rules-to-schema.rule_type_map.array'))) { + if (in_array($ruleName, LaravelRuleType::array())) { // Check if what we are dealing with is not an object type with properties if (count(array_diff_key($nestedRuleset, array_flip([config('rules-to-schema.validation_rule_token')]))) == 0) { $schema->type()->array(); } } - if (in_array($ruleName, config('rules-to-schema.rule_type_map.nullable'))) { + if (in_array($ruleName, LaravelRuleType::nullable())) { $schema->type()->null(); } diff --git a/src/ParsesNormalizedRuleset.php b/src/ParsesNormalizedRuleset.php index 1219f30..613ecea 100644 --- a/src/ParsesNormalizedRuleset.php +++ b/src/ParsesNormalizedRuleset.php @@ -3,6 +3,7 @@ namespace LaravelRulesToSchema; use FluentJsonSchema\FluentSchema; +use LaravelRulesToSchema\Contracts\RuleParser; use Mockery\Exception; trait ParsesNormalizedRuleset @@ -13,11 +14,11 @@ public function parseRuleset(string $name, array $nestedRuleset): null|FluentSch $schemas = [$name => FluentSchema::make()]; - foreach (config('rules-to-schema.parsers') as $parserClass) { + foreach (\LaravelRulesToSchema\Facades\LaravelRulesToSchema::getParsers() as $parserClass) { $instance = app($parserClass); - if (! $instance instanceof \LaravelRulesToSchema\Contracts\RuleParser) { - throw new Exception('Rule parsers must implement '.\LaravelRulesToSchema\Contracts\RuleParser::class); + if (! $instance instanceof RuleParser) { + throw new Exception('Rule parsers must implement '.RuleParser::class); } $newSchemas = []; diff --git a/tests/.pest/snapshots/ConfigurationTest/can_register_custom_parsers.snap b/tests/.pest/snapshots/ConfigurationTest/can_register_custom_parsers.snap new file mode 100644 index 0000000..0730fbe --- /dev/null +++ b/tests/.pest/snapshots/ConfigurationTest/can_register_custom_parsers.snap @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "custom": { + "type": "array", + "items": { + "type": "integer" + } + } + } +} \ No newline at end of file diff --git a/tests/.pest/snapshots/ConfigurationTest/can_register_custom_rule_schemas.snap b/tests/.pest/snapshots/ConfigurationTest/can_register_custom_rule_schemas.snap new file mode 100644 index 0000000..286150c --- /dev/null +++ b/tests/.pest/snapshots/ConfigurationTest/can_register_custom_rule_schemas.snap @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "custom": { + "type": [ + "null", + "string" + ] + } + } +} \ No newline at end of file diff --git a/tests/.pest/snapshots/ConfigurationTest/custom_rule_can_have_custom_schema.snap b/tests/.pest/snapshots/ConfigurationTest/custom_rule_can_have_custom_schema.snap index feefc7d..0d5fa10 100644 --- a/tests/.pest/snapshots/ConfigurationTest/custom_rule_can_have_custom_schema.snap +++ b/tests/.pest/snapshots/ConfigurationTest/custom_rule_can_have_custom_schema.snap @@ -22,6 +22,36 @@ } } } + }, + "integer": { + "type": "integer" + }, + "number": { + "type": "number" + }, + "boolean": { + "type": "boolean" + }, + "array": { + "type": "array" + }, + "nullable": { + "type": "null" + }, + "custom_rule_as_enum": { + "type": "string" + }, + "custom_rule_as_enum_array": { + "type": [ + "null", + "number" + ] + }, + "custom_multiple_types": { + "type": [ + "null", + "string" + ] } } } \ No newline at end of file diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index e78a317..7eb2915 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -1,43 +1,64 @@ CustomRuleSchemaDefinition::class, + 'custom_registered_rule' => CustomRuleSchemaDefinition::class, + + 'custom_string_rule' => 'string', + 'custom_integer_rule' => 'integer', + 'custom_number_rule' => 'number', + 'custom_boolean_rule' => 'boolean', + 'custom_array_rule' => 'array', + 'custom_nullable_rule' => 'null', + + 'custom_rule_as_enum' => JsonSchemaType::STRING, + 'custom_rule_as_enum_array' => [JsonSchemaType::NULL, JsonSchemaType::NUMBER], + + 'custom_multiple_types' => ['null', 'string'], + ]); $rules = [ - 'string' => ['custom_string_rule'], - 'string_class' => [new CustomRule], - 'integer' => ['custom_integer_rule'], - 'number' => ['custom_number_rule'], - 'boolean' => ['custom_boolean_rule'], - 'array' => ['custom_array_rule'], - 'nullable' => ['custom_nullable_rule'], - 'exclude' => ['custom_exclude_rule'], + 'custom_with_schema' => [new CustomRule], + 'custom_registered_rule' => ['custom_registered_rule'], + + 'integer' => ['custom_integer_rule'], + 'number' => ['custom_number_rule'], + 'boolean' => ['custom_boolean_rule'], + 'array' => ['custom_array_rule'], + 'nullable' => ['custom_nullable_rule'], + + 'custom_rule_as_enum' => ['custom_rule_as_enum'], + 'custom_rule_as_enum_array' => ['custom_rule_as_enum_array'], + + 'custom_multiple_types' => ['custom_multiple_types'], ]; expect(LaravelRulesToSchema::parse($rules)->compile()) ->toMatchSnapshot(); }); -test('custom rule can have custom schema', function () { - Config::set('rules-to-schema.custom_rule_schemas', [ - CustomRule::class => CustomRuleSchemaDefinition::class, - 'custom_registered_rule' => CustomRuleSchemaDefinition::class, - ]); +test('can register custom parsers', function () { + LaravelRulesToSchema::registerParser(TestCustomParser::class); + $rules = [ + 'custom' => ['test_custom_parser'], + ]; + expect(LaravelRulesToSchema::parse($rules)->compile()) + ->toMatchSnapshot(); +}); + +test('can register custom rule schemas', function () { + LaravelRulesToSchema::registerCustomRuleSchema('custom', ['null', 'string']); $rules = [ - 'custom_with_schema' => [new CustomRule], - 'custom_registered_rule' => ['custom_registered_rule'], + 'custom' => ['custom'], ]; expect(LaravelRulesToSchema::parse($rules)->compile()) diff --git a/tests/Fixtures/TestCustomParser.php b/tests/Fixtures/TestCustomParser.php new file mode 100644 index 0000000..0fd02a2 --- /dev/null +++ b/tests/Fixtures/TestCustomParser.php @@ -0,0 +1,29 @@ +type()->array() + ->items(FluentSchema::make() + ->type()->integer() + ) + ->return(); + } + } + + return $schema; + } +}