diff --git a/resources/views/docs.blade.php b/resources/views/docs.blade.php index 165460270..bdee78565 100644 --- a/resources/views/docs.blade.php +++ b/resources/views/docs.blade.php @@ -37,7 +37,7 @@ const csrfToken = getCookieValue(CSRF_TOKEN_COOKIE_KEY); if (csrfToken) { const { headers = new Headers() } = options || {}; - updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURI(csrfToken)); + updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURIComponent(csrfToken)); return originalFetch(url, { ...options, headers, diff --git a/src/ScrambleServiceProvider.php b/src/ScrambleServiceProvider.php index 40514fc59..61cde7328 100644 --- a/src/ScrambleServiceProvider.php +++ b/src/ScrambleServiceProvider.php @@ -41,11 +41,13 @@ use Dedoc\Scramble\Support\OperationExtensions\ResponseExtension; use Dedoc\Scramble\Support\ServerFactory; use Dedoc\Scramble\Support\TypeToSchemaExtensions\AnonymousResourceCollectionTypeToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\CursorPaginatorTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\EloquentCollectionToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\EnumToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\JsonResourceTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\LengthAwarePaginatorTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ModelToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\PaginatorTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResponseTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\VoidTypeToSchema; use PhpParser\ParserFactory; @@ -179,6 +181,8 @@ public function configurePackage(Package $package): void ModelToSchema::class, EloquentCollectionToSchema::class, AnonymousResourceCollectionTypeToSchema::class, + CursorPaginatorTypeToSchema::class, + PaginatorTypeToSchema::class, LengthAwarePaginatorTypeToSchema::class, ResponseTypeToSchema::class, VoidTypeToSchema::class, diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 7f454fe55..79d168394 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -158,8 +158,7 @@ public function transform(Type $type) $openApiType->examples($examples); } - if ($default = ExamplesExtractor::make($docNode, '@default') - ->extract(preferString: $openApiType instanceof StringType)) { + if ($default = ExamplesExtractor::make($docNode, '@default')->extract(preferString: $openApiType instanceof StringType)) { $openApiType->default($default[0]); } diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index 505c42990..6ff044c30 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -3,20 +3,22 @@ namespace Dedoc\Scramble\Support\OperationExtensions; use Dedoc\Scramble\Extensions\OperationExtension; +use Dedoc\Scramble\Support\Generator\Combined\AllOf; use Dedoc\Scramble\Support\Generator\Operation; use Dedoc\Scramble\Support\Generator\Parameter; use Dedoc\Scramble\Support\Generator\Reference; use Dedoc\Scramble\Support\Generator\RequestBodyObject; use Dedoc\Scramble\Support\Generator\Schema; -use Dedoc\Scramble\Support\Generator\Types\ObjectType; +use Dedoc\Scramble\Support\Generator\Types\Type; use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\FormRequestRulesExtractor; -use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\RulesToParameters; +use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ParametersExtractionResult; +use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\RequestMethodCallsExtractor; use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ValidateCallExtractor; use Dedoc\Scramble\Support\RouteInfo; use Illuminate\Routing\Route; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Str; -use PhpParser\Node\Stmt\ClassMethod; use Throwable; class RequestBodyExtension extends OperationExtension @@ -33,35 +35,26 @@ public function handle(Operation $operation, RouteInfo $routeInfo) */ $routeInfo->getMethodType(); - [$bodyParams, $schemaName, $schemaDescription] = [[], null, null]; + $rulesResults = collect(); + try { - [$bodyParams, $schemaName, $schemaDescription] = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode()); + $rulesResults = collect($this->extractRouteRequestValidationRules($routeInfo, $routeInfo->methodNode())); } catch (Throwable $exception) { if (app()->environment('testing')) { throw $exception; } - $description = $description->append('⚠️Cannot generate request documentation: '.$exception->getMessage()); + $description = $description->append('⚠️Cannot generate request documentation: ' . $exception->getMessage()); } $operation ->summary(Str::of($routeInfo->phpDoc()->getAttribute('summary'))->rtrim('.')) ->description($description); - $bodyParamsNames = array_map(fn ($p) => $p->name, $bodyParams); + $allParams = $rulesResults->flatMap->parameters->unique('name')->values()->all(); - $allParams = [ - ...$bodyParams, - ...array_filter( - array_values($routeInfo->requestParametersFromCalls->data), - fn ($p) => ! in_array($p->name, $bodyParamsNames), - ), - ]; [$queryParams, $bodyParams] = collect($allParams) - ->partition(function (Parameter $parameter) { - return $parameter->getAttribute('isInQuery'); - }); - $queryParams = $queryParams->toArray(); - $bodyParams = $bodyParams->toArray(); + ->partition(fn(Parameter $p) => $p->getAttribute('isInQuery')) + ->map->toArray(); $mediaType = $this->getMediaType($operation, $routeInfo, $allParams); @@ -77,39 +70,66 @@ public function handle(Operation $operation, RouteInfo $routeInfo) return; } - $this->addRequestBody( - $operation, - $mediaType, - Schema::createFromParameters($bodyParams), - $schemaName, - $schemaDescription, + [$schemaResults, $schemalessResults] = $rulesResults->partition('schemaName'); + $schemalessResults = collect([$this->mergeSchemalessRulesResults($schemalessResults->values())]); + + $schemas = $schemaResults->merge($schemalessResults) + ->filter(fn(ParametersExtractionResult $r) => count($r->parameters) || $r->schemaName) + ->map(function (ParametersExtractionResult $r) use ($queryParams) { + $qpNames = collect($queryParams)->keyBy('name'); + + $r->parameters = collect($r->parameters)->filter(fn($p) => ! $qpNames->has($p->name))->values()->all(); + + return $r; + }) + ->values() + ->map($this->makeSchemaFromResults(...)); + + if ($schemas->isEmpty()) { + return; + } + + $schema = $this->makeComposedRequestBodySchema($schemas); + if (! $schema instanceof Reference) { + $schema = Schema::fromType($schema); + } + + $operation->addRequestBodyObject( + RequestBodyObject::make()->setContent($mediaType, $schema), ); } - protected function addRequestBody(Operation $operation, string $mediaType, Schema $requestBodySchema, ?string $schemaName, ?string $schemaDescription) + protected function makeSchemaFromResults(ParametersExtractionResult $result): Type { - if (! $schemaName) { - $operation->addRequestBodyObject(RequestBodyObject::make()->setContent($mediaType, $requestBodySchema)); + $requestBodySchema = Schema::createFromParameters($result->parameters); - return; + if (! $result->schemaName) { + return $requestBodySchema->type; } $components = $this->openApiTransformer->getComponents(); - if (! $components->hasSchema($schemaName)) { - $requestBodySchema->type->setDescription($schemaDescription ?: ''); + if (! $components->hasSchema($result->schemaName)) { + $requestBodySchema->type->setDescription($result->description ?: ''); - $components->addSchema($schemaName, $requestBodySchema); + $components->addSchema($result->schemaName, $requestBodySchema); } - $operation->addRequestBodyObject( - RequestBodyObject::make()->setContent( - $mediaType, - app(Reference::class, [ - 'referenceType' => 'schemas', - 'fullName' => $schemaName, - 'components' => $components, - ]), - ) + return new Reference('schemas', $result->schemaName, $components); + } + + protected function makeComposedRequestBodySchema(Collection $schemas) + { + if ($schemas->count() === 1) { + return $schemas->first(); + } + + return (new AllOf())->setItems($schemas->all()); + } + + protected function mergeSchemalessRulesResults(Collection $schemalessResults): ParametersExtractionResult + { + return new ParametersExtractionResult( + parameters: $schemalessResults->values()->flatMap->parameters->unique('name')->values()->all(), ); } @@ -141,37 +161,52 @@ protected function hasBinary($bodyParams): bool }); } - protected function extractParamsFromRequestValidationRules(Route $route, ?ClassMethod $methodNode) + protected function extractRouteRequestValidationRules(RouteInfo $routeInfo, $methodNode) { - [$rules, $nodesResults] = $this->extractRouteRequestValidationRules($route, $methodNode); - - return [ - (new RulesToParameters($rules, $nodesResults, $this->openApiTransformer))->handle(), - $nodesResults[0]->schemaName ?? null, - $nodesResults[0]->description ?? null, + /* + * These are the extractors that are getting types from the validation rules, so it is + * certain that a property must have the extracted type. + */ + $typeDefiningHandlers = [ + new FormRequestRulesExtractor($methodNode, $this->openApiTransformer), + new ValidateCallExtractor( + $methodNode, + $this->openApiTransformer, + $routeInfo->route, + ), ]; - } - protected function extractRouteRequestValidationRules(Route $route, $methodNode) - { - $rules = []; - $nodesResults = []; - - // Custom form request's class `validate` method - if (($formRequestRulesExtractor = new FormRequestRulesExtractor($methodNode))->shouldHandle()) { - if (count($formRequestRules = $formRequestRulesExtractor->extract($route))) { - $rules = array_merge($rules, $formRequestRules); - $nodesResults[] = $formRequestRulesExtractor->node(); - } - } + $validationRulesExtractedResults = collect($typeDefiningHandlers) + ->filter(static fn($h) => $h->shouldHandle()) + ->map(static fn($h) => $h->extract($routeInfo)) + ->values() + ->toArray(); - if (($validateCallExtractor = new ValidateCallExtractor($methodNode, $route))->shouldHandle()) { - if ($validateCallRules = $validateCallExtractor->extract()) { - $rules = array_merge($rules, $validateCallRules); - $nodesResults[] = $validateCallExtractor->node(); - } - } + /* + * This is the extractor that cannot re-define the incoming type but can add new properties. + * Also, it is useful for additional details. + */ + $detailsExtractor = new RequestMethodCallsExtractor(); + + $methodCallsExtractedResults = $detailsExtractor->extract($routeInfo); + + return $this->mergeExtractedProperties($validationRulesExtractedResults, $methodCallsExtractedResults); + } - return [$rules, array_filter($nodesResults)]; + /** + * @param ParametersExtractionResult[] $rulesExtractedResults + */ + protected function mergeExtractedProperties( + array $rulesExtractedResults, + ParametersExtractionResult $methodCallsExtractedResult, + ) { + $rulesParameters = collect($rulesExtractedResults)->flatMap->parameters->keyBy('name'); + + $methodCallsExtractedResult->parameters = collect($methodCallsExtractedResult->parameters) + ->filter(fn(Parameter $p) => ! $rulesParameters->has($p->name)) + ->values() + ->all(); + + return [...$rulesExtractedResults, $methodCallsExtractedResult]; } } diff --git a/src/Support/OperationExtensions/RulesExtractor/FormRequestRulesExtractor.php b/src/Support/OperationExtensions/RulesExtractor/FormRequestRulesExtractor.php index 08d0ede77..f5690ce06 100644 --- a/src/Support/OperationExtensions/RulesExtractor/FormRequestRulesExtractor.php +++ b/src/Support/OperationExtensions/RulesExtractor/FormRequestRulesExtractor.php @@ -3,6 +3,8 @@ namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor; use Dedoc\Scramble\Infer; +use Dedoc\Scramble\Support\Generator\TypeTransformer; +use Dedoc\Scramble\Support\RouteInfo; use Dedoc\Scramble\Support\SchemaClassDocReflector; use Illuminate\Http\Request; use Illuminate\Routing\Route; @@ -14,16 +16,13 @@ use ReflectionClass; use Spatie\LaravelData\Contracts\BaseData; -class FormRequestRulesExtractor +class FormRequestRulesExtractor implements RulesExtractor { - private ?FunctionLike $handler; + use GeneratesParametersFromRules; - public function __construct(?FunctionLike $handler) - { - $this->handler = $handler; - } + public function __construct(private ?FunctionLike $handler, private TypeTransformer $typeTransformer) {} - public function shouldHandle() + public function shouldHandle(): bool { if (! $this->handler) { return false; @@ -42,7 +41,7 @@ public function shouldHandle() return true; } - public function node() + public function extract(RouteInfo $routeInfo): ParametersExtractionResult { $requestClassName = $this->getFormRequestClassName(); @@ -54,19 +53,23 @@ public function node() ? null : $phpDocReflector->getSchemaName($requestClassName); - return new ValidationNodesResult( - (new NodeFinder)->find( - Arr::wrap($classReflector->getMethod('rules')->getAstNode()->stmts), - fn (Node $node) => $node instanceof Node\Expr\ArrayItem - && $node->key instanceof Node\Scalar\String_ - && $node->getAttribute('parsedPhpDoc'), + return new ParametersExtractionResult( + parameters: $this->makeParameters( + node: (new NodeFinder)->find( + Arr::wrap($classReflector->getMethod('rules')->getAstNode()->stmts), + fn (Node $node) => $node instanceof Node\Expr\ArrayItem + && $node->key instanceof Node\Scalar\String_ + && $node->getAttribute('parsedPhpDoc'), + ), + rules: $this->rules($routeInfo->route), + typeTransformer: $this->typeTransformer, ), schemaName: $schemaName, description: $phpDocReflector->getDescription(), ); } - public function extract(Route $route) + protected function rules(Route $route) { $requestClassName = $this->getFormRequestClassName(); diff --git a/src/Support/OperationExtensions/RulesExtractor/GeneratesParametersFromRules.php b/src/Support/OperationExtensions/RulesExtractor/GeneratesParametersFromRules.php new file mode 100644 index 000000000..85a27bd02 --- /dev/null +++ b/src/Support/OperationExtensions/RulesExtractor/GeneratesParametersFromRules.php @@ -0,0 +1,13 @@ +handle(); + } +} diff --git a/src/Support/OperationExtensions/RulesExtractor/ValidationNodesResult.php b/src/Support/OperationExtensions/RulesExtractor/ParametersExtractionResult.php similarity index 70% rename from src/Support/OperationExtensions/RulesExtractor/ValidationNodesResult.php rename to src/Support/OperationExtensions/RulesExtractor/ParametersExtractionResult.php index 563812b88..e284f8907 100644 --- a/src/Support/OperationExtensions/RulesExtractor/ValidationNodesResult.php +++ b/src/Support/OperationExtensions/RulesExtractor/ParametersExtractionResult.php @@ -2,10 +2,13 @@ namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor; -class ValidationNodesResult +/** + * @internal + */ +class ParametersExtractionResult { public function __construct( - public $node, + public array $parameters, public ?string $schemaName = null, public ?string $description = null, ) {} diff --git a/src/Support/OperationExtensions/RulesExtractor/RequestMethodCallsExtractor.php b/src/Support/OperationExtensions/RulesExtractor/RequestMethodCallsExtractor.php new file mode 100644 index 000000000..8f00ae889 --- /dev/null +++ b/src/Support/OperationExtensions/RulesExtractor/RequestMethodCallsExtractor.php @@ -0,0 +1,20 @@ +requestParametersFromCalls->data), + ); + } +} diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesExtractor.php b/src/Support/OperationExtensions/RulesExtractor/RulesExtractor.php new file mode 100644 index 000000000..bb9c344e1 --- /dev/null +++ b/src/Support/OperationExtensions/RulesExtractor/RulesExtractor.php @@ -0,0 +1,12 @@ + */ private array $nodeDocs; @@ -31,9 +27,8 @@ class RulesToParameters public function __construct(array $rules, array $validationNodesResults, TypeTransformer $openApiTransformer) { $this->rules = $rules; - $this->validationNodesResults = $validationNodesResults; $this->openApiTransformer = $openApiTransformer; - $this->nodeDocs = $this->extractNodeDocs(); + $this->nodeDocs = $this->extractNodeDocs($validationNodesResults); } public function handle(): array @@ -215,23 +210,12 @@ private function getOrCreateDeepTypeContainer(Type &$base, array $path) } } - private function extractNodeDocs() + private function extractNodeDocs($validationNodesResults) { - return collect($this->validationNodesResults) - ->mapWithKeys(function (ValidationNodesResult $result) { - $arrayNodes = (new NodeFinder)->find( - Arr::wrap($result->node), - fn (Node $node) => $node instanceof Node\Expr\ArrayItem - && $node->key instanceof Node\Scalar\String_ - && $node->getAttribute('parsedPhpDoc') - ); - - return collect($arrayNodes) - ->mapWithKeys(fn (Node\Expr\ArrayItem $item) => [ - $item->key->value => $item->getAttribute('parsedPhpDoc'), - ]) - ->toArray(); - }) + return collect($validationNodesResults) + ->mapWithKeys(fn (Node\Expr\ArrayItem $item) => [ + $item->key->value => $item->getAttribute('parsedPhpDoc'), + ]) ->toArray(); } diff --git a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php index 6a4b907ba..3b924b67e 100644 --- a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php +++ b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php @@ -2,6 +2,8 @@ namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor; +use Dedoc\Scramble\Support\Generator\TypeTransformer; +use Dedoc\Scramble\Support\RouteInfo; use Dedoc\Scramble\Support\SchemaClassDocReflector; use Illuminate\Http\Request; use Illuminate\Routing\Route; @@ -10,127 +12,142 @@ use PhpParser\PrettyPrinter\Standard; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; -class ValidateCallExtractor +class ValidateCallExtractor implements RulesExtractor { - protected ?Node\FunctionLike $handle; - protected ?Route $route; + use GeneratesParametersFromRules; - public function __construct(?Node\FunctionLike $handle, ?Route $route = null) - { - $this->handle = $handle; - $this->route = $route; - } + public function __construct( + protected ?Node\FunctionLike $handle, + protected TypeTransformer $typeTransformer, + protected ?Route $route = null, + ) {} public function shouldHandle(): bool { return (bool) $this->handle; } - public function node(): ?ValidationNodesResult + public function extract(RouteInfo $routeInfo): ParametersExtractionResult { $methodNode = $this->handle; // $request->validate, when $request is a Request instance /** @var Node\Expr\MethodCall $callToValidate */ - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\MethodCall + fn(Node $node) => $node instanceof Node\Expr\MethodCall && $node->var instanceof Node\Expr\Variable && is_a($this->getPossibleParamType($methodNode, $node->var), Request::class, true) && $node->name instanceof Node\Identifier - && $node->name->name === 'validate' + && $node->name->name === 'validate', ); $validationRules = $callToValidate->args[0] ?? null; if (! $validationRules) { // $this->validate($request, $rules), rules are second param. First should be $request, but no way to check type. So relying on convention. - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\MethodCall + fn(Node $node) => $node instanceof Node\Expr\MethodCall && count($node->args) >= 2 && $node->var instanceof Node\Expr\Variable && $node->var->name === 'this' && $node->name instanceof Node\Identifier && $node->name->name === 'validate' && $node->args[0]->value instanceof Node\Expr\Variable && is_a($this->getPossibleParamType($methodNode, $node->args[0]->value), Request::class, true) - && $node->name->name === 'validate' + && $node->name->name === 'validate', ); $validationRules = $callToValidate->args[1] ?? null; } if (! $validationRules) { // Validator::make($request->...(), $rules), rules are second param. First should be $request, but no way to check type. So relying on convention. - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\StaticCall + fn(Node $node) => $node instanceof Node\Expr\StaticCall && count($node->args) >= 2 - && $node->class instanceof Node\Name && is_a($node->class->toString(), \Illuminate\Support\Facades\Validator::class, true) + && $node->class instanceof Node\Name && is_a($node->class->toString(), + \Illuminate\Support\Facades\Validator::class, + true) && $node->name instanceof Node\Identifier && $node->name->name === 'make' - && $node->args[0]->value instanceof Node\Expr\MethodCall && is_a($this->getPossibleParamType($methodNode, $node->args[0]->value->var), Request::class, true) + && $node->args[0]->value instanceof Node\Expr\MethodCall && is_a($this->getPossibleParamType($methodNode, + $node->args[0]->value->var), + Request::class, + true), ); $validationRules = $callToValidate->args[1] ?? null; } if (! $validationRules) { - return null; + return new ParametersExtractionResult(parameters: []); } - $phpDocReflector = new SchemaClassDocReflector($callToValidate->getAttribute('parsedPhpDoc', new PhpDocNode([]))); - - return new ValidationNodesResult( - $validationRules instanceof Node\Arg ? $validationRules->value : $validationRules, + $validationRulesNode = $validationRules instanceof Node\Arg ? $validationRules->value : $validationRules; + + $phpDocReflector = new SchemaClassDocReflector($callToValidate->getAttribute('parsedPhpDoc', + new PhpDocNode([]))); + + return new ParametersExtractionResult( + parameters: $this->makeParameters( + node: (new NodeFinder())->find( + $validationRulesNode instanceof Node\Expr\Array_ ? $validationRulesNode->items : [], + fn(Node $node) => $node instanceof Node\Expr\ArrayItem + && $node->key instanceof Node\Scalar\String_ + && $node->getAttribute('parsedPhpDoc'), + ), + rules: $this->rules($validationRulesNode), + typeTransformer: $this->typeTransformer, + ), schemaName: $phpDocReflector->getSchemaName(), description: $phpDocReflector->getDescription(), ); } - public function extract() + public function rules($validationRules): array { + if (! $validationRules) { + return []; + } + $methodNode = $this->handle; - $validationRules = $this->node()->node ?? null; - - if ($validationRules) { - $printer = new Standard; - $validationRulesCode = $printer->prettyPrint([$validationRules]); - - $injectableParams = collect($methodNode->getParams()) - ->filter(fn (Node\Param $param) => isset($param->type->name)) - ->filter(fn (Node\Param $param) => ! class_exists($className = (string) $param->type) || ! is_a($className, Request::class, true)) - ->filter(fn (Node\Param $param) => isset($param->var->name) && is_string($param->var->name)) - ->mapWithKeys(function (Node\Param $param) { - try { - $type = (string) $param->type; - $primitives = [ - 'int' => 1, - 'bool' => true, - 'string' => '', - 'float' => 1, - ]; - $value = $primitives[$type] ?? app($type); - - return [ - $param->var->name => $value, - ]; - } catch (\Throwable $e) { - return []; - } - }) - ->all(); - - try { - extract($injectableParams); - - if ($this->route) { - $rules = (fn() => eval("\$request = request(); return $validationRulesCode;")) - ->call($this->route->getController()); - } else { - $rules = eval("\$request = request(); return $validationRulesCode;"); + + $printer = new Standard(); + $validationRulesCode = $printer->prettyPrint([$validationRules]); + + $injectableParams = collect($methodNode->getParams()) + ->filter(fn(Node\Param $param) => isset($param->type->name)) + ->filter(fn(Node\Param $param) => ! class_exists($className = (string) $param->type) || ! is_a($className, + Request::class, + true)) + ->filter(fn(Node\Param $param) => isset($param->var->name) && is_string($param->var->name)) + ->mapWithKeys(function (Node\Param $param) { + try { + $type = (string) $param->type; + $primitives = [ + 'int' => 1, + 'bool' => true, + 'string' => '', + 'float' => 1, + ]; + $value = $primitives[$type] ?? app($type); + + return [ + $param->var->name => $value, + ]; + } catch (\Throwable $e) { + return []; } - } catch (\Throwable $exception) { - throw $exception; - } + }) + ->all(); + + extract($injectableParams); + + if ($this->route) { + $rules = (fn() => eval("\$request = request(); return $validationRulesCode;")) + ->call($this->route->getController()); + } else { + $rules = eval("\$request = request(); return $validationRulesCode;"); } - return $rules ?? null; + return $rules ?? []; } private function getPossibleParamType(Node\Stmt\ClassMethod $methodNode, Node\Expr\Variable $node): ?string diff --git a/src/Support/TypeToSchemaExtensions/AnonymousResourceCollectionTypeToSchema.php b/src/Support/TypeToSchemaExtensions/AnonymousResourceCollectionTypeToSchema.php index 8256984a6..014d2dd26 100644 --- a/src/Support/TypeToSchemaExtensions/AnonymousResourceCollectionTypeToSchema.php +++ b/src/Support/TypeToSchemaExtensions/AnonymousResourceCollectionTypeToSchema.php @@ -7,6 +7,7 @@ use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\Types\ArrayType as OpenApiArrayType; use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType; +use Dedoc\Scramble\Support\Generator\Types\StringType; use Dedoc\Scramble\Support\Generator\Types\UnknownType; use Dedoc\Scramble\Support\Type\Generic; use Dedoc\Scramble\Support\Type\KeyedArrayType; @@ -15,6 +16,10 @@ use Dedoc\Scramble\Support\Type\TypeWalker; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\Json\PaginatedResourceResponse; +use Illuminate\Pagination\AbstractCursorPaginator; +use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Support\Arr; class AnonymousResourceCollectionTypeToSchema extends TypeToSchemaExtension { @@ -51,21 +56,21 @@ public function toResponse(Type $type) $additional->items = $this->flattenMergeValues($additional->items); } - // In case of paginated resource, we want to get pagination response. - if ($type->templateTypes[0] instanceof Generic && ! $type->templateTypes[0]->isInstanceOf(JsonResource::class)) { - return $this->openApiTransformer->toResponse($type->templateTypes[0]); - } - if (! $collectingResourceType = $this->getCollectingResourceType($type)) { return null; } - $jsonResourceOpenApiType = $this->openApiTransformer->transform($collectingResourceType); - - $shouldWrap = ($wrapKey = AnonymousResourceCollection::$wrap ?? null) !== null + $shouldWrap = ($wrapKey = $collectingResourceType->name::$wrap ?? null) !== null || $additional instanceof KeyedArrayType; $wrapKey = $wrapKey ?: 'data'; + // In case of paginated resource, we want to get pagination response. + if ($type->templateTypes[0] instanceof Generic && ! $type->templateTypes[0]->isInstanceOf(JsonResource::class)) { + return $this->getPaginatedCollectionResponse($type->templateTypes[0], $collectingResourceType, $wrapKey); + } + + $jsonResourceOpenApiType = $this->openApiTransformer->transform($collectingResourceType); + $openApiType = $shouldWrap ? (new OpenApiObjectType) ->addProperty($wrapKey, (new OpenApiArrayType)->setItems($jsonResourceOpenApiType)) @@ -83,6 +88,53 @@ public function toResponse(Type $type) ->setContent('application/json', Schema::fromType($openApiType)); } + /** + * @see PaginatedResourceResponse + */ + private function getPaginatedCollectionResponse(Generic $type, ObjectType $collectingClassType, string $wrapKey) + { + if (! $type->isInstanceOf(AbstractPaginator::class) && ! $type->isInstanceOf(AbstractCursorPaginator::class)) { + return null; + } + + $paginatorResponse = $this->openApiTransformer->toResponse($type); + $paginatorSchema = array_values($paginatorResponse->content)[0] ?? null; + $paginatorSchemaType = $paginatorSchema->type ?? null; + + if (! $paginatorSchemaType instanceof OpenApiObjectType) { + // should not happen + return null; + } + + $responseType = (new OpenApiObjectType) + ->addProperty($wrapKey, $paginatorSchemaType->properties['data']) + ->addProperty('meta', tap(new OpenApiObjectType, function (OpenApiObjectType $type) use ($paginatorSchemaType) { + $type->properties = Arr::except($paginatorSchemaType->properties, [ + 'data', + 'first_page_url', + 'last_page_url', + 'prev_page_url', + 'next_page_url', + ]); + $type->setRequired(array_keys($type->properties)); + })) + ->addProperty('links', tap(new OpenApiObjectType, function (OpenApiObjectType $type) use ($paginatorSchemaType) { + $defaultLinkType = (new StringType)->nullable(true); + $type->properties = [ + 'first' => $paginatorSchemaType->properties['first_page_url'] ?? $defaultLinkType, + 'last' => $paginatorSchemaType->properties['last_page_url'] ?? $defaultLinkType, + 'prev' => $paginatorSchemaType->properties['prev_page_url'] ?? $defaultLinkType, + 'next' => $paginatorSchemaType->properties['next_page_url'] ?? $defaultLinkType, + ]; + $type->setRequired(array_keys($type->properties)); + })) + ->setRequired([$wrapKey, 'links', 'meta']); + + return Response::make(200) + ->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`') + ->setContent('application/json', Schema::fromType($responseType)); + } + private function getCollectingResourceType(Generic $type): ?ObjectType { // In case of paginated resource, we still want to get to the underlying JsonResource. diff --git a/src/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchema.php b/src/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchema.php new file mode 100644 index 000000000..28b26fa91 --- /dev/null +++ b/src/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchema.php @@ -0,0 +1,58 @@ +name === CursorPaginator::class + && count($type->templateTypes) === 1 + && $type->templateTypes[0] instanceof ObjectType; + } + + /** + * @param Generic $type + */ + public function toResponse(Type $type) + { + $collectingClassType = $type->templateTypes[0]; + + if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) { + return null; + } + + if (! ($collectingType = $this->openApiTransformer->transform($collectingClassType))) { + return null; + } + + $type = (new OpenApiObjectType) + ->addProperty('data', (new ArrayType)->setItems($collectingType)) + ->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.')) + ->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.')) + ->addProperty('next_cursor', (new StringType)->nullable(true)->setDescription('The "cursor" that points to the next set of items.')) + ->addProperty('next_page_url', (new StringType)->format('uri')->nullable(true)) + ->addProperty('prev_cursor', (new StringType)->nullable(true)->setDescription('The "cursor" that points to the previous set of items.')) + ->addProperty('prev_page_url', (new StringType)->format('uri')->nullable(true)) + ->setRequired(['data', 'path', 'per_page', 'next_cursor', 'next_page_url', 'prev_cursor', 'prev_page_url']); + + return Response::make(200) + ->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`') + ->setContent('application/json', Schema::fromType($type)); + } +} diff --git a/src/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchema.php b/src/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchema.php index e5720612a..1ea41bde1 100644 --- a/src/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchema.php +++ b/src/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchema.php @@ -42,37 +42,27 @@ public function toResponse(Type $type) return null; } - $type = new OpenApiObjectType; - $type->addProperty('data', (new ArrayType)->setItems($collectingType)); - $type->addProperty( - 'links', - (new OpenApiObjectType) - ->addProperty('first', (new StringType)->nullable(true)) - ->addProperty('last', (new StringType)->nullable(true)) - ->addProperty('prev', (new StringType)->nullable(true)) - ->addProperty('next', (new StringType)->nullable(true)) - ->setRequired(['first', 'last', 'prev', 'next']) - ); - $type->addProperty( - 'meta', - (new OpenApiObjectType) - ->addProperty('current_page', new IntegerType) - ->addProperty('from', (new IntegerType)->nullable(true)) - ->addProperty('last_page', new IntegerType) - ->addProperty('links', (new ArrayType)->setItems( - (new OpenApiObjectType) - ->addProperty('url', (new StringType)->nullable(true)) - ->addProperty('label', new StringType) - ->addProperty('active', new BooleanType) - ->setRequired(['url', 'label', 'active']) - )->setDescription('Generated paginator links.')) - ->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.')) - ->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.')) - ->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.')) - ->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.')) - ->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total']) - ); - $type->setRequired(['data', 'links', 'meta']); + $type = (new OpenApiObjectType) + ->addProperty('current_page', new IntegerType) + ->addProperty('data', (new ArrayType)->setItems($collectingType)) + ->addProperty('first_page_url', (new StringType)->nullable(true)) + ->addProperty('from', (new IntegerType)->nullable(true)) + ->addProperty('last_page_url', (new StringType)->nullable(true)) + ->addProperty('last_page', new IntegerType) + ->addProperty('links', (new ArrayType)->setItems( + (new OpenApiObjectType) + ->addProperty('url', (new StringType)->nullable(true)) + ->addProperty('label', new StringType) + ->addProperty('active', new BooleanType) + ->setRequired(['url', 'label', 'active']) + )->setDescription('Generated paginator links.')) + ->addProperty('next_page_url', (new StringType)->nullable(true)) + ->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.')) + ->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.')) + ->addProperty('prev_page_url', (new StringType)->nullable(true)) + ->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.')) + ->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.')) + ->setRequired(['current_page', 'data', 'first_page_url', 'from', 'last_page_url', 'last_page', 'links', 'next_page_url', 'path', 'per_page', 'prev_page_url', 'to', 'total']); return Response::make(200) ->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`') diff --git a/src/Support/TypeToSchemaExtensions/PaginatorTypeToSchema.php b/src/Support/TypeToSchemaExtensions/PaginatorTypeToSchema.php new file mode 100644 index 000000000..46a03b60d --- /dev/null +++ b/src/Support/TypeToSchemaExtensions/PaginatorTypeToSchema.php @@ -0,0 +1,60 @@ +name === Paginator::class + && count($type->templateTypes) === 1 + && $type->templateTypes[0] instanceof ObjectType; + } + + /** + * @param Generic $type + */ + public function toResponse(Type $type) + { + $collectingClassType = $type->templateTypes[0]; + + if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) { + return null; + } + + if (! ($collectingType = $this->openApiTransformer->transform($collectingClassType))) { + return null; + } + + $type = (new OpenApiObjectType) + ->addProperty('current_page', new IntegerType) + ->addProperty('data', (new ArrayType)->setItems($collectingType)) + ->addProperty('first_page_url', (new StringType)->nullable(true)) + ->addProperty('from', (new IntegerType)->nullable(true)) + ->addProperty('next_page_url', (new StringType)->nullable(true)) + ->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.')) + ->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.')) + ->addProperty('prev_page_url', (new StringType)->nullable(true)) + ->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.')) + ->setRequired(['current_page', 'data', 'first_page_url', 'from', 'next_page_url', 'path', 'per_page', 'prev_page_url', 'to']); + + return Response::make(200) + ->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`') + ->setContent('application/json', Schema::fromType($type)); + } +} diff --git a/tests/.pest/snapshots/Support/OperationExtensions/RequestBodyExtensionTest/it_allows_to_use_validation_on_form_request.snap b/tests/.pest/snapshots/Support/OperationExtensions/RequestBodyExtensionTest/it_allows_to_use_validation_on_form_request.snap new file mode 100644 index 000000000..000cd52fd --- /dev/null +++ b/tests/.pest/snapshots/Support/OperationExtensions/RequestBodyExtensionTest/it_allows_to_use_validation_on_form_request.snap @@ -0,0 +1,133 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Laravel", + "version": "0.0.1" + }, + "servers": [ + { + "url": "http:\/\/localhost\/api" + } + ], + "paths": { + "\/a": { + "post": { + "operationId": "allowsBothFormRequestAndInlineValidationRules.a", + "tags": [ + "AllowsBothFormRequestAndInlineValidationRules" + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "allOf": [ + { + "$ref": "#\/components\/schemas\/FormRequest_WithData" + }, + { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "" + }, + "422": { + "$ref": "#\/components\/responses\/ValidationException" + } + } + } + }, + "\/b": { + "post": { + "operationId": "allowsBothFormRequestAndInlineValidationRules.b", + "tags": [ + "AllowsBothFormRequestAndInlineValidationRules" + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "allOf": [ + { + "$ref": "#\/components\/schemas\/FormRequest_WithData" + }, + { + "type": "object", + "properties": { + "baz": { + "type": "number" + } + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "" + }, + "422": { + "$ref": "#\/components\/responses\/ValidationException" + } + } + } + } + }, + "components": { + "schemas": { + "FormRequest_WithData": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "title": "FormRequest_WithData" + } + }, + "responses": { + "ValidationException": { + "description": "Validation error", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Errors overview." + }, + "errors": { + "type": "object", + "description": "A detailed description of each field that failed validation.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": [ + "message", + "errors" + ] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/.pest/snapshots/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap new file mode 100644 index 000000000..dba1b0da6 --- /dev/null +++ b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap @@ -0,0 +1,66 @@ +{ + "description": "Paginated set of `CursorPaginatorTypeToSchemaTest_Resource`", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/CursorPaginatorTypeToSchemaTest_Resource" + } + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "Base path for paginator generated URLs." + }, + "per_page": { + "type": "integer", + "description": "Number of items shown per page." + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "The \"cursor\" that points to the next set of items." + }, + "next_page_url": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "prev_cursor": { + "type": [ + "string", + "null" + ], + "description": "The \"cursor\" that points to the previous set of items." + }, + "prev_page_url": { + "type": [ + "string", + "null" + ], + "format": "uri" + } + }, + "required": [ + "data", + "path", + "per_page", + "next_cursor", + "next_page_url", + "prev_cursor", + "prev_page_url" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/.pest/snapshots/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap new file mode 100644 index 000000000..3790f10b5 --- /dev/null +++ b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap @@ -0,0 +1,117 @@ +{ + "description": "Paginated set of `LengthAwarePaginatorTypeToSchemaTest_Resource`", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/LengthAwarePaginatorTypeToSchemaTest_Resource" + } + }, + "first_page_url": { + "type": [ + "string", + "null" + ] + }, + "from": { + "type": [ + "integer", + "null" + ] + }, + "last_page_url": { + "type": [ + "string", + "null" + ] + }, + "last_page": { + "type": "integer" + }, + "links": { + "type": "array", + "description": "Generated paginator links.", + "items": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ] + }, + "label": { + "type": "string" + }, + "active": { + "type": "boolean" + } + }, + "required": [ + "url", + "label", + "active" + ] + } + }, + "next_page_url": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "Base path for paginator generated URLs." + }, + "per_page": { + "type": "integer", + "description": "Number of items shown per page." + }, + "prev_page_url": { + "type": [ + "string", + "null" + ] + }, + "to": { + "type": [ + "integer", + "null" + ], + "description": "Number of the last item in the slice." + }, + "total": { + "type": "integer", + "description": "Total number of items being paginated." + } + }, + "required": [ + "current_page", + "data", + "first_page_url", + "from", + "last_page_url", + "last_page", + "links", + "next_page_url", + "path", + "per_page", + "prev_page_url", + "to", + "total" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/.pest/snapshots/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap new file mode 100644 index 000000000..216a051c3 --- /dev/null +++ b/tests/.pest/snapshots/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest/it_correctly_documents_when_annotated.snap @@ -0,0 +1,74 @@ +{ + "description": "Paginated set of `PaginatorTypeToSchemaTest_Resource`", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/PaginatorTypeToSchemaTest_Resource" + } + }, + "first_page_url": { + "type": [ + "string", + "null" + ] + }, + "from": { + "type": [ + "integer", + "null" + ] + }, + "next_page_url": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "Base path for paginator generated URLs." + }, + "per_page": { + "type": "integer", + "description": "Number of items shown per page." + }, + "prev_page_url": { + "type": [ + "string", + "null" + ] + }, + "to": { + "type": [ + "integer", + "null" + ], + "description": "Number of the last item in the slice." + } + }, + "required": [ + "current_page", + "data", + "first_page_url", + "from", + "next_page_url", + "path", + "per_page", + "prev_page_url", + "to" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/Support/OperationExtensions/RequestBodyExtensionTest.php b/tests/Support/OperationExtensions/RequestBodyExtensionTest.php index 2c6f2f689..07b02ecad 100644 --- a/tests/Support/OperationExtensions/RequestBodyExtensionTest.php +++ b/tests/Support/OperationExtensions/RequestBodyExtensionTest.php @@ -1,9 +1,24 @@ not->toHaveKey('requestBody'); +}); +class RequestBodyExtensionTest__doesnt_use_body_when_empty +{ + public function store(Illuminate\Http\Request $request) {} +} + it('uses application/json media type as a default request media type', function () { $openApiDocument = generateForRoute(function () { return RouteFacade::post('api/test', [RequestBodyExtensionTest__uses_application_json_as_default::class, 'index']); @@ -412,6 +427,38 @@ public function rules() } } +it('allows to use validation on form request', function () { + $routes = collect([ + RouteFacade::post('a', [AllowsBothFormRequestAndInlineValidationRules::class, 'a']), + RouteFacade::post('b', [AllowsBothFormRequestAndInlineValidationRules::class, 'b']), + ])->map->uri->toArray(); + + Scramble::routes(fn (Route $r) => in_array($r->uri, $routes)); + + $document = app()->make(\Dedoc\Scramble\Generator::class)(); + + expect($document)->toMatchSnapshot(); +}); +class FormRequest_WithData extends FormRequest +{ + public function rules() + { + return ['foo' => 'string']; + } +} +class AllowsBothFormRequestAndInlineValidationRules +{ + public function a(FormRequest_WithData $request) + { + $request->validate(['bar' => 'string']); + } + + public function b(FormRequest_WithData $request) + { + $request->validate(['baz' => 'numeric']); + } +} + it('allows to add description for validation calls in schemas', function () { $document = generateForRoute(function () { return RouteFacade::post('test', Validation_DescriptionSchemaNamesTest_Controller::class); diff --git a/tests/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest.php new file mode 100644 index 000000000..9621ba094 --- /dev/null +++ b/tests/Support/TypeToSchemaExtensions/CursorPaginatorTypeToSchemaTest.php @@ -0,0 +1,35 @@ +shouldHandle($type))->toBeTrue(); + expect($extension->toResponse($type)->toArray())->toMatchSnapshot(); +}); + +class CursorPaginatorTypeToSchemaTest_Resource extends JsonResource +{ + public function toArray(Request $request) + { + return ['id' => 1]; + } +} diff --git a/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php index 318038304..6e85085b6 100644 --- a/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php +++ b/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php @@ -138,3 +138,36 @@ public function getInteger(): int return unknown(); } } + +it('handles default in json api resource', function () { + $type = new Generic(JsonResourceTypeToSchemaTest_WithDefault::class, [new UnknownType]); + + $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [ + JsonResourceTypeToSchema::class, + ResponseTypeToSchema::class, + ]); + $extension = new JsonResourceTypeToSchema($infer, $transformer, $components); + + expect($extension->toSchema($type)->toArray())->toBe([ + 'type' => 'object', + 'properties' => [ + 'foo' => [ + 'type' => 'string', + 'default' => 'fooBar', + ], + ], + 'required' => ['foo'], + ]); +}); +class JsonResourceTypeToSchemaTest_WithDefault extends \Illuminate\Http\Resources\Json\JsonResource +{ + public function toArray(\Illuminate\Http\Request $request) + { + return [ + /** + * @default fooBar + */ + 'foo' => $this->resource->foo, + ]; + } +} diff --git a/tests/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest.php new file mode 100644 index 000000000..5e2e62a0b --- /dev/null +++ b/tests/Support/TypeToSchemaExtensions/LengthAwarePaginatorTypeToSchemaTest.php @@ -0,0 +1,35 @@ +shouldHandle($type))->toBeTrue(); + expect($extension->toResponse($type)->toArray())->toMatchSnapshot(); +}); + +class LengthAwarePaginatorTypeToSchemaTest_Resource extends JsonResource +{ + public function toArray(Request $request) + { + return ['id' => 1]; + } +} diff --git a/tests/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest.php new file mode 100644 index 000000000..a85d50dd4 --- /dev/null +++ b/tests/Support/TypeToSchemaExtensions/PaginatorTypeToSchemaTest.php @@ -0,0 +1,35 @@ +shouldHandle($type))->toBeTrue(); + expect($extension->toResponse($type)->toArray())->toMatchSnapshot(); +}); + +class PaginatorTypeToSchemaTest_Resource extends JsonResource +{ + public function toArray(Request $request) + { + return ['id' => 1]; + } +}