Skip to content

Commit

Permalink
Support more inline validator forms
Browse files Browse the repository at this point in the history
Support parsing `$request->validate(...)` without assignment, and `$this->validate($request, ...)`
  • Loading branch information
shalvah committed Jun 10, 2022
1 parent 52b175a commit 29940c2
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

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
$comments = $validationAssignmentExpression->getComments();
$comments = $validationStatement->getComments();
$comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
if (strpos(strtolower($comments), "query parameters") !== false) {
return false;
Expand Down
71 changes: 38 additions & 33 deletions src/Extracting/Strategies/GetFromInlineValidatorBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Extracting\MethodAstParser;
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\Stmt\ClassMethod;

Expand All @@ -27,44 +31,26 @@ public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)

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 [[], []];
}

// 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) {
foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
if (
Expand Down Expand Up @@ -144,8 +130,27 @@ protected function shouldCastUserExample()
return true;
}

protected function isAssignmentMeantForThisStrategy(Node\Expr\Assign $validationAssignmentExpression): bool
protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
{
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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

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
$comments = $validationAssignmentExpression->getComments();
$comments = $validationStatement->getComments();
$comments = join("\n", array_map(fn($comment) => $comment->getReformattedText(), $comments));
if (strpos(strtolower($comments), "query parameters") !== false) {
return true;
Expand Down
41 changes: 41 additions & 0 deletions src/Extracting/ValidationRulesFinders/RequestValidate.php
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions src/Extracting/ValidationRulesFinders/ThisValidate.php
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions src/Extracting/ValidationRulesFinders/ValidatorMake.php
Original file line number Diff line number Diff line change
@@ -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 changes: 76 additions & 0 deletions tests/Fixtures/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,31 @@ public function withInlineRequestValidate(Request $request)
// 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)
{
// Query parameters
Expand Down Expand Up @@ -501,6 +526,57 @@ public function withInlineValidatorMake(Request $request)
}
}

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)
{
return null;
Expand Down
53 changes: 52 additions & 1 deletion tests/Strategies/GetFromInlineValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class GetFromInlineValidatorTest extends BaseLaravelTest
];

/** @test */
public function can_fetch_from_request_validate()
public function can_fetch_from_request_validate_assignment()
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
Expand All @@ -105,6 +105,57 @@ public function __construct(array $parameters = [])
$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 */
public function can_fetch_from_validator_make()
{
Expand Down

0 comments on commit 29940c2

Please sign in to comment.