Sfoglia il codice sorgente

Merge pull request #616 from MissaelAnda/parse-inline-enums

Parses inline Enum Rules
Shalvah 2 anni fa
parent
commit
45f1bda7a5

+ 44 - 0
src/Extracting/Strategies/GetFromInlineValidatorBase.php

@@ -82,6 +82,14 @@ class GetFromInlineValidatorBase extends Strategy
                         $rulesList[] = $arrayItem->value->value;
                     }
 
+                    // Try to extract Enum rule
+                    else if (
+                        function_exists('enum_exists') &&
+                        ($enum = $this->extractEnumClassFromArrayItem($arrayItem)) &&
+                        enum_exists($enum) && method_exists($enum, 'tryFrom')
+                    ) {
+                        $rulesList[] = 'in:' . implode(',', array_map(fn ($case) => $case->value, $enum::cases()));
+                    }
                 }
                 $rules[$paramName] = join('|', $rulesList);
             } else {
@@ -111,6 +119,42 @@ class GetFromInlineValidatorBase extends Strategy
         return [$rules, $customParameterData];
     }
 
+    protected function extractEnumClassFromArrayItem(Node\Expr\ArrayItem $arrayItem): ?string
+    {
+        $args = [];
+
+        // Enum rule with the form "new Enum(...)"
+        if ($arrayItem->value instanceof Node\Expr\New_ &&
+            $arrayItem->value->class instanceof Node\Name &&
+            last($arrayItem->value->class->parts) === 'Enum'
+        ) {
+            $args = $arrayItem->value->args;
+        }
+
+        // Enum rule with the form "Rule::enum(...)"
+        else if ($arrayItem->value instanceof Node\Expr\StaticCall &&
+            $arrayItem->value->class instanceof Node\Name &&
+            last($arrayItem->value->class->parts) === 'Rule' &&
+            $arrayItem->value->name instanceof Node\Identifier &&
+            $arrayItem->value->name->name === 'enum'
+        ) {
+            $args = $arrayItem->value->args;
+        }
+
+        if (count($args) !== 1 || !$args[0] instanceof Node\Arg) return null;
+
+        $arg = $args[0];
+        if ($arg->value instanceof Node\Expr\ClassConstFetch &&
+            $arg->value->class instanceof Node\Name
+        ) {
+            return '\\' . implode('\\', $arg->value->class->parts);
+        } else if ($arg->value instanceof Node\Scalar\String_) {
+            return $arg->value->value;
+        }
+
+        return null;
+    }
+
     protected function getMissingCustomDataMessage($parameterName)
     {
         return "No extra data found for parameter '$parameterName' from your inline validator. You can add a comment above '$parameterName' with a description and example.";

+ 11 - 0
tests/Fixtures/TestController.php

@@ -4,6 +4,7 @@ namespace Knuckles\Scribe\Tests\Fixtures;
 
 use Illuminate\Http\Request;
 use Illuminate\Routing\Controller;
+use Illuminate\Validation\Rule;
 use Knuckles\Scribe\Tools\Utils;
 
 /**
@@ -578,6 +579,16 @@ class TestController extends Controller
         return null;
     }
 
+    public function withEnumRule(Request $request)
+    {
+        $request->validate([
+            'enum_class' => ['required', new Rules\Enum(\Knuckles\Scribe\Tests\Fixtures\TestStringBackedEnum::class), 'nullable'],
+            'enum_string' => ['required', Rule::enum('\Knuckles\Scribe\Tests\Fixtures\TestIntegerBackedEnum'), 'nullable'],
+            // Not full path class call won't work
+            'enum_inexistent' => ['required', new Rules\Enum(TestStringBackedEnum::class)],
+        ]);
+    }
+
     /**
      * Can only run on PHP 8.1
     public function withInjectedEnumAndModel(Category $category, TestUser $user)

+ 45 - 0
tests/Strategies/GetFromInlineValidatorTest.php

@@ -7,6 +7,7 @@ use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Extracting\Strategies\BodyParameters;
 use Knuckles\Scribe\Extracting\Strategies\QueryParameters;
 use Knuckles\Scribe\Tests\BaseLaravelTest;
+use Knuckles\Scribe\Tests\Fixtures;
 use Knuckles\Scribe\Tests\Fixtures\TestController;
 use Knuckles\Scribe\Tools\DocumentationConfig;
 use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
@@ -175,6 +176,50 @@ class GetFromInlineValidatorTest extends BaseLaravelTest
         $this->assertEquals([], $results);
     }
 
+    /** @test */
+    public function can_fetch_inline_enum_rules()
+    {
+        if (phpversion() < 8.1) {
+            $this->markTestSkipped('Enums are only supported in PHP 8.1 or later');
+        }
+
+        $endpoint = $this->endpoint(function (ExtractedEndpointData $e) {
+            $e->method = new \ReflectionMethod(TestController::class, 'withEnumRule');
+        });
+
+        $results = $this->fetchViaBodyParams($endpoint);
+
+        $expected = [
+            'enum_class' => [
+                'type' => 'string',
+                'description' => 'Must be one of <code>red</code>, <code>green</code>, or <code>blue</code>.',
+                'required' => true,
+            ],
+            'enum_string' => [
+                'type' => 'string',
+                'description' => 'Must be one of <code>1</code>, <code>2</code>, or <code>3</code>.',
+                'required' => true,
+            ],
+            'enum_inexistent' => [
+                'type' => 'string',
+                'description' => 'Not full path class call won\'t work.',
+                'required' => true,
+            ],
+        ];
+
+        $getCase = fn ($case) => $case->value;
+
+        $this->assertArraySubset($expected, $results);
+        $this->assertTrue(in_array(
+            $results['enum_class']['example'],
+            array_map($getCase, Fixtures\TestStringBackedEnum::cases())
+        ));
+        $this->assertTrue(in_array(
+            $results['enum_string']['example'],
+            array_map($getCase, Fixtures\TestIntegerBackedEnum::cases())
+        ));
+    }
+
     protected function endpoint(Closure $configure): ExtractedEndpointData
     {
         $endpoint = new class extends ExtractedEndpointData {