Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for custom rules and closure rules #611

Merged
merged 4 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 53 additions & 18 deletions src/Extracting/ParsesValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ClosureValidationRule;
use Knuckles\Scribe\Exceptions\CouldntProcessValidationRule;
use Knuckles\Scribe\Exceptions\ProblemParsingValidationRules;
use Knuckles\Scribe\Exceptions\ScribeException;
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
use Knuckles\Scribe\Tools\WritingUtils as w;
use ReflectionClass;
use Throwable;

trait ParsesValidationRules
Expand Down Expand Up @@ -164,11 +166,56 @@ protected function normaliseRules(array $rules): array
*/
protected function parseRule($rule, array &$parameterData, bool $independentOnly, array $allParameters = []): bool
{
try {
if (!(is_string($rule) || $rule instanceof Rule)) {
return true;
// Reminders:
// 1. Append to the description (with a leading space); don't overwrite.
// 2. Avoid testing on the value of $parameterData['type'],
// as that may not have been set yet, since the rules can be in any order.
// For this reason, only deterministic rules are supported
// 3. All rules supported must be rules that we can generate a valid dummy value for.

if ($rule instanceof ClosureValidationRule || $rule instanceof \Closure) {
$reflection = new \ReflectionFunction($rule instanceof ClosureValidationRule ? $rule->callback : $rule);

if (is_string($description = $reflection->getDocComment())) {
$finalDescription = '';
// Cleanup comment block and extract just the description
foreach (explode("\n", $description) as $line) {
$cleaned = preg_replace(['/\*+\/$/', '/^\/\*+\s*/', '/^\*+\s*/'], '', trim($line));
if ($cleaned != '') $finalDescription .= ' ' . $cleaned;
}

$parameterData['description'] .= $finalDescription;
}

return true;
}

if ($rule instanceof Rule) {
if (method_exists($rule, 'docs')) {
$customData = call_user_func_array([$rule, 'docs'], []) ?: [];

if (isset($customData['description'])) {
$parameterData['description'] .= ' ' . $customData['description'];
}
if (isset($customData['example'])) {
$parameterData['setter'] = fn() => $customData['example'];
} elseif (isset($customData['setter'])) {
$parameterData['setter'] = $customData['setter'];
}

$parameterData = array_merge($parameterData, Arr::except($customData, [
'description', 'example', 'setter',
]));
}

return true;
}

if (!is_string($rule)) {
return false;
}

try {
// Convert string rules into rule + arguments (eg "in:1,2" becomes ["in", ["1", "2"]])
$parsedRule = $this->parseStringRuleIntoRuleAndArguments($rule);
[$rule, $arguments] = $parsedRule;
Expand All @@ -178,12 +225,6 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
return false;
}

// Reminders:
// 1. Append to the description (with a leading space); don't overwrite.
// 2. Avoid testing on the value of $parameterData['type'],
// as that may not have been set yet, since the rules can be in any order.
// For this reason, only deterministic rules are supported
// 3. All rules supported must be rules that we can generate a valid dummy value for.
switch ($rule) {
case 'required':
$parameterData['required'] = true;
Expand All @@ -196,7 +237,7 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
break;

/*
* Primitive types. No description should be added
* Primitive types. No description should be added
*/
case 'bool':
case 'boolean':
Expand Down Expand Up @@ -453,11 +494,11 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
// Other rules not supported
break;
}

return true;
} catch (Throwable $e) {
throw CouldntProcessValidationRule::forParam($parameterData['name'], $rule, $e);
}

return true;
}

/**
Expand All @@ -474,12 +515,6 @@ protected function parseStringRuleIntoRuleAndArguments($rule): array
{
$ruleArguments = [];

// Convert any custom Rule objects to strings
if ($rule instanceof Rule) {
$className = substr(strrchr(get_class($rule), "\\"), 1);
return [$className, []];
}

if (strpos($rule, ':') !== false) {
[$rule, $argumentsString] = explode(':', $rule, 2);

Expand Down
42 changes: 42 additions & 0 deletions tests/Strategies/GetFromFormRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ public function allows_customisation_of_form_request_instantiation()
Globals::$__instantiateFormRequestUsing = null;
}

/** @test */
public function custom_rule_example_doesnt_override_form_request_example()
{
$strategy = new BodyParameters\GetFromFormRequest(new DocumentationConfig([]));
$parametersFromFormRequest = $strategy->getParametersFromValidationRules(
[
'dummy' => ['required', new DummyValidationRule],
],
[
'dummy' => [
'description' => 'New description.',
'example' => 'Overrided example.',
],
],
);

$parsed = $strategy->normaliseArrayAndObjectParameters($parametersFromFormRequest);
$this->assertEquals('Overrided example.', $parsed['dummy']['example']);
$this->assertEquals('New description. This is a dummy test rule.', $parsed['dummy']['description']);
}

protected function fetchViaBodyParams(\ReflectionMethod $method): array
{
$strategy = new BodyParameters\GetFromFormRequest(new DocumentationConfig([]));
Expand All @@ -254,3 +275,24 @@ protected function fetchViaQueryParams(\ReflectionMethod $method): array
return $strategy->getParametersFromFormRequest($method, $route);
}
}

class DummyValidationRule implements \Illuminate\Contracts\Validation\Rule
{
public function passes($attribute, $value)
{
return true;
}

public function message()
{
return '.';
}

public static function docs()
{
return [
'description' => 'This is a dummy test rule.',
'example' => 'Default example, only added if none other give.',
];
}
}
76 changes: 76 additions & 0 deletions tests/Unit/ValidationRuleParsingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,61 @@ public function child_does_not_overwrite_parent_status()
$this->assertCount(2, $results);
$this->assertEquals(true, $results['array_param']['required']);
}

/** @test */
public function can_parse_custom_closure_rules()
{
// Single line DocComment
$ruleset = [
'closure' => [
'bail', 'required',
/** This is a single line parsed closure rule. */
function ($attribute, $value, $fail) {
$fail('Always fail.');
},
],
];

$results = $this->strategy->parse($ruleset);
$this->assertEquals(
'This is a single line parsed closure rule.',
$results['closure']['description']
);

// Block DocComment
$ruleset = [
'closure' => [
'bail', 'required',
/**
* This is a block DocComment
* parsed on a closure rule.
* Extra info.
*/
function ($attribute, $value, $fail) {
$fail('Always fail.');
},
],
];

$results = $this->strategy->parse($ruleset);
$this->assertEquals(
'This is a block DocComment parsed on a closure rule. Extra info.',
$results['closure']['description']
);
}

/** @test */
public function can_parse_custom_rule_classes()
{
$ruleset = [
// The page number. Example: 1
'custom_rule' => ['bail', 'required', new DummyWithDocsValidationRule],
];

$results = $this->strategy->parse($ruleset);
$this->assertEquals(true, $results['custom_rule']['required']);
$this->assertEquals('This is a dummy test rule.', $results['custom_rule']['description']);
}
}

class DummyValidationRule implements \Illuminate\Contracts\Validation\Rule
Expand All @@ -465,3 +520,24 @@ public function message()
return '.';
}
}

class DummyWithDocsValidationRule implements \Illuminate\Contracts\Validation\Rule
{
public function passes($attribute, $value)
{
return true;
}

public function message()
{
return '.';
}

public static function docs()
{
return [
'description' => 'This is a dummy test rule.',
'example' => 'Default example, only added if none other give.',
];
}
}