mirror of
https://github.com/ambieco/scribe.git
synced 2026-04-24 10:44:55 +08:00
Implement inline validator support
This commit is contained in:
@@ -12,6 +12,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
### Removals
|
||||
|
||||
## 2.7.2 (Saturday, 22 May 2021)
|
||||
### Fixed
|
||||
- Fix laravel type generation (https://github.com/knuckleswtf/scribe/pull/218)
|
||||
|
||||
## 2.7.1 (Friday, 21 May 2021)
|
||||
### Fixed
|
||||
- Use correct Laravel public path (https://github.com/knuckleswtf/scribe/pull/216)
|
||||
|
||||
## 2.7.0 (Friday, 21 May 2021)
|
||||
### Modified
|
||||
- Use Laravel `public_path` rather than `public/` for assets (https://github.com/knuckleswtf/scribe/pull/214)
|
||||
|
||||
@@ -24,4 +24,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
- Support headers in response
|
||||
- Include responses in Postman collection
|
||||
- Body parameters array
|
||||
- Auto detect Dingo
|
||||
- Auto detect Dingo
|
||||
- Removed `$stage`
|
||||
@@ -27,6 +27,7 @@
|
||||
"illuminate/support": "^6.0|^7.0|^8.0",
|
||||
"league/flysystem": "^1.0",
|
||||
"mpociot/reflection-docblock": "^1.0.1",
|
||||
"nikic/php-parser": "^4.10",
|
||||
"nunomaduro/collision": "^3.0|^4.0|^5.0",
|
||||
"ramsey/uuid": "^3.8|^4.0",
|
||||
"shalvah/clara": "^2.6",
|
||||
|
||||
@@ -42,5 +42,8 @@
|
||||
<file>tests/Unit/OpenAPISpecWriterTest.php</file>
|
||||
<file>tests/Unit/PostmanCollectionWriterTest.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Unit Tests 4">
|
||||
<file>tests/Unit/ValidationRuleParsingTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
||||
@@ -10,7 +10,7 @@ $response = $client->{{ strtolower($endpoint->methods[0]) }}(
|
||||
[
|
||||
@if(!empty($endpoint->headers))@php
|
||||
// We don't need the Content-Type header because Guzzle sets it automatically when you use json or multipart.
|
||||
unset($route['headers']['Content-Type']);
|
||||
unset($endpoint->headers['Content-Type']);
|
||||
@endphp
|
||||
'headers' => {!! u::printPhpValue($endpoint->headers, 8) !!},
|
||||
@endif
|
||||
|
||||
@@ -11,35 +11,17 @@ use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
*/
|
||||
class ApiDetails
|
||||
{
|
||||
/**
|
||||
* @var DocumentationConfig
|
||||
*/
|
||||
private $config;
|
||||
private DocumentationConfig $config;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $baseUrl;
|
||||
private string $baseUrl;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $preserveUserChanges;
|
||||
private bool $preserveUserChanges;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $markdownOutputPath = '.scribe';
|
||||
private string $markdownOutputPath = '.scribe';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $fileModificationTimesFile;
|
||||
private string $fileModificationTimesFile;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $lastTimesWeModifiedTheseFiles = [];
|
||||
private array $lastTimesWeModifiedTheseFiles = [];
|
||||
|
||||
public function __construct(DocumentationConfig $config = null, bool $preserveUserChanges = true)
|
||||
{
|
||||
|
||||
95
src/Extracting/MethodAstParser.php
Normal file
95
src/Extracting/MethodAstParser.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Exception;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\ParserFactory;
|
||||
use ReflectionFunctionAbstract;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* MethodAstParser
|
||||
* Utility class to help with retrieving (and caching) ASTs of route methods.
|
||||
*/
|
||||
class MethodAstParser
|
||||
{
|
||||
protected static array $methodAsts = [];
|
||||
protected static array $classAsts = [];
|
||||
|
||||
public static function getMethodAst(ReflectionFunctionAbstract $method)
|
||||
{
|
||||
$methodName = $method->name;
|
||||
$fileName = $method->getFileName();
|
||||
|
||||
$methodAst = self::getCachedMethodAst($fileName, $methodName);
|
||||
|
||||
if ($methodAst) {
|
||||
return $methodAst;
|
||||
}
|
||||
|
||||
$classAst = self::getClassAst($fileName);
|
||||
$methodAst = self::findMethodInClassAst($classAst, $methodName);
|
||||
self::cacheMethodAst($fileName, $methodName, $methodAst);
|
||||
|
||||
return $methodAst;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sourceCode
|
||||
*
|
||||
* @return \PhpParser\Node\Stmt[]|null
|
||||
*/
|
||||
protected static function parseClassSourceCode(string $sourceCode): ?array
|
||||
{
|
||||
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
||||
try {
|
||||
$ast = $parser->parse($sourceCode);
|
||||
} catch (Throwable $error) {
|
||||
throw new Exception("Parse error: {$error->getMessage()}");
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \PhpParser\Node\Stmt[] $ast
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return Node|null
|
||||
*/
|
||||
protected static function findMethodInClassAst(array $ast, string $methodName)
|
||||
{
|
||||
$nodeFinder = new NodeFinder;
|
||||
return $nodeFinder->findFirst($ast, function(Node $node) use ($methodName) {
|
||||
// Todo handle closures
|
||||
return $node instanceof Node\Stmt\ClassMethod
|
||||
&& $node->name->toString() === $methodName;
|
||||
});
|
||||
}
|
||||
|
||||
protected static function getCachedMethodAst(string $fileName, string $methodName)
|
||||
{
|
||||
$key = self::getAstCacheId($fileName, $methodName);
|
||||
return self::$methodAsts[$key] ?? null;
|
||||
}
|
||||
|
||||
protected static function cacheMethodAst(string $fileName, string $methodName, Node $methodAst)
|
||||
{
|
||||
$key = self::getAstCacheId($fileName, $methodName);
|
||||
self::$methodAsts[$key] = $methodAst;
|
||||
}
|
||||
|
||||
private static function getAstCacheId(string $fileName, string $methodName): string
|
||||
{
|
||||
return $fileName . "///". $methodName;
|
||||
}
|
||||
|
||||
private static function getClassAst(string $fileName)
|
||||
{
|
||||
$classAst = self::$classAsts[$fileName]
|
||||
?? self::parseClassSourceCode(file_get_contents($fileName));
|
||||
return self::$classAsts[$fileName] = $classAst;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,10 @@ trait ParamHelpers
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === "array") {
|
||||
$type = "string[]";
|
||||
}
|
||||
|
||||
if (Str::endsWith($type, '[]')) {
|
||||
$baseType = strtolower(substr($type, 0, strlen($type) - 2));
|
||||
return is_array($value) ? array_map(function ($v) use ($baseType) {
|
||||
|
||||
479
src/Extracting/ParsesValidationRules.php
Normal file
479
src/Extracting/ParsesValidationRules.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\WritingUtils as w;
|
||||
|
||||
trait ParsesValidationRules
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
public static \stdClass $MISSING_VALUE;
|
||||
|
||||
public function getBodyParametersFromValidationRules(array $validationRules, array $customParameterData = []): array
|
||||
{
|
||||
self::$MISSING_VALUE = new \stdClass();
|
||||
$validationRules = $this->normaliseRules($validationRules);
|
||||
|
||||
$parameters = [];
|
||||
foreach ($validationRules as $parameter => $ruleset) {
|
||||
if (count($customParameterData) && !isset($customParameterData[$parameter])) {
|
||||
c::debug($this->getMissingCustomDataMessage($parameter));
|
||||
}
|
||||
$userSpecifiedParameterInfo = $customParameterData[$parameter] ?? [];
|
||||
|
||||
$parameterData = [
|
||||
'name' => $parameter,
|
||||
'required' => false,
|
||||
'type' => null,
|
||||
'example' => self::$MISSING_VALUE,
|
||||
'description' => '',
|
||||
];
|
||||
|
||||
// Make sure the user-specified example overwrites others.
|
||||
if (isset($userSpecifiedParameterInfo['example'])) {
|
||||
$parameterData['example'] = $userSpecifiedParameterInfo['example'];
|
||||
}
|
||||
|
||||
foreach ($ruleset as $rule) {
|
||||
$this->parseRule($rule, $parameterData);
|
||||
}
|
||||
if ($this->shouldCastUserExample() && isset($userSpecifiedParameterInfo['example'])) {
|
||||
// Examples in comments are strings, we need to cast them properly
|
||||
$parameterData['example'] = $this->castToType($userSpecifiedParameterInfo['example'], $parameterData['type'] ?? 'string');
|
||||
}
|
||||
|
||||
$parameterData = $this->setParameterExampleAndDescription($parameterData, $userSpecifiedParameterInfo);
|
||||
|
||||
$parameterData['name'] = $parameter;
|
||||
$parameters[$parameter] = $parameterData;
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform validation rules from:
|
||||
* 'param1' => 'int|required' TO 'param1' => ['int', 'required']
|
||||
*
|
||||
* @param array<string,string|string[]> $rules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function normaliseRules(array $rules): array
|
||||
{
|
||||
// We can simply call Validator::make($data, $rules)->getRules() to get the normalised rules,
|
||||
// but Laravel will ignore any nested array rules (`ids.*')
|
||||
// unless the key referenced (`ids`) exists in the dataset and is a non-empty array
|
||||
// So we'll create a single-item array for each array parameter
|
||||
$testData = [];
|
||||
foreach ($rules as $key => $ruleset) {
|
||||
if (!Str::contains($key, '.*')) continue;
|
||||
|
||||
// All we need is for Laravel to see this key exists
|
||||
Arr::set($testData, str_replace('.*', '.0', $key), Str::random());
|
||||
}
|
||||
|
||||
// Now this will return the complete ruleset.
|
||||
// Nested array parameters will be present, with '*' replaced by '0'
|
||||
$newRules = Validator::make($testData, $rules)->getRules();
|
||||
|
||||
// Transform the key names back from 'ids.0' to 'ids.*'
|
||||
return collect($newRules)->mapWithKeys(function ($val, $paramName) use ($rules) {
|
||||
if (Str::contains($paramName, '.0')) {
|
||||
$genericArrayKeyName = str_replace('.0', '.*', $paramName);
|
||||
|
||||
// But only if that was the original value
|
||||
if (isset($rules[$genericArrayKeyName])) {
|
||||
$paramName = $genericArrayKeyName;
|
||||
}
|
||||
}
|
||||
|
||||
return [$paramName => $val];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a validation rule and extract a parameter type, description and setter (used to generate an example).
|
||||
*
|
||||
* @param $rule
|
||||
* @param $parameterData
|
||||
*/
|
||||
protected function parseRule($rule, &$parameterData)
|
||||
{
|
||||
// Convert string rules into rule + arguments (eg "in:1,2" becomes ["in", ["1", "2"]])
|
||||
$parsedRule = $this->parseStringRuleIntoRuleAndArguments($rule);
|
||||
[$rule, $arguments] = $parsedRule;
|
||||
|
||||
// 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;
|
||||
break;
|
||||
|
||||
/*
|
||||
* Primitive types. No description should be added
|
||||
*/
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
$parameterData['setter'] = function () {
|
||||
return Arr::random([true, false]);
|
||||
};
|
||||
$parameterData['type'] = 'boolean';
|
||||
break;
|
||||
case 'string':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('string');
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
break;
|
||||
case 'int':
|
||||
case 'integer':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('integer');
|
||||
};
|
||||
$parameterData['type'] = 'integer';
|
||||
break;
|
||||
case 'numeric':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('number');
|
||||
};
|
||||
$parameterData['type'] = 'number';
|
||||
break;
|
||||
case 'array':
|
||||
$parameterData['setter'] = function () {
|
||||
return [$this->generateDummyValue('string')];
|
||||
};
|
||||
$parameterData['type'] = 'array'; // The cleanup code in normaliseArrayAndObjectParameters() will set this to a valid type (x[] or object)
|
||||
break;
|
||||
case 'file':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= 'The value must be a file.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Special string types
|
||||
*/
|
||||
case 'timezone':
|
||||
// Laravel's message merely says "The value must be a valid zone"
|
||||
$parameterData['description'] .= "The value must be a valid time zone, such as <code>Africa/Accra</code>. ";
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->timezone;
|
||||
};
|
||||
break;
|
||||
case 'email':
|
||||
$parameterData['description'] .= $this->getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->safeEmail;
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
break;
|
||||
case 'url':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->url;
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel's message is "The value format is invalid". Ugh.🤮
|
||||
$parameterData['description'] .= "The value must be a valid URL. ";
|
||||
break;
|
||||
case 'ip':
|
||||
$parameterData['description'] .= $this->getDescription($rule) . ' ';
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->ipv4;
|
||||
};
|
||||
break;
|
||||
case 'json':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= $this->getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return json_encode([$this->getFaker()->word, $this->getFaker()->word,]);
|
||||
};
|
||||
break;
|
||||
case 'date':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= $this->getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return date('Y-m-d\TH:i:s', time());
|
||||
};
|
||||
break;
|
||||
case 'date_format':
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel description here is "The value must match the format Y-m-d". Not descriptive enough.
|
||||
$parameterData['description'] .= "The value must be a valid date in the format {$arguments[0]} ";
|
||||
$parameterData['setter'] = function () use ($arguments) {
|
||||
return date($arguments[0], time());
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Special number types. Some rules here may apply to other types, but we treat them as being numeric.
|
||||
*/
|
||||
/*
|
||||
* min, max and between not supported until we can figure out a proper way
|
||||
* to make them compatible with multiple types (string, number, file)
|
||||
case 'min':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':min' => $arguments[0]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween($arguments[0]); };
|
||||
break;
|
||||
case 'max':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':max' => $arguments[0]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween(0, $arguments[0]); };
|
||||
break;
|
||||
case 'between':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':min' => $arguments[0], ':max' => $arguments[1]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween($arguments[0], $arguments[1]); };
|
||||
break;*/
|
||||
|
||||
/**
|
||||
* Special file types.
|
||||
*/
|
||||
case 'image':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= $this->getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
// This is fine because the file example generator generates an image
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Other rules.
|
||||
*/
|
||||
case 'in':
|
||||
// Not using the rule description here because it only says "The attribute is invalid"
|
||||
$description = 'The value must be one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments);
|
||||
$parameterData['description'] .= $description . ' ';
|
||||
$parameterData['setter'] = function () use ($arguments) {
|
||||
return Arr::random($arguments);
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
// Other rules not supported
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string rule into the base rule and arguments.
|
||||
* Laravel validation rules are specified in the format {rule}:{arguments}
|
||||
* Arguments are separated by commas.
|
||||
* For instance the rule "max:3" states that the value may only be three letters.
|
||||
*
|
||||
* @param string|Rule $rule
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseStringRuleIntoRuleAndArguments($rule): array
|
||||
{
|
||||
$ruleArguments = [];
|
||||
|
||||
// Convert any 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);
|
||||
|
||||
// These rules can have ommas in their arguments, so we don't split on commas
|
||||
if (in_array(strtolower($rule), ['regex', 'date', 'date_format'])) {
|
||||
$ruleArguments = [$argumentsString];
|
||||
} else {
|
||||
$ruleArguments = str_getcsv($argumentsString);
|
||||
}
|
||||
}
|
||||
|
||||
return [strtolower(trim($rule)), $ruleArguments];
|
||||
}
|
||||
|
||||
protected function setParameterExampleAndDescription(array $parameterData, array $userSpecifiedParameterInfo): array
|
||||
{
|
||||
// If no example was given by the user, set an autogenerated example.
|
||||
// Each parsed rule returns a 'setter' function. We'll evaluate the last one.
|
||||
if ($parameterData['example'] === self::$MISSING_VALUE && isset($parameterData['setter'])) {
|
||||
$parameterData['example'] = $parameterData['setter']();
|
||||
}
|
||||
|
||||
// Make sure the user-specified description comes first (and add full stops where needed).
|
||||
$userSpecifiedDescription = $userSpecifiedParameterInfo['description'] ?? '';
|
||||
if (!empty($userSpecifiedDescription) && !Str::endsWith($userSpecifiedDescription, '.')) {
|
||||
$userSpecifiedDescription .= '.';
|
||||
}
|
||||
$validationDescription = trim($parameterData['description'] ?: '');
|
||||
$fullDescription = trim($userSpecifiedDescription . ' ' . trim($validationDescription));
|
||||
$parameterData['description'] = $fullDescription ? rtrim($fullDescription, '.') . '.' : $fullDescription;
|
||||
|
||||
// Set a default type
|
||||
if (is_null($parameterData['type'])) {
|
||||
$parameterData['type'] = 'string';
|
||||
}
|
||||
// If the parameter is required and has no example, generate one.
|
||||
if ($parameterData['required'] === true && $parameterData['example'] === self::$MISSING_VALUE) {
|
||||
$parameterData['example'] = $this->generateDummyValue($parameterData['type']);
|
||||
}
|
||||
|
||||
if (!is_null($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE) {
|
||||
// Casting again is important since values may have been cast to string in the validator
|
||||
$parameterData['example'] = $this->castToType($parameterData['example'], $parameterData['type']);
|
||||
}
|
||||
return $parameterData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel uses .* notation for arrays. This PR aims to normalise that into our "new syntax".
|
||||
*
|
||||
* 'years.*' with type 'integer' becomes 'years' with type 'integer[]'
|
||||
* 'cars.*.age' with type 'string' becomes 'cars[].age' with type 'string' and 'cars' with type 'object[]'
|
||||
* 'cars.*.things.*.*' with type 'string' becomes 'cars[].things' with type 'string[][]' and 'cars' with type
|
||||
* 'object[]'
|
||||
*
|
||||
* @param array[] $bodyParametersFromValidationRules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function normaliseArrayAndObjectParameters(array $bodyParametersFromValidationRules): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($bodyParametersFromValidationRules as $name => $details) {
|
||||
if (isset($results[$name])) {
|
||||
continue;
|
||||
}
|
||||
if ($details['type'] === 'array') {
|
||||
// Generic array type. If a child item exists,
|
||||
// this will be overwritten with the correct type (such as object or object[]) by the code below
|
||||
$details['type'] = 'string[]';
|
||||
}
|
||||
|
||||
if (Str::endsWith($name, '.*')) {
|
||||
// Wrap array example properly
|
||||
$needsWrapping = !is_array($details['example']);
|
||||
|
||||
$nestingLevel = 0;
|
||||
// Change cars.*.dogs.things.*.* with type X to cars.*.dogs.things with type X[][]
|
||||
while (Str::endsWith($name, '.*')) {
|
||||
$details['type'] .= '[]';
|
||||
if ($needsWrapping) {
|
||||
// Make it two items in each array
|
||||
$secondItem = $secondValue = $details['setter']();
|
||||
for ($i = 0; $i < $nestingLevel; $i++) {
|
||||
$secondItem = [$secondValue];
|
||||
}
|
||||
$details['example'] = [$details['example'], $secondItem];
|
||||
}
|
||||
$name = substr($name, 0, -2);
|
||||
$nestingLevel++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now make sure the field cars.*.dogs exists
|
||||
$parentPath = $name;
|
||||
while (Str::contains($parentPath, '.')) {
|
||||
$parentPath = preg_replace('/\.[^.]+$/', '', $parentPath);
|
||||
if (empty($bodyParametersFromValidationRules[$parentPath])) {
|
||||
if (Str::endsWith($parentPath, '.*')) {
|
||||
$parentPath = substr($parentPath, 0, -2);
|
||||
$type = 'object[]';
|
||||
$example = [[]];
|
||||
} else {
|
||||
$type = 'object';
|
||||
$example = [];
|
||||
}
|
||||
$normalisedPath = str_replace('.*.', '[].', $parentPath);
|
||||
$results[$normalisedPath] = [
|
||||
'name' => $normalisedPath,
|
||||
'type' => $type,
|
||||
'required' => false,
|
||||
'description' => '',
|
||||
'example' => $example,
|
||||
];
|
||||
} else {
|
||||
// if the parent field already exists with a type 'array'
|
||||
$parentDetails = $bodyParametersFromValidationRules[$parentPath];
|
||||
unset($bodyParametersFromValidationRules[$parentPath]);
|
||||
if (Str::endsWith($parentPath, '.*')) {
|
||||
$parentPath = substr($parentPath, 0, -2);
|
||||
$parentDetails['type'] = 'object[]';
|
||||
// Set the example too. Very likely the example array was an array of strings or an empty array
|
||||
if (empty($parentDetails['example']) || is_string($parentDetails['example'][0]) || is_string($parentDetails['example'][0][0])) {
|
||||
$parentDetails['example'] = [[]];
|
||||
}
|
||||
} else {
|
||||
$parentDetails['type'] = 'object';
|
||||
if (empty($parentDetails['example']) || is_string($parentDetails['example'][0])) {
|
||||
$parentDetails['example'] = [];
|
||||
}
|
||||
}
|
||||
$normalisedPath = str_replace('.*.', '[].', $parentPath);
|
||||
$parentDetails['name'] = $normalisedPath;
|
||||
$results[$normalisedPath] = $parentDetails;
|
||||
}
|
||||
}
|
||||
|
||||
$details['name'] = $name = str_replace('.*.', '[].', $name);
|
||||
unset($details['setter']);
|
||||
|
||||
// Change type 'array' to 'object' if there are subfields
|
||||
if (
|
||||
$details['type'] === 'array'
|
||||
&& Arr::first(array_keys($bodyParametersFromValidationRules), function ($key) use ($name) {
|
||||
return preg_match("/{$name}\\.[^*]/", $key);
|
||||
})
|
||||
) {
|
||||
$details['type'] = 'object';
|
||||
}
|
||||
$results[$name] = $details;
|
||||
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function getDescription(string $rule, array $arguments = [], $baseType = 'string'): string
|
||||
{
|
||||
$description = trans("validation.{$rule}");
|
||||
// For rules that can apply to multiple types (eg 'max' rule), Laravel returns an array of possible messages
|
||||
// 'numeric' => 'The :attribute must not be greater than :max'
|
||||
// 'file' => 'The :attribute must have a size less than :max kilobytes'
|
||||
if (is_array($description)) {
|
||||
$description = $description[$baseType];
|
||||
}
|
||||
|
||||
// Convert messages from failure type ("The value is not a valid date.") to info ("The value must be a valid date.")
|
||||
$description = str_replace(['is not', 'does not'], ['must be', 'must'], $description);
|
||||
|
||||
foreach ($arguments as $placeholder => $argument) {
|
||||
$description = str_replace($placeholder, $argument, $description);
|
||||
}
|
||||
|
||||
return str_replace(":attribute", "value", $description);
|
||||
}
|
||||
|
||||
protected function getMissingCustomDataMessage($parameterName)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
protected function shouldCastUserExample()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,6 @@ use ReflectionUnionType;
|
||||
|
||||
class GetFromBodyParamTag extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'bodyParameters';
|
||||
|
||||
use ParamHelpers;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
|
||||
@@ -5,15 +5,9 @@ namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Dingo\Api\Http\FormRequest as DingoFormRequest;
|
||||
use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\ParsesValidationRules;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Extracting\ValidationRuleDescriptionParser as d;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\WritingUtils as w;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionFunctionAbstract;
|
||||
@@ -22,12 +16,7 @@ use ReflectionUnionType;
|
||||
|
||||
class GetFromFormRequest extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'bodyParameters';
|
||||
|
||||
public static $MISSING_VALUE;
|
||||
|
||||
use ParamHelpers;
|
||||
use ParsesValidationRules;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
|
||||
{
|
||||
@@ -115,417 +104,10 @@ class GetFromFormRequest extends Strategy
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getBodyParametersFromValidationRules(array $validationRules, array $customParameterData = [])
|
||||
protected function getMissingCustomDataMessage($parameterName)
|
||||
{
|
||||
self::$MISSING_VALUE = new \stdClass();
|
||||
$rules = $this->normaliseRules($validationRules);
|
||||
|
||||
$parameters = [];
|
||||
foreach ($rules as $parameter => $ruleset) {
|
||||
if (count($customParameterData) && !isset($customParameterData[$parameter])) {
|
||||
c::debug("No data found for parameter '$parameter' from your bodyParameters() method. Add an entry for '$parameter' so you can add description and example.");
|
||||
}
|
||||
$userSpecifiedParameterInfo = $customParameterData[$parameter] ?? [];
|
||||
|
||||
$parameterData = [
|
||||
'name' => $parameter,
|
||||
'required' => false,
|
||||
'type' => null,
|
||||
'example' => self::$MISSING_VALUE,
|
||||
'description' => '',
|
||||
];
|
||||
|
||||
// Make sure the user-specified example overwrites others.
|
||||
if (isset($userSpecifiedParameterInfo['example'])) {
|
||||
$parameterData['example'] = $userSpecifiedParameterInfo['example'];
|
||||
}
|
||||
|
||||
foreach ($ruleset as $rule) {
|
||||
$this->parseRule($rule, $parameterData);
|
||||
}
|
||||
|
||||
// Set autogenerated examples if none was supplied.
|
||||
// Each rule returns a 'setter' function, so we can lazily evaluate the last one only if we need it.
|
||||
if ($parameterData['example'] === self::$MISSING_VALUE && isset($parameterData['setter'])) {
|
||||
$parameterData['example'] = $parameterData['setter']();
|
||||
}
|
||||
|
||||
// Make sure the user-specified description comes first.
|
||||
$userSpecifiedDescription = $userSpecifiedParameterInfo['description'] ?? '';
|
||||
$validationDescription = trim($parameterData['description'] ?: '');
|
||||
$fullDescription = trim($userSpecifiedDescription . ' ' . trim($validationDescription));
|
||||
// Let's have our sentences end with full stops, like civilized people.🙂
|
||||
$parameterData['description'] = $fullDescription ? rtrim($fullDescription, '.') . '.' : $fullDescription;
|
||||
|
||||
// Set default values for type
|
||||
if (is_null($parameterData['type'])) {
|
||||
$parameterData['type'] = 'string';
|
||||
}
|
||||
// Set values when parameter is required and has no value
|
||||
if ($parameterData['required'] === true && $parameterData['example'] === self::$MISSING_VALUE) {
|
||||
$parameterData['example'] = $this->generateDummyValue($parameterData['type']);
|
||||
}
|
||||
|
||||
if (!is_null($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE) {
|
||||
// The cast is important since values may have been cast to string in the validator
|
||||
$parameterData['example'] = $this->castToType($parameterData['example'], $parameterData['type']);
|
||||
}
|
||||
|
||||
$parameterData['name'] = $parameter;
|
||||
$parameters[$parameter] = $parameterData;
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
return "No data found for parameter '$parameterName' in your bodyParameters() method. Add an entry for '$parameterName' so you can add a description and example.";
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will transform validation rules from:
|
||||
* 'param1' => 'int|required' TO 'param1' => ['int', 'required']
|
||||
*
|
||||
* @param array<string,string|string[]> $rules
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function normaliseRules(array $rules)
|
||||
{
|
||||
// We can simply call Validator::make($data, $rules)->getRules() to get the normalised rules,
|
||||
// but Laravel will ignore any nested array rules (`ids.*')
|
||||
// unless the key referenced (`ids`) exists in the dataset and is a non-empty array
|
||||
// So we'll create a single-item array for each array parameter
|
||||
$testData = [];
|
||||
foreach ($rules as $key => $ruleset) {
|
||||
if (!Str::contains($key, '.*')) continue;
|
||||
|
||||
// All we need is for Laravel to see this key exists
|
||||
Arr::set($testData, str_replace('.*', '.0', $key), Str::random());
|
||||
}
|
||||
|
||||
// Now this will return the complete ruleset.
|
||||
// Nested array parameters will be present, with '*' replaced by '0'
|
||||
$newRules = Validator::make($testData, $rules)->getRules();
|
||||
|
||||
// Transform the key names back from 'ids.0' to 'ids.*'
|
||||
return collect($newRules)->mapWithKeys(function ($val, $paramName) use ($rules) {
|
||||
if (Str::contains($paramName, '.0')) {
|
||||
$genericArrayKeyName = str_replace('.0', '.*', $paramName);
|
||||
|
||||
// But only if that was the original value
|
||||
if (isset($rules[$genericArrayKeyName])) {
|
||||
$paramName = $genericArrayKeyName;
|
||||
}
|
||||
}
|
||||
|
||||
return [$paramName => $val];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function parseRule($rule, &$parameterData)
|
||||
{
|
||||
$parsedRule = $this->parseStringRuleIntoRuleAndArguments($rule);
|
||||
[$rule, $arguments] = $parsedRule;
|
||||
|
||||
// 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;
|
||||
break;
|
||||
|
||||
/*
|
||||
* Primitive types. No description should be added
|
||||
*/
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
$parameterData['setter'] = function () {
|
||||
return Arr::random([true, false]);
|
||||
};
|
||||
$parameterData['type'] = 'boolean';
|
||||
break;
|
||||
case 'string':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('string');
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
break;
|
||||
case 'int':
|
||||
case 'integer':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('integer');
|
||||
};
|
||||
$parameterData['type'] = 'integer';
|
||||
break;
|
||||
case 'numeric':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('number');
|
||||
};
|
||||
$parameterData['type'] = 'number';
|
||||
break;
|
||||
case 'array':
|
||||
$parameterData['setter'] = function () {
|
||||
return [$this->generateDummyValue('string')];
|
||||
};
|
||||
$parameterData['type'] = 'array'; // The cleanup code in normaliseArrayAndObjectParameters() will set this to a valid type (x[] or object)
|
||||
break;
|
||||
case 'file':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= 'The value must be a file.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Special string types
|
||||
*/
|
||||
case 'timezone':
|
||||
// Laravel's message merely says "The value must be a valid zone"
|
||||
$parameterData['description'] .= "The value must be a valid time zone, such as <code>Africa/Accra</code>. ";
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->timezone;
|
||||
};
|
||||
break;
|
||||
case 'email':
|
||||
$parameterData['description'] .= d::getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->safeEmail;
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
break;
|
||||
case 'url':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->url;
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel's message is "The value format is invalid". Ugh.🤮
|
||||
$parameterData['description'] .= "The value must be a valid URL. ";
|
||||
break;
|
||||
case 'ip':
|
||||
$parameterData['description'] .= d::getDescription($rule) . ' ';
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->ipv4;
|
||||
};
|
||||
break;
|
||||
case 'json':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= d::getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return json_encode([$this->getFaker()->word, $this->getFaker()->word,]);
|
||||
};
|
||||
break;
|
||||
case 'date':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= d::getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
return date(\DateTime::ISO8601, time());
|
||||
};
|
||||
break;
|
||||
case 'date_format':
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel description here is "The value must match the format Y-m-d". Not descriptive enough.
|
||||
$parameterData['description'] .= "The value must be a valid date in the format {$arguments[0]} ";
|
||||
$parameterData['setter'] = function () use ($arguments) {
|
||||
return date($arguments[0], time());
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Special number types. Some rules here may apply to other types, but we treat them as being numeric.
|
||||
*//*
|
||||
* min, max and between not supported until we can figure out a proper way
|
||||
* to make them compatible with multiple types (string, number, file)
|
||||
case 'min':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':min' => $arguments[0]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween($arguments[0]); };
|
||||
break;
|
||||
case 'max':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':max' => $arguments[0]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween(0, $arguments[0]); };
|
||||
break;
|
||||
case 'between':
|
||||
$parameterData['type'] = $parameterData['type'] ?: 'number';
|
||||
$parameterData['description'] .= Description::getDescription($rule, [':min' => $arguments[0], ':max' => $arguments[1]], 'numeric').' ';
|
||||
$parameterData['setter'] = function () { return $this->getFaker()->numberBetween($arguments[0], $arguments[1]); };
|
||||
break;*/
|
||||
|
||||
/**
|
||||
* Special file types.
|
||||
*/
|
||||
case 'image':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= d::getDescription($rule) . ' ';
|
||||
$parameterData['setter'] = function () {
|
||||
// This is fine because the file example generator generates an image
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Other rules.
|
||||
*/
|
||||
case 'in':
|
||||
// Not using the rule description here because it only says "The attribute is invalid"
|
||||
$description = 'The value must be one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments);
|
||||
$parameterData['description'] .= $description . ' ';
|
||||
$parameterData['setter'] = function () use ($arguments) {
|
||||
return Arr::random($arguments);
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
// Other rules not supported
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string rule into the base rule and arguments.
|
||||
* Laravel validation rules are specified in the format {rule}:{arguments}
|
||||
* Arguments are separated by commas.
|
||||
* For instance the rule "max:3" states that the value may only be three letters.
|
||||
*
|
||||
* @param string|Rule $rule
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseStringRuleIntoRuleAndArguments($rule)
|
||||
{
|
||||
$ruleArguments = [];
|
||||
|
||||
// Convert any 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);
|
||||
|
||||
// These rules can have ommas in their arguments, so we don't split on commas
|
||||
if (in_array(strtolower($rule), ['regex', 'date', 'date_format'])) {
|
||||
$ruleArguments = [$argumentsString];
|
||||
} else {
|
||||
$ruleArguments = str_getcsv($argumentsString);
|
||||
}
|
||||
}
|
||||
|
||||
return [strtolower(trim($rule)), $ruleArguments];
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel uses .* notation for arrays. This PR aims to normalise that into our "new syntax".
|
||||
*
|
||||
* 'years.*' with type 'integer' becomes 'years' with type 'integer[]'
|
||||
* 'cars.*.age' with type 'string' becomes 'cars[].age' with type 'string' and 'cars' with type 'object[]'
|
||||
* 'cars.*.things.*.*' with type 'string' becomes 'cars[].things' with type 'string[][]' and 'cars' with type
|
||||
* 'object[]'
|
||||
*
|
||||
* @param array[] $bodyParametersFromValidationRules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function normaliseArrayAndObjectParameters(array $bodyParametersFromValidationRules): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($bodyParametersFromValidationRules as $name => $details) {
|
||||
|
||||
if (isset($results[$name])) {
|
||||
continue;
|
||||
}
|
||||
if ($details['type'] === 'array') {
|
||||
// Generic array type. If a child item exists,
|
||||
// this will be overwritten with the correct type (such as object or object[]) by the code below
|
||||
$details['type'] = 'string[]';
|
||||
}
|
||||
|
||||
if (Str::endsWith($name, '.*')) {
|
||||
// Wrap array example properly
|
||||
$needsWrapping = !is_array($details['example']);
|
||||
|
||||
$nestingLevel = 0;
|
||||
// Change cars.*.dogs.things.*.* with type X to cars.*.dogs.things with type X[][]
|
||||
while (Str::endsWith($name, '.*')) {
|
||||
$details['type'] .= '[]';
|
||||
if ($needsWrapping) {
|
||||
// Make it two items in each array
|
||||
$secondItem = $secondValue = $details['setter']();
|
||||
for ($i = 0; $i < $nestingLevel; $i++) {
|
||||
$secondItem = [$secondValue];
|
||||
}
|
||||
$details['example'] = [$details['example'], $secondItem];
|
||||
}
|
||||
$name = substr($name, 0, -2);
|
||||
$nestingLevel++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now make sure the field cars.*.dogs exists
|
||||
$parentPath = $name;
|
||||
while (Str::contains($parentPath, '.')) {
|
||||
$parentPath = preg_replace('/\.[^.]+$/', '', $parentPath);
|
||||
if (empty($bodyParametersFromValidationRules[$parentPath])) {
|
||||
if (Str::endsWith($parentPath, '.*')) {
|
||||
$parentPath = substr($parentPath, 0, -2);
|
||||
$type = 'object[]';
|
||||
$example = [[]];
|
||||
} else {
|
||||
$type = 'object';
|
||||
$example = [];
|
||||
}
|
||||
$normalisedPath = str_replace('.*.', '[].', $parentPath);
|
||||
$results[$normalisedPath] = [
|
||||
'name' => $normalisedPath,
|
||||
'type' => $type,
|
||||
'required' => false,
|
||||
'description' => '',
|
||||
'example' => $example,
|
||||
];
|
||||
} else {
|
||||
// if the parent field already exists with a type 'array'
|
||||
$parentDetails = $bodyParametersFromValidationRules[$parentPath];
|
||||
unset($bodyParametersFromValidationRules[$parentPath]);
|
||||
if (Str::endsWith($parentPath, '.*')) {
|
||||
$parentPath = substr($parentPath, 0, -2);
|
||||
$parentDetails['type'] = 'object[]';
|
||||
// Set the example too. Very likely the example array was an array of strings or an empty array
|
||||
if (empty($parentDetails['example']) || is_string($parentDetails['example'][0]) || is_string($parentDetails['example'][0][0])) {
|
||||
$parentDetails['example'] = [[]];
|
||||
}
|
||||
} else {
|
||||
$parentDetails['type'] = 'object';
|
||||
if (empty($parentDetails['example']) || is_string($parentDetails['example'][0])) {
|
||||
$parentDetails['example'] = [];
|
||||
}
|
||||
}
|
||||
$normalisedPath = str_replace('.*.', '[].', $parentPath);
|
||||
$parentDetails['name'] = $normalisedPath;
|
||||
$results[$normalisedPath] = $parentDetails;
|
||||
}
|
||||
}
|
||||
|
||||
$details['name'] = $name = str_replace('.*.', '[].', $name);
|
||||
unset($details['setter']);
|
||||
|
||||
// Change type 'array' to 'object' if there are subfields
|
||||
if (
|
||||
$details['type'] === 'array'
|
||||
&& Arr::first(array_keys($bodyParametersFromValidationRules), function ($key) use ($name) {
|
||||
return preg_match("/{$name}\\.[^*]/", $key);
|
||||
})
|
||||
) {
|
||||
$details['type'] = 'object';
|
||||
}
|
||||
$results[$name] = $details;
|
||||
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\MethodAstParser;
|
||||
use Knuckles\Scribe\Extracting\ParsesValidationRules;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
|
||||
class GetFromInlineValidator extends Strategy
|
||||
{
|
||||
use ParsesValidationRules;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
|
||||
{
|
||||
$methodAst = MethodAstParser::getMethodAst($endpointData->method);
|
||||
[$validationRules, $customParameterData] = $this->lookForInlineValidationRules($methodAst);
|
||||
|
||||
$bodyParametersFromValidationRules = $this->getBodyParametersFromValidationRules($validationRules, $customParameterData);
|
||||
return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
|
||||
}
|
||||
|
||||
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;
|
||||
foreach ($statements as $index => $node) {
|
||||
// Filter to only assignment expressions
|
||||
if (!($node instanceof Node\Stmt\Expression) || !($node->expr instanceof Node\Expr\Assign)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assignment = $node->expr;
|
||||
$rvalue = $assignment->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 && end($rvalue->class->parts) == "Validator"
|
||||
&& $rvalue->name->name == "make"
|
||||
) {
|
||||
$validationRules = $rvalue->args[1]->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If validation rules were saved in a variable (like $rules),
|
||||
// find the var and expand the value
|
||||
if ($validationRules instanceof Node\Expr\Variable) {
|
||||
foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
|
||||
if (
|
||||
$earlierStatement instanceof Node\Stmt\Expression
|
||||
&& $earlierStatement->expr instanceof Node\Expr\Assign
|
||||
&& $earlierStatement->expr->var instanceof Node\Expr\Variable
|
||||
&& $earlierStatement->expr->var->name == $validationRules->name
|
||||
) {
|
||||
$validationRules = $earlierStatement->expr->expr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$validationRules instanceof Node\Expr\Array_) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
$rules = [];
|
||||
$customParameterData = [];
|
||||
foreach ($validationRules->items as $item) {
|
||||
$paramName = $item->key->value;
|
||||
|
||||
// Might be an expression or concatenated string, etc.
|
||||
// For now, let's focus on simple strings and arrays of strings
|
||||
if ($item->value instanceof Node\Scalar\String_) {
|
||||
$rules[$paramName] = $item->value->value;
|
||||
} else if ($item->value instanceof Node\Expr\Array_) {
|
||||
$rulesList = [];
|
||||
foreach ($item->value->items as $arrayItem) {
|
||||
/** @var Node\Expr\ArrayItem $arrayItem */
|
||||
if ($arrayItem->value instanceof Node\Scalar\String_) {
|
||||
$rulesList[] = $arrayItem->value->value;
|
||||
}
|
||||
|
||||
}
|
||||
$rules[$paramName] = join('|', $rulesList);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = $example = null;
|
||||
$comments = join("\n", array_map(
|
||||
fn($comment) => ltrim(ltrim($comment->getReformattedText(), "/")),
|
||||
$item->getComments()
|
||||
)
|
||||
);
|
||||
|
||||
if ($comments) {
|
||||
$description = trim(str_replace(['No-example.', 'No-example'], '', $comments));
|
||||
$example = null;
|
||||
if (preg_match('/(.*\s+|^)Example:\s*([\s\S]+)\s*/m', $description, $matches)) {
|
||||
$description = trim($matches[1]);
|
||||
$example = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
$customParameterData[$paramName] = compact('description', 'example');
|
||||
}
|
||||
|
||||
return [$rules, $customParameterData];
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
|
||||
protected function shouldCastUserExample()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@ class GetFromHeaderTag extends Strategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
/** @var string */
|
||||
public $stage = 'headers';
|
||||
public string $stage = 'headers';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
|
||||
{
|
||||
foreach ($endpointData->method->getParameters() as $param) {
|
||||
$paramType = $param->getType();
|
||||
|
||||
@@ -7,10 +7,9 @@ use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
|
||||
class GetFromRouteRules extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'headers';
|
||||
public string $stage = 'headers';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
|
||||
{
|
||||
return $routeRules['headers'] ?? [];
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ use Mpociot\Reflection\DocBlock;
|
||||
|
||||
class GetFromDocBlocks extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'metadata';
|
||||
public string $stage = 'metadata';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
$methodDocBlock = $docBlocks['method'];
|
||||
|
||||
@@ -18,8 +18,7 @@ use ReflectionUnionType;
|
||||
|
||||
class GetFromQueryParamTag extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'queryParameters';
|
||||
public string $stage = 'queryParameters';
|
||||
|
||||
use ParamHelpers;
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
class GetFromResponseFieldTag extends Strategy
|
||||
{
|
||||
/** @var string */
|
||||
public $stage = 'responseFields';
|
||||
public string $stage = 'responseFields';
|
||||
|
||||
use ParamHelpers;
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ use Mpociot\Reflection\DocBlock\Tag;
|
||||
*/
|
||||
class UseApiResourceTags extends Strategy
|
||||
{
|
||||
public string $stage = 'responses';
|
||||
|
||||
use DatabaseTransactionHelpers;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
|
||||
@@ -14,6 +14,8 @@ use Mpociot\Reflection\DocBlock\Tag;
|
||||
*/
|
||||
class UseResponseFileTag extends Strategy
|
||||
{
|
||||
public string $stage = 'responses';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
|
||||
@@ -13,6 +13,8 @@ use Mpociot\Reflection\DocBlock\Tag;
|
||||
*/
|
||||
class UseResponseTag extends Strategy
|
||||
{
|
||||
public string $stage = 'responses';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
|
||||
@@ -26,6 +26,8 @@ use ReflectionFunctionAbstract;
|
||||
*/
|
||||
class UseTransformerTags extends Strategy
|
||||
{
|
||||
public string $stage = 'responses';
|
||||
|
||||
use DatabaseTransactionHelpers;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
|
||||
@@ -9,15 +9,13 @@ abstract class Strategy
|
||||
{
|
||||
/**
|
||||
* The Scribe config
|
||||
* @var \Knuckles\Scribe\Tools\DocumentationConfig
|
||||
*/
|
||||
protected $config;
|
||||
protected DocumentationConfig $config;
|
||||
|
||||
/**
|
||||
* The current stage of route processing
|
||||
*/
|
||||
/** @var string */
|
||||
public $stage ;
|
||||
public string $stage;
|
||||
|
||||
public function __construct(DocumentationConfig $config)
|
||||
{
|
||||
|
||||
@@ -12,8 +12,7 @@ class GetFromLaravelAPI extends Strategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
/** @var string */
|
||||
public $stage = 'urlParameters';
|
||||
public string $stage = 'urlParameters';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
{
|
||||
|
||||
@@ -13,8 +13,7 @@ class GetFromLumenAPI extends Strategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
/** @var string */
|
||||
public $stage = 'urlParameters';
|
||||
public string $stage = 'urlParameters';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
{
|
||||
|
||||
@@ -19,8 +19,7 @@ class GetFromUrlParamTag extends Strategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
/** @var string */
|
||||
public $stage = 'urlParameters';
|
||||
public string $stage = 'urlParameters';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)
|
||||
{
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
class ValidationRuleDescriptionParser
|
||||
{
|
||||
private $rule;
|
||||
|
||||
private $arguments = [];
|
||||
|
||||
/**
|
||||
* @param string $rule
|
||||
*/
|
||||
public function __construct(string $rule = null)
|
||||
{
|
||||
$this->rule = $rule;
|
||||
}
|
||||
|
||||
public static function getDescription(string $rule, array $arguments = [], $type = 'string'): string
|
||||
{
|
||||
$instance = new self($rule);
|
||||
|
||||
$instance->arguments = $arguments;
|
||||
|
||||
return $instance->makeDescription($type);
|
||||
}
|
||||
|
||||
protected function makeDescription($baseType = 'string'): string
|
||||
{
|
||||
$description = trans("validation.{$this->rule}");
|
||||
// For rules that can apply to multiple types (eg 'max' rule), Laravel returns an array of possible messages
|
||||
// 'numeric' => 'The :attribute must not be greater than :max'
|
||||
// 'file' => 'The :attribute must have a size less than :max kilobytes'
|
||||
if (is_array($description)) {
|
||||
$description = $description[$baseType];
|
||||
}
|
||||
|
||||
// Convert messages from failure type ("The value is not a valid date.") to info ("The value must be a valid date.")
|
||||
$description = str_replace(['is not', 'does not'], ['must be', 'must'], $description);
|
||||
|
||||
return $this->replaceArguments($description);
|
||||
}
|
||||
|
||||
protected function replaceArguments(string $description): string
|
||||
{
|
||||
foreach ($this->arguments as $placeholder => $argument) {
|
||||
$description = str_replace($placeholder, $argument, $description);
|
||||
}
|
||||
|
||||
$description = str_replace(":attribute", "value", $description);
|
||||
|
||||
return $description;
|
||||
}
|
||||
}
|
||||
@@ -409,4 +409,59 @@ class TestController extends Controller
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function withInlineRequestValidate(Request $request)
|
||||
{
|
||||
// Some stuff
|
||||
$validated = $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 withInlineValidatorMake(Request $request)
|
||||
{
|
||||
// Some stuff
|
||||
$validator = Validator::make($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
|
||||
if ($validator->fails()) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ class GetFromFormRequestTest extends BaseLaravelTest
|
||||
/** @test */
|
||||
public function can_fetch_from_form_request()
|
||||
{
|
||||
$methodName = 'withFormRequestParameter';
|
||||
$method = new \ReflectionMethod(TestController::class, $methodName);
|
||||
$method = new \ReflectionMethod(TestController::class, 'withFormRequestParameter');
|
||||
|
||||
$strategy = new GetFromFormRequest(new DocumentationConfig([]));
|
||||
$results = $strategy->getBodyParametersFromFormRequest($method);
|
||||
@@ -100,202 +99,4 @@ class GetFromFormRequestTest extends BaseLaravelTest
|
||||
|
||||
$this->assertIsArray($results['ids']['example']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider supportedRules
|
||||
*/
|
||||
public function can_handle_specific_rules($ruleset, $customInfo, $expected)
|
||||
{
|
||||
$strategy = new GetFromFormRequest(new DocumentationConfig([]));
|
||||
$results = $strategy->getBodyParametersFromValidationRules($ruleset, $customInfo);
|
||||
|
||||
$parameterName = array_keys($ruleset)[0];
|
||||
|
||||
if (isset($expected['required'])) {
|
||||
$this->assertEquals($expected['required'], $results[$parameterName]['required']);
|
||||
}
|
||||
|
||||
if (!empty($expected['type'])) {
|
||||
$this->assertEquals($expected['type'], $results[$parameterName]['type']);
|
||||
}
|
||||
|
||||
if (!empty($expected['description'])) {
|
||||
$this->assertStringEndsWith($expected['description'], $results[$parameterName]['description']);
|
||||
}
|
||||
|
||||
// Validate that the generated values actually pass
|
||||
$validator = Validator::make([$parameterName => $results[$parameterName]['example']], $ruleset);
|
||||
try {
|
||||
$validator->validate();
|
||||
} catch (ValidationException $e) {
|
||||
dump('Value: ', $results[$parameterName]['example']);
|
||||
dump($e->errors());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_transform_arrays_and_objects()
|
||||
{
|
||||
$strategy = new GetFromFormRequest(new DocumentationConfig([]));
|
||||
$ruleset = [
|
||||
'array_param' => 'array|required',
|
||||
'array_param.*' => 'string',
|
||||
];
|
||||
$results = $strategy->normaliseArrayAndObjectParameters($strategy->getBodyParametersFromValidationRules($ruleset));
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertEquals('string[]', $results['array_param']['type']);
|
||||
|
||||
$ruleset = [
|
||||
'object_param' => 'array|required',
|
||||
'object_param.field1.*' => 'string',
|
||||
'object_param.field2' => 'integer|required',
|
||||
];
|
||||
$results = $strategy->normaliseArrayAndObjectParameters($strategy->getBodyParametersFromValidationRules($ruleset));
|
||||
$this->assertCount(3, $results);
|
||||
$this->assertEquals('object', $results['object_param']['type']);
|
||||
$this->assertEquals('string[]', $results['object_param.field1']['type']);
|
||||
$this->assertEquals('integer', $results['object_param.field2']['type']);
|
||||
|
||||
$ruleset = [
|
||||
'array_of_objects_with_array.*.another.*.one.field1.*' => 'string|required',
|
||||
'array_of_objects_with_array.*.another.*.one.field2' => 'integer',
|
||||
'array_of_objects_with_array.*.another.*.two.field2' => 'numeric',
|
||||
];
|
||||
$results = $strategy->normaliseArrayAndObjectParameters($strategy->getBodyParametersFromValidationRules($ruleset));
|
||||
$this->assertCount(7, $results);
|
||||
$this->assertEquals('object[]', $results['array_of_objects_with_array']['type']);
|
||||
$this->assertEquals('object[]', $results['array_of_objects_with_array[].another']['type']);
|
||||
$this->assertEquals('object', $results['array_of_objects_with_array[].another[].one']['type']);
|
||||
$this->assertEquals('object', $results['array_of_objects_with_array[].another[].two']['type']);
|
||||
$this->assertEquals('string[]', $results['array_of_objects_with_array[].another[].one.field1']['type']);
|
||||
$this->assertEquals('integer', $results['array_of_objects_with_array[].another[].one.field2']['type']);
|
||||
$this->assertEquals('number', $results['array_of_objects_with_array[].another[].two.field2']['type']);
|
||||
}
|
||||
|
||||
public function supportedRules()
|
||||
{
|
||||
$description = 'A description';
|
||||
// Key is just an identifier
|
||||
// First array in each key is the validation ruleset,
|
||||
// Second is custom information from bodyParameters()
|
||||
// Third is expected result
|
||||
return [
|
||||
'required' => [
|
||||
['required_param' => 'required'],
|
||||
['required_param' => ['description' => $description]],
|
||||
[
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'string' => [
|
||||
['string_param' => 'string|required'],
|
||||
['string_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'boolean' => [
|
||||
['boolean_param' => 'boolean|required'],
|
||||
['boolean_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'boolean',
|
||||
],
|
||||
],
|
||||
'integer' => [
|
||||
['integer_param' => 'integer|required'],
|
||||
['integer_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'integer',
|
||||
],
|
||||
],
|
||||
'numeric' => [
|
||||
['numeric_param' => 'numeric|required'],
|
||||
['numeric_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'number',
|
||||
],
|
||||
],
|
||||
'array' => [
|
||||
['array_param' => 'array|required'],
|
||||
['array_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'array',
|
||||
],
|
||||
],
|
||||
'file' => [
|
||||
['file_param' => 'file|required'],
|
||||
['file_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a file.',
|
||||
'type' => 'file',
|
||||
],
|
||||
],
|
||||
'timezone' => [
|
||||
['timezone_param' => 'timezone|required'],
|
||||
['timezone_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid time zone, such as <code>Africa/Accra</code>.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'email' => [
|
||||
['email_param' => 'email|required'],
|
||||
['email_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid email address.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'url' => [
|
||||
['url_param' => 'url|required'],
|
||||
['url_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid URL.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'ip' => [
|
||||
['ip_param' => 'ip|required'],
|
||||
['ip_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid IP address.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'json' => [
|
||||
['json_param' => 'json|required'],
|
||||
['json_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid JSON string.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'date' => [
|
||||
['date_param' => 'date|required'],
|
||||
['date_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid date.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'date_format' => [
|
||||
['date_format_param' => 'date_format:Y-m-d|required'],
|
||||
['date_format_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be a valid date in the format Y-m-d.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'in' => [
|
||||
['in_param' => 'in:3,5,6|required'],
|
||||
['in_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => 'The value must be one of <code>3</code>, <code>5</code>, or <code>6</code>.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
123
tests/Strategies/BodyParameters/GetFromInlineValidatorTest.php
Normal file
123
tests/Strategies/BodyParameters/GetFromInlineValidatorTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tests\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\Strategies\BodyParameters\GetFromInlineValidator;
|
||||
use Knuckles\Scribe\Tests\BaseLaravelTest;
|
||||
use Knuckles\Scribe\Tests\Fixtures\TestController;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||
|
||||
class GetFromInlineValidatorTest extends BaseLaravelTest
|
||||
{
|
||||
use ArraySubsetAsserts;
|
||||
|
||||
private static $expected = [
|
||||
'user_id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'description' => 'The id of the user.',
|
||||
'example' => 9,
|
||||
],
|
||||
'room_id' => [
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
'description' => 'The id of the room. The value must be one of <code>3</code>, <code>5</code>, or <code>6</code>.',
|
||||
],
|
||||
'forever' => [
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'description' => 'Whether to ban the user forever.',
|
||||
'example' => false,
|
||||
],
|
||||
'another_one' => [
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'description' => 'Just need something here.',
|
||||
],
|
||||
'even_more_param' => [
|
||||
'type' => 'string[]',
|
||||
'required' => false,
|
||||
'description' => '',
|
||||
],
|
||||
'book' => [
|
||||
'type' => 'object',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
'example' => [],
|
||||
],
|
||||
'book.name' => [
|
||||
'type' => 'string',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
],
|
||||
'book.author_id' => [
|
||||
'type' => 'integer',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
],
|
||||
'book.pages_count' => [
|
||||
'type' => 'integer',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
],
|
||||
'ids' => [
|
||||
'type' => 'integer[]',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
],
|
||||
'users' => [
|
||||
'type' => 'object[]',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
'example' => [[]],
|
||||
],
|
||||
'users[].first_name' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The first name of the user.',
|
||||
'required' => false,
|
||||
'example' => 'John',
|
||||
],
|
||||
'users[].last_name' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The last name of the user.',
|
||||
'required' => false,
|
||||
'example' => 'Doe',
|
||||
],
|
||||
];
|
||||
|
||||
/** @test */
|
||||
public function can_fetch_from_request_validate()
|
||||
{
|
||||
$endpoint = new class extends ExtractedEndpointData {
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
$this->method = new \ReflectionMethod(TestController::class, 'withInlineRequestValidate');
|
||||
}
|
||||
};
|
||||
|
||||
$strategy = new GetFromInlineValidator(new DocumentationConfig([]));
|
||||
$results = $strategy($endpoint, []);
|
||||
|
||||
$this->assertArraySubset(self::$expected, $results);
|
||||
$this->assertIsArray($results['ids']['example']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function can_fetch_from_validator_make()
|
||||
{
|
||||
$endpoint = new class extends ExtractedEndpointData {
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
$this->method = new \ReflectionMethod(TestController::class, 'withInlineValidatorMake');
|
||||
}
|
||||
};
|
||||
|
||||
$strategy = new GetFromInlineValidator(new DocumentationConfig([]));
|
||||
$results = $strategy($endpoint, []);
|
||||
|
||||
$this->assertArraySubset(self::$expected, $results);
|
||||
$this->assertIsArray($results['ids']['example']);
|
||||
}
|
||||
}
|
||||
202
tests/Unit/ValidationRuleParsingTest.php
Normal file
202
tests/Unit/ValidationRuleParsingTest.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tests\Unit;
|
||||
|
||||
use Knuckles\Scribe\Extracting\ParsesValidationRules;
|
||||
use Knuckles\Scribe\Tests\BaseLaravelTest;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
|
||||
class ValidationRuleParsingTest extends BaseLaravelTest
|
||||
{
|
||||
private $strategy;
|
||||
|
||||
public function __construct(?string $name = null, array $data = [], $dataName = '')
|
||||
{
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$this->strategy = new class {
|
||||
use ParsesValidationRules;
|
||||
|
||||
public function parse($validationRules, $customParameterData = []): array
|
||||
{
|
||||
$this->config = new DocumentationConfig([]);
|
||||
$bodyParametersFromValidationRules = $this->getBodyParametersFromValidationRules($validationRules, $customParameterData);
|
||||
return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider supportedRules
|
||||
*/
|
||||
public function can_parse_supported_rules(array $ruleset, array $customInfo, array $expected)
|
||||
{
|
||||
$results = $this->strategy->parse($ruleset, $customInfo);
|
||||
|
||||
$parameterName = array_keys($ruleset)[0];
|
||||
|
||||
$this->assertEquals($expected['type'], $results[$parameterName]['type']);
|
||||
$this->assertStringEndsWith($expected['description'], $results[$parameterName]['description']);
|
||||
}
|
||||
|
||||
|
||||
/** @test */
|
||||
public function can_transform_arrays_and_objects()
|
||||
{
|
||||
$ruleset = [
|
||||
'array_param' => 'array|required',
|
||||
'array_param.*' => 'string',
|
||||
];
|
||||
$results = $this->strategy->parse($ruleset);
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertEquals('string[]', $results['array_param']['type']);
|
||||
|
||||
$ruleset = [
|
||||
'object_param' => 'array|required',
|
||||
'object_param.field1.*' => 'string',
|
||||
'object_param.field2' => 'integer|required',
|
||||
];
|
||||
$results = $this->strategy->parse($ruleset);
|
||||
$this->assertCount(3, $results);
|
||||
$this->assertEquals('object', $results['object_param']['type']);
|
||||
$this->assertEquals('string[]', $results['object_param.field1']['type']);
|
||||
$this->assertEquals('integer', $results['object_param.field2']['type']);
|
||||
|
||||
$ruleset = [
|
||||
'array_of_objects_with_array.*.another.*.one.field1.*' => 'string|required',
|
||||
'array_of_objects_with_array.*.another.*.one.field2' => 'integer',
|
||||
'array_of_objects_with_array.*.another.*.two.field2' => 'numeric',
|
||||
];
|
||||
$results = $this->strategy->parse($ruleset);
|
||||
$this->assertCount(7, $results);
|
||||
$this->assertEquals('object[]', $results['array_of_objects_with_array']['type']);
|
||||
$this->assertEquals('object[]', $results['array_of_objects_with_array[].another']['type']);
|
||||
$this->assertEquals('object', $results['array_of_objects_with_array[].another[].one']['type']);
|
||||
$this->assertEquals('object', $results['array_of_objects_with_array[].another[].two']['type']);
|
||||
$this->assertEquals('string[]', $results['array_of_objects_with_array[].another[].one.field1']['type']);
|
||||
$this->assertEquals('integer', $results['array_of_objects_with_array[].another[].one.field2']['type']);
|
||||
$this->assertEquals('number', $results['array_of_objects_with_array[].another[].two.field2']['type']);
|
||||
}
|
||||
|
||||
public function supportedRules()
|
||||
{
|
||||
$description = 'A description';
|
||||
// Key is just an identifier
|
||||
// First array in each key is the validation ruleset,
|
||||
// Second is custom information (from bodyParameters() or comments)
|
||||
// Third is expected result
|
||||
|
||||
yield 'string' => [
|
||||
['string_param' => 'string'],
|
||||
['string_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'string',
|
||||
'description' => $description . ".",
|
||||
],
|
||||
];
|
||||
yield 'boolean' => [
|
||||
['boolean_param' => 'boolean'],
|
||||
[],
|
||||
[
|
||||
'type' => 'boolean',
|
||||
'description' => "",
|
||||
],
|
||||
];
|
||||
yield 'integer' => [
|
||||
['integer_param' => 'integer'],
|
||||
[],
|
||||
[
|
||||
'type' => 'integer',
|
||||
'description' => "",
|
||||
],
|
||||
];
|
||||
yield 'numeric' => [
|
||||
['numeric_param' => 'numeric'],
|
||||
['numeric_param' => ['description' => $description]],
|
||||
[
|
||||
'type' => 'number',
|
||||
'description' => $description . ".",
|
||||
],
|
||||
];
|
||||
yield 'array' => [
|
||||
['array_param' => 'array'],
|
||||
[],
|
||||
[
|
||||
'type' => 'string[]',
|
||||
'description' => '',
|
||||
],
|
||||
];
|
||||
yield 'file' => [
|
||||
['file_param' => 'file|required'],
|
||||
['file_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => "$description. The value must be a file.",
|
||||
'type' => 'file',
|
||||
],
|
||||
];
|
||||
yield 'timezone' => [
|
||||
['timezone_param' => 'timezone|required'],
|
||||
[],
|
||||
[
|
||||
'description' => 'The value must be a valid time zone, such as <code>Africa/Accra</code>.',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'email' => [
|
||||
['email_param' => 'email|required'],
|
||||
[],
|
||||
[
|
||||
'description' => 'The value must be a valid email address.',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'url' => [
|
||||
['url_param' => 'url|required'],
|
||||
['url_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => "$description. The value must be a valid URL.",
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'ip' => [
|
||||
['ip_param' => 'ip|required'],
|
||||
['ip_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => "$description. The value must be a valid IP address.",
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'json' => [
|
||||
['json_param' => 'json|required'],
|
||||
['json_param' => []],
|
||||
[
|
||||
'description' => 'The value must be a valid JSON string.',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'date' => [
|
||||
['date_param' => 'date|required'],
|
||||
[],
|
||||
[
|
||||
'description' => 'The value must be a valid date.',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'date_format' => [
|
||||
['date_format_param' => 'date_format:Y-m-d|required'],
|
||||
['date_format_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => "$description. The value must be a valid date in the format Y-m-d.",
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
yield 'in' => [
|
||||
['in_param' => 'in:3,5,6'],
|
||||
['in_param' => ['description' => $description]],
|
||||
[
|
||||
'description' => "$description. The value must be one of <code>3</code>, <code>5</code>, or <code>6</code>.",
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user