Browse Source

Support more inline validator forms

Support parsing `$request->validate(...)` without assignment, and `$this->validate($request, ...)`
shalvah 2 years ago
parent
commit
29940c2e05

+ 2 - 2
src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php

@@ -7,10 +7,10 @@ use PhpParser\Node;
 
 
 class GetFromInlineValidator extends GetFromInlineValidatorBase
 class GetFromInlineValidator extends GetFromInlineValidatorBase
 {
 {
-    protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool
+    protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
     {
     {
         // Only use this validator for body params if there's no "// Query parameters" comment above
         // Only use this validator for body params if there's no "// Query parameters" comment above
-        $comments = $validationAssignmentExpression->getComments();
+        $comments = $validationStatement->getComments();
         $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
         $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
         if (strpos(strtolower($comments), "query parameters") !== false) {
         if (strpos(strtolower($comments), "query parameters") !== false) {
             return false;
             return false;

+ 38 - 33
src/Extracting/Strategies/GetFromInlineValidatorBase.php

@@ -5,6 +5,10 @@ namespace Knuckles\Scribe\Extracting\Strategies;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Extracting\MethodAstParser;
 use Knuckles\Scribe\Extracting\MethodAstParser;
 use Knuckles\Scribe\Extracting\ParsesValidationRules;
 use Knuckles\Scribe\Extracting\ParsesValidationRules;
+use Knuckles\Scribe\Extracting\ValidationRulesFinders\RequestValidate;
+use Knuckles\Scribe\Extracting\ValidationRulesFinders\ThisValidate;
+use Knuckles\Scribe\Extracting\ValidationRulesFinders\ValidatorMake;
+use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 use PhpParser\Node;
 use PhpParser\Node;
 use PhpParser\Node\Stmt\ClassMethod;
 use PhpParser\Node\Stmt\ClassMethod;
 
 
@@ -27,44 +31,26 @@ class GetFromInlineValidatorBase extends Strategy
 
 
     public function lookForInlineValidationRules(ClassMethod $methodAst): array
     public function lookForInlineValidationRules(ClassMethod $methodAst): array
     {
     {
-        // Validation usually happens early on, so let's assume it's in the first 6 statements
-        $statements = array_slice($methodAst->stmts, 0, 6);
-
-        $validationRules = null;
-        $validationAssignmentExpression = null;
-        $index = null;
-        foreach ($statements as $index => $node) {
-            // Filter to only assignment expressions
-            if (!($node instanceof Node\Stmt\Expression) || !($node->expr instanceof Node\Expr\Assign)) {
-                continue;
-            }
-
-            $validationAssignmentExpression = $node->expr;
-            $rvalue = $validationAssignmentExpression->expr;
-
-            // Look for $validated = $request->validate(...)
-            if (
-                $rvalue instanceof Node\Expr\MethodCall && $rvalue->var instanceof Node\Expr\Variable
-                && in_array($rvalue->var->name, ["request", "req"]) && $rvalue->name->name == "validate"
-            ) {
-                $validationRules = $rvalue->args[0]->value;
-                break;
-            } else if (
-                // Try $validator = Validator::make(...)
-                $rvalue instanceof Node\Expr\StaticCall && !empty($rvalue->class->parts) && end($rvalue->class->parts) == "Validator"
-                && $rvalue->name->name == "make"
-            ) {
-                $validationRules = $rvalue->args[1]->value;
-                break;
-            }
+        // Validation usually happens early on, so let's assume it's in the first 10 statements
+        $statements = array_slice($methodAst->stmts, 0, 10);
+
+        // Todo remove in future
+        if (method_exists($this, 'isAssignmentMeantForThisStrategy')) {
+            c::error("A custom strategy of yours is using a removed method isAssignmentMeantForThisStrategy().\n");
+            c::error("Fix this by changing the method name to isValidationStatementMeantForThisStrategy()\n");
+            c::error("and changing the type of its argument to Node.\n");
+            exit(1);
         }
         }
 
 
-        if ($validationAssignmentExpression && !$this->isAssignmentMeantForThisStrategy($validationAssignmentExpression)) {
+        [$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements);
+
+        if ($validationStatement &&
+            !$this->isValidationStatementMeantForThisStrategy($validationStatement)) {
             return [[], []];
             return [[], []];
         }
         }
 
 
         // If validation rules were saved in a variable (like $rules),
         // If validation rules were saved in a variable (like $rules),
-        // find the var and expand the value
+        // try to find the var and expand the value
         if ($validationRules instanceof Node\Expr\Variable) {
         if ($validationRules instanceof Node\Expr\Variable) {
             foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
             foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
                 if (
                 if (
@@ -144,8 +130,27 @@ class GetFromInlineValidatorBase extends Strategy
         return true;
         return true;
     }
     }
 
 
-    protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool
+    protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
     {
     {
         return true;
         return true;
     }
     }
+
+    protected function findValidationExpression($statements): ?array
+    {
+        $strategies = [
+            RequestValidate::class, // $request->validate(...);
+            ValidatorMake::class, // Validator::make($request, ...)
+            ThisValidate::class, // $this->validate(...);
+        ];
+
+        foreach ($statements as $index => $node) {
+            foreach ($strategies as $strategy) {
+                if ($validationRules = $strategy::find($node)) {
+                    return [$index, $node, $validationRules];
+                }
+            }
+        }
+
+        return [null, null, null];
+    }
 }
 }

+ 2 - 2
src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php

@@ -7,10 +7,10 @@ use PhpParser\Node;
 
 
 class GetFromInlineValidator extends GetFromInlineValidatorBase
 class GetFromInlineValidator extends GetFromInlineValidatorBase
 {
 {
-    protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool
+    protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
     {
     {
         // Only use this validator for query params if there's a "// Query parameters" comment above
         // Only use this validator for query params if there's a "// Query parameters" comment above
-        $comments = $validationAssignmentExpression->getComments();
+        $comments = $validationStatement->getComments();
         $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
         $comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
         if (strpos(strtolower($comments), "query parameters") !== false) {
         if (strpos(strtolower($comments), "query parameters") !== false) {
             return true;
             return true;

+ 41 - 0
src/Extracting/ValidationRulesFinders/RequestValidate.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Knuckles\Scribe\Extracting\ValidationRulesFinders;
+
+use PhpParser\Node;
+
+/**
+ * This class looks for
+ *   $anyVariable = $request->validate(...);
+ * or just
+ *   $request->validate(...);
+ *
+ * Also supports `$req` instead of `$request`
+ * Also supports `->validateWithBag('', ...)`
+ */
+class RequestValidate
+{
+    public static function find(Node $node)
+    {
+        if (!($node instanceof Node\Stmt\Expression)) return;
+
+        $expr = $node->expr;
+        if ($expr instanceof Node\Expr\Assign) {
+            $expr = $expr->expr; // If it's an assignment, get the expression on the RHS
+        }
+
+        if (
+            $expr instanceof Node\Expr\MethodCall
+            && $expr->var instanceof Node\Expr\Variable
+            && in_array($expr->var->name, ["request", "req"])
+        ) {
+            if ($expr->name->name == "validate") {
+                return $expr->args[0]->value;
+            }
+
+            if ($expr->name->name == "validateWithBag") {
+                return $expr->args[1]->value;
+            }
+        }
+    }
+}

+ 36 - 0
src/Extracting/ValidationRulesFinders/ThisValidate.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Knuckles\Scribe\Extracting\ValidationRulesFinders;
+
+use PhpParser\Node;
+
+/**
+ * This class looks for
+ *   $anyVariable = $this->validate($request, ...);
+ * or just
+ *   $this->validate($request, ...);
+ *
+ * Also supports `$req` instead of `$request`
+ */
+class ThisValidate
+{
+    public static function find(Node $node)
+    {
+        if (!($node instanceof Node\Stmt\Expression)) return;
+
+        $expr = $node->expr;
+        if ($expr instanceof Node\Expr\Assign) {
+            $expr = $expr->expr; // If it's an assignment, get the expression on the RHS
+        }
+
+        if (
+            $expr instanceof Node\Expr\MethodCall
+            && $expr->var instanceof Node\Expr\Variable
+            && $expr->var->name === "this"
+        ) {
+            if ($expr->name->name == "validate") {
+                return $expr->args[1]->value;
+            }
+        }
+    }
+}

+ 34 - 0
src/Extracting/ValidationRulesFinders/ValidatorMake.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Knuckles\Scribe\Extracting\ValidationRulesFinders;
+
+use PhpParser\Node;
+
+/**
+ * This class looks for
+ *   $validator = Validator::make($request, ...)
+ *
+ * The variable names (`$validator` and `$request`) don't matter.
+ */
+class ValidatorMake
+{
+    public static function find(Node $node)
+    {
+        // Make sure it's an assignment
+        if (!($node instanceof Node\Stmt\Expression)
+            || !($node->expr instanceof Node\Expr\Assign)) {
+            return;
+        }
+
+        $expr = $node->expr->expr; // Get the expression on the RHS
+
+        if (
+            $expr instanceof Node\Expr\StaticCall
+            && !empty($expr->class->parts)
+            && end($expr->class->parts) == "Validator"
+            && $expr->name->name == "make"
+        ) {
+            return $expr->args[1]->value;
+        }
+    }
+}

+ 76 - 0
tests/Fixtures/TestController.php

@@ -446,6 +446,31 @@ class TestController extends Controller
         // Do stuff
         // Do stuff
     }
     }
 
 
+    public function withInlineRequestValidateNoAssignment(Request $request)
+    {
+        $request->validate([
+            // The id of the user. Example: 9
+            'user_id' => 'int|required',
+            // The id of the room.
+            'room_id' => ['string', 'in:3,5,6'],
+            // Whether to ban the user forever. Example: false
+            'forever' => 'boolean',
+            // Just need something here
+            'another_one' => 'numeric',
+            'even_more_param' => 'array',
+            'book.name' => 'string',
+            'book.author_id' => 'integer',
+            'book.pages_count' => 'integer',
+            'ids.*' => 'integer',
+            // The first name of the user. Example: John
+            'users.*.first_name' => ['string'],
+            // The last name of the user. Example: Doe
+            'users.*.last_name' => 'string',
+        ]);
+
+        // Do stuff
+    }
+
     public function withInlineRequestValidateQueryParams(Request $request)
     public function withInlineRequestValidateQueryParams(Request $request)
     {
     {
         // Query parameters
         // Query parameters
@@ -501,6 +526,57 @@ class TestController extends Controller
         }
         }
     }
     }
 
 
+    public function withInlineRequestValidateWithBag(Request $request)
+    {
+        $request->validateWithBag('stuff', [
+            // The id of the user. Example: 9
+            'user_id' => 'int|required',
+            // The id of the room.
+            'room_id' => ['string', 'in:3,5,6'],
+            // Whether to ban the user forever. Example: false
+            'forever' => 'boolean',
+            // Just need something here
+            'another_one' => 'numeric',
+            'even_more_param' => 'array',
+            'book.name' => 'string',
+            'book.author_id' => 'integer',
+            'book.pages_count' => 'integer',
+            'ids.*' => 'integer',
+            // The first name of the user. Example: John
+            'users.*.first_name' => ['string'],
+            // The last name of the user. Example: Doe
+            'users.*.last_name' => 'string',
+        ]);
+
+        // Do stuff
+    }
+
+
+    public function withInlineThisValidate(Request $request)
+    {
+        $this->validate($request, [
+            // The id of the user. Example: 9
+            'user_id' => 'int|required',
+            // The id of the room.
+            'room_id' => ['string', 'in:3,5,6'],
+            // Whether to ban the user forever. Example: false
+            'forever' => 'boolean',
+            // Just need something here
+            'another_one' => 'numeric',
+            'even_more_param' => 'array',
+            'book.name' => 'string',
+            'book.author_id' => 'integer',
+            'book.pages_count' => 'integer',
+            'ids.*' => 'integer',
+            // The first name of the user. Example: John
+            'users.*.first_name' => ['string'],
+            // The last name of the user. Example: Doe
+            'users.*.last_name' => 'string',
+        ]);
+
+        // Do stuff
+    }
+
     public function withInjectedModel(TestUser $user)
     public function withInjectedModel(TestUser $user)
     {
     {
         return null;
         return null;

+ 52 - 1
tests/Strategies/GetFromInlineValidatorTest.php

@@ -89,7 +89,7 @@ class GetFromInlineValidatorTest extends BaseLaravelTest
     ];
     ];
 
 
     /** @test */
     /** @test */
-    public function can_fetch_from_request_validate()
+    public function can_fetch_from_request_validate_assignment()
     {
     {
         $endpoint = new class extends ExtractedEndpointData {
         $endpoint = new class extends ExtractedEndpointData {
             public function __construct(array $parameters = [])
             public function __construct(array $parameters = [])
@@ -105,6 +105,57 @@ class GetFromInlineValidatorTest extends BaseLaravelTest
         $this->assertIsArray($results['ids']['example']);
         $this->assertIsArray($results['ids']['example']);
     }
     }
 
 
+    /** @test */
+    public function can_fetch_from_request_validate_expression()
+    {
+        $endpoint = new class extends ExtractedEndpointData {
+            public function __construct(array $parameters = [])
+            {
+                $this->method = new \ReflectionMethod(TestController::class, 'withInlineRequestValidateNoAssignment');
+            }
+        };
+
+        $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([]));
+        $results = $strategy($endpoint, []);
+
+        $this->assertArraySubset(self::$expected, $results);
+        $this->assertIsArray($results['ids']['example']);
+    }
+
+    /** @test */
+    public function can_fetch_from_request_validatewithbag()
+    {
+        $endpoint = new class extends ExtractedEndpointData {
+            public function __construct(array $parameters = [])
+            {
+                $this->method = new \ReflectionMethod(TestController::class, 'withInlineRequestValidateWithBag');
+            }
+        };
+
+        $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([]));
+        $results = $strategy($endpoint, []);
+
+        $this->assertArraySubset(self::$expected, $results);
+        $this->assertIsArray($results['ids']['example']);
+    }
+
+    /** @test */
+    public function can_fetch_from_this_validate()
+    {
+        $endpoint = new class extends ExtractedEndpointData {
+            public function __construct(array $parameters = [])
+            {
+                $this->method = new \ReflectionMethod(TestController::class, 'withInlineThisValidate');
+            }
+        };
+
+        $strategy = new BodyParameters\GetFromInlineValidator(new DocumentationConfig([]));
+        $results = $strategy($endpoint, []);
+
+        $this->assertArraySubset(self::$expected, $results);
+        $this->assertIsArray($results['ids']['example']);
+    }
+
     /** @test */
     /** @test */
     public function can_fetch_from_validator_make()
     public function can_fetch_from_validator_make()
     {
     {