Repository: dedoc/scramble (php)

Issue:
Add ability to set a default value in docs for params
### Discussed in https://github.com/dedoc/scramble/discussions/266

<div type='discussions-op-text'>

<sup>Originally posted by **slnw** October 25, 2023</sup>
Hi, I have response param $foo which default value is 20. How do I set it in the openapi docs?</div>

Pull request description:
Request params inference from method calls on request, default request values support
fixes #340 

Patch:
diff --git a/src/Infer/Analyzer/MethodAnalyzer.php b/src/Infer/Analyzer/MethodAnalyzer.php
index 3b21495..e09d4c5 100644
--- a/src/Infer/Analyzer/MethodAnalyzer.php
+++ b/src/Infer/Analyzer/MethodAnalyzer.php
@@ -5,6 +5,7 @@ namespace Dedoc\Scramble\Infer\Analyzer;
 use Dedoc\Scramble\Infer\Context;
 use Dedoc\Scramble\Infer\Definition\ClassDefinition;
 use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
+use Dedoc\Scramble\Infer\Handler\IndexBuildingHandler;
 use Dedoc\Scramble\Infer\Reflector\ClassReflector;
 use Dedoc\Scramble\Infer\Scope\Index;
 use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
@@ -25,11 +26,12 @@ class MethodAnalyzer
     ) {
     }
 
-    public function analyze(FunctionLikeDefinition $methodDefinition)
+    public function analyze(FunctionLikeDefinition $methodDefinition, array $indexBuilders = [])
     {
         $this->traverseClassMethod(
             [$this->getClassReflector()->getMethod($methodDefinition->type->name)->getAstNode()],
             $methodDefinition,
+            $indexBuilders,
         );
 
         $methodDefinition = $this->index
@@ -46,7 +48,7 @@ class MethodAnalyzer
         return ClassReflector::make($this->classDefinition->name);
     }
 
-    private function traverseClassMethod(array $nodes, FunctionLikeDefinition $methodDefinition)
+    private function traverseClassMethod(array $nodes, FunctionLikeDefinition $methodDefinition, array $indexBuilders = [])
     {
         $traverser = new NodeTraverser;
 
@@ -57,6 +59,7 @@ class MethodAnalyzer
             $nameResolver,
             new Scope($this->index, new NodeTypesResolver(), new ScopeContext($this->classDefinition), $nameResolver),
             Context::getInstance()->extensionsBroker->extensions,
+            [new IndexBuildingHandler($indexBuilders)],
         ));
 
         $node = (new NodeFinder())
diff --git a/src/Infer/Definition/ClassDefinition.php b/src/Infer/Definition/ClassDefinition.php
index ae07977..9affd5b 100644
--- a/src/Infer/Definition/ClassDefinition.php
+++ b/src/Infer/Definition/ClassDefinition.php
@@ -43,7 +43,7 @@ class ClassDefinition
         return $this->isInstanceOf($className) && $this->name !== $className;
     }
 
-    public function getMethodDefinition(string $name, Scope $scope = new GlobalScope)
+    public function getMethodDefinition(string $name, Scope $scope = new GlobalScope, array $indexBuilders = [])
     {
         if (! array_key_exists($name, $this->methods)) {
             return null;
@@ -55,7 +55,7 @@ class ClassDefinition
             $this->methods[$name] = (new MethodAnalyzer(
                 $scope->index,
                 $this
-            ))->analyze($methodDefinition);
+            ))->analyze($methodDefinition, $indexBuilders);
         }
 
         $methodScope = new Scope(
diff --git a/src/Infer/Handler/IndexBuildingHandler.php b/src/Infer/Handler/IndexBuildingHandler.php
new file mode 100644
index 0000000..f707ddf
--- /dev/null
+++ b/src/Infer/Handler/IndexBuildingHandler.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Dedoc\Scramble\Infer\Handler;
+
+use Dedoc\Scramble\Infer\Scope\Scope;
+use PhpParser\Node;
+
+class IndexBuildingHandler
+{
+    public function __construct(
+        private array $indexBuilders,
+    ) {
+    }
+
+    public function shouldHandle($node)
+    {
+        return true;
+    }
+
+    public function leave(Node $node, Scope $scope)
+    {
+        foreach ($this->indexBuilders as $indexBuilder) {
+            $indexBuilder->afterAnalyzedNode($scope, $node);
+        }
+    }
+}
diff --git a/src/Infer/SimpleTypeGetters/ScalarTypeGetter.php b/src/Infer/SimpleTypeGetters/ScalarTypeGetter.php
index b22ae76..01cfa2e 100644
--- a/src/Infer/SimpleTypeGetters/ScalarTypeGetter.php
+++ b/src/Infer/SimpleTypeGetters/ScalarTypeGetter.php
@@ -2,6 +2,7 @@
 
 namespace Dedoc\Scramble\Infer\SimpleTypeGetters;
 
+use Dedoc\Scramble\Support\Type\Literal\LiteralFloatType;
 use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
 use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
 use Dedoc\Scramble\Support\Type\Type;
@@ -20,6 +21,10 @@ class ScalarTypeGetter
             return new LiteralIntegerType($node->value);
         }
 
+        if ($node instanceof Node\Scalar\DNumber) {
+            return new LiteralFloatType($node->value);
+        }
+
         return new UnknownType('Cannot get type from scalar');
     }
 }
diff --git a/src/Infer/TypeInferer.php b/src/Infer/TypeInferer.php
index e0a442b..e691e00 100644
--- a/src/Infer/TypeInferer.php
+++ b/src/Infer/TypeInferer.php
@@ -37,6 +37,7 @@ class TypeInferer extends NodeVisitorAbstract
         private FileNameResolver $nameResolver,
         private ?Scope $scope = null,
         array $extensions = [],
+        array $handlers = [],
     ) {
         $this->handlers = [
             new FunctionLikeHandler(),
@@ -56,6 +57,7 @@ class TypeInferer extends NodeVisitorAbstract
                 fn ($ext) => $ext instanceof ExpressionExceptionExtension,
             ))),
             new PhpDocHandler(),
+            ...$handlers,
         ];
     }
 
diff --git a/src/Support/Generator/Parameter.php b/src/Support/Generator/Parameter.php
index a4ef059..c9b5770 100644
--- a/src/Support/Generator/Parameter.php
+++ b/src/Support/Generator/Parameter.php
@@ -4,10 +4,14 @@ namespace Dedoc\Scramble\Support\Generator;
 
 class Parameter
 {
+    use WithAttributes;
+
     public string $name;
 
     /**
      * Possible values are "query", "header", "path" or "cookie".
+     *
+     * @var "query"|"header"|"path"|"cookie".
      */
     public string $in;
 
@@ -18,6 +22,9 @@ class Parameter
     /** @var array|scalar|null|MissingExample */
     public $example;
 
+    /** @var array|scalar|null|MissingExample */
+    public $default;
+
     public bool $deprecated = false;
 
     public bool $allowEmptyValue = false;
@@ -28,7 +35,9 @@ class Parameter
     {
         $this->name = $name;
         $this->in = $in;
+
         $this->example = new MissingExample;
+        $this->default = new MissingExample;
 
         if ($this->in === 'path') {
             $this->required = true;
@@ -55,7 +64,11 @@ class Parameter
             $result['schema'] = $this->schema->toArray();
         }
 
-        return array_merge($result, $this->example instanceof MissingExample ? [] : ['example' => $this->example]);
+        return array_merge(
+            $result,
+            $this->example instanceof MissingExample ? [] : ['example' => $this->example],
+            $this->default instanceof MissingExample ? [] : ['default' => $this->default],
+        );
     }
 
     public function required(bool $required)
@@ -79,6 +92,13 @@ class Parameter
         return $this;
     }
 
+    public function default($default)
+    {
+        $this->default = $default;
+
+        return $this;
+    }
+
     public function description(string $description)
     {
         $this->description = $description;
diff --git a/src/Support/Generator/Schema.php b/src/Support/Generator/Schema.php
index 24759a1..125b695 100644
--- a/src/Support/Generator/Schema.php
+++ b/src/Support/Generator/Schema.php
@@ -46,6 +46,7 @@ class Schema
 
                 $paramType->setDescription($parameter->description);
                 $paramType->example($parameter->example);
+                $paramType->default($parameter->default);
 
                 $type->addProperty($parameter->name, $paramType);
             })
diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php
index d2949bf..9327518 100644
--- a/src/Support/Generator/Types/Type.php
+++ b/src/Support/Generator/Types/Type.php
@@ -18,6 +18,9 @@ abstract class Type
     /** @var array|scalar|null|MissingExample */
     public $example;
 
+    /** @var array|scalar|null|MissingExample */
+    public $default;
+
     /** @var array<array|scalar|null|MissingExample> */
     public $examples = [];
 
@@ -29,6 +32,7 @@ abstract class Type
     {
         $this->type = $type;
         $this->example = new MissingExample;
+        $this->default = new MissingExample;
     }
 
     public function nullable(bool $nullable)
@@ -53,6 +57,7 @@ abstract class Type
         $this->enum = $fromType->enum;
         $this->description = $fromType->description;
         $this->example = $fromType->example;
+        $this->default = $fromType->default;
 
         return $this;
     }
@@ -67,6 +72,7 @@ abstract class Type
                 'enum' => count($this->enum) ? $this->enum : null,
             ]),
             $this->example instanceof MissingExample ? [] : ['example' => $this->example],
+            $this->default instanceof MissingExample ? [] : ['default' => $this->default],
             count(
                 $examples = collect($this->examples)
                     ->reject(fn ($example) => $example instanceof MissingExample)
@@ -100,6 +106,16 @@ abstract class Type
         return $this;
     }
 
+    /**
+     * @param  array|scalar|null|MissingExample  $default
+     */
+    public function default($default)
+    {
+        $this->default = $default;
+
+        return $this;
+    }
+
     /**
      * @param  array<array|scalar|null|MissingExample>  $examples
      */
diff --git a/src/Support/Helpers/ExamplesExtractor.php b/src/Support/Helpers/ExamplesExtractor.php
index bc0c908..ecfa67f 100644
--- a/src/Support/Helpers/ExamplesExtractor.php
+++ b/src/Support/Helpers/ExamplesExtractor.php
@@ -11,18 +11,19 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
 class ExamplesExtractor
 {
     public function __construct(
-        private ?PhpDocNode $docNode
+        private ?PhpDocNode $docNode,
+        private string $tagName = '@example',
     ) {
     }
 
-    public static function make(?PhpDocNode $docNode)
+    public static function make(?PhpDocNode $docNode, string $tagName = '@example')
     {
-        return new self($docNode);
+        return new self($docNode, $tagName);
     }
 
     public function extract(bool $preferString = false)
     {
-        if (! count($examples = $this->docNode->getTagsByName('@example'))) {
+        if (! count($examples = $this->docNode?->getTagsByName($this->tagName) ?? [])) {
             return [];
         }
 
@@ -52,6 +53,10 @@ class ExamplesExtractor
             $exampleValue = $exampleValue === 'true';
         } elseif (is_numeric($exampleValue) && ! $preferString) {
             $exampleValue = floatval($exampleValue);
+
+            if (floor($exampleValue) == $exampleValue) {
+                $exampleValue = intval($exampleValue);
+            }
         }
 
         return $exampleValue;
diff --git a/src/Support/IndexBuilders/Bag.php b/src/Support/IndexBuilders/Bag.php
new file mode 100644
index 0000000..876f0c7
--- /dev/null
+++ b/src/Support/IndexBuilders/Bag.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Dedoc\Scramble\Support\IndexBuilders;
+
+class Bag
+{
+    public function __construct(
+        public array $data = []
+    ) {
+    }
+
+    public function set(string $key, $value)
+    {
+        $this->data[$key] = $value;
+
+        return $this;
+    }
+}
diff --git a/src/Support/IndexBuilders/RequestParametersBuilder.php b/src/Support/IndexBuilders/RequestParametersBuilder.php
new file mode 100644
index 0000000..e02a737
--- /dev/null
+++ b/src/Support/IndexBuilders/RequestParametersBuilder.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace Dedoc\Scramble\Support\IndexBuilders;
+
+use Dedoc\Scramble\Infer\Scope\Scope;
+use Dedoc\Scramble\Support\Generator\MissingExample;
+use Dedoc\Scramble\Support\Generator\Parameter;
+use Dedoc\Scramble\Support\Generator\Schema;
+use Dedoc\Scramble\Support\Generator\TypeTransformer;
+use Dedoc\Scramble\Support\Helpers\ExamplesExtractor;
+use Dedoc\Scramble\Support\Type\BooleanType;
+use Dedoc\Scramble\Support\Type\FloatType;
+use Dedoc\Scramble\Support\Type\IntegerType;
+use Dedoc\Scramble\Support\Type\Literal\LiteralBooleanType;
+use Dedoc\Scramble\Support\Type\Literal\LiteralFloatType;
+use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
+use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
+use Dedoc\Scramble\Support\Type\MixedType;
+use Dedoc\Scramble\Support\Type\ObjectType;
+use Dedoc\Scramble\Support\Type\StringType;
+use Dedoc\Scramble\Support\Type\TypeHelper;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use PhpParser\Comment;
+use PhpParser\Node;
+use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
+
+class RequestParametersBuilder
+{
+    public function __construct(public Bag $bag)
+    {
+    }
+
+    public function afterAnalyzedNode(Scope $scope, Node $node)
+    {
+        if (! $node instanceof Node\Stmt\Expression) {
+            return;
+        }
+
+        if (! $node->expr instanceof Node\Expr\MethodCall) {
+            return;
+        }
+
+        $methodCallNode = $node->expr;
+
+        $varType = $scope->getType($methodCallNode->var);
+
+        if (! $varType->isInstanceOf(Request::class)) {
+            return;
+        }
+
+        if (! $name = $this->getNameNodeValue($scope, $methodCallNode->name)) {
+            return;
+        }
+
+        if (! ($parameterName = TypeHelper::getArgType($scope, $methodCallNode->args, ['key', 0])->value ?? null)) {
+            return;
+        }
+
+        if ($this->shouldIgnoreParameter($node)) {
+            return;
+        }
+
+        $parameter = Parameter::make($parameterName, 'query'/* @todo: this is just a temp solution */);
+
+        [$parameterType, $parameterDefault] = match ($name) {
+            'integer' => $this->makeIntegerParameter($scope, $methodCallNode),
+            'float' => $this->makeFloatParameter($scope, $methodCallNode),
+            'boolean' => $this->makeBooleanParameter($scope, $methodCallNode),
+            'enum' => $this->makeEnumParameter($scope, $methodCallNode),
+            'query' => $this->makeQueryParameter($scope, $methodCallNode, $parameter),
+            'string', 'str', 'get', 'input', 'post' => $this->makeStringParameter($scope, $methodCallNode),
+            default => [null, null],
+        };
+
+        if (! $parameterType) {
+            return;
+        }
+
+        if ($parameterDefaultFromDoc = $this->getParameterDefaultFromPhpDoc($node)) {
+            $parameterDefault = $parameterDefaultFromDoc;
+        }
+
+        $this->checkExplicitParameterPlacementInQuery($node, $parameter);
+
+        $parameter
+            ->description($this->makeDescriptionFromComments($node))
+            ->setSchema(Schema::fromType(
+                app(TypeTransformer::class)->transform($parameterType)
+            ))
+            ->default($parameterDefault ?? new MissingExample);
+
+        $this->bag->set($parameterName, $parameter);
+    }
+
+    private function getNameNodeValue(Scope $scope, Node $nameNode)
+    {
+        if ($nameNode instanceof Node\Identifier) {
+            return $nameNode->name;
+        }
+
+        $type = $scope->getType($nameNode);
+        if (! $type instanceof LiteralStringType) {
+            return null;
+        }
+
+        return $type->value;
+    }
+
+    private function makeIntegerParameter(Scope $scope, Node $node)
+    {
+        return [
+            new IntegerType,
+            TypeHelper::getArgType($scope, $node->args, ['default', 1], new LiteralIntegerType(0))->value ?? null,
+        ];
+    }
+
+    private function makeFloatParameter(Scope $scope, Node $node)
+    {
+        return [
+            new FloatType,
+            TypeHelper::getArgType($scope, $node->args, ['default', 1], new LiteralFloatType(0))->value ?? null,
+        ];
+    }
+
+    private function makeBooleanParameter(Scope $scope, Node $node)
+    {
+        return [
+            new BooleanType,
+            TypeHelper::getArgType($scope, $node->args, ['default', 1], new LiteralBooleanType(false))->value ?? null,
+        ];
+    }
+
+    private function makeStringParameter(Scope $scope, Node $node)
+    {
+        return [
+            new StringType,
+            TypeHelper::getArgType($scope, $node->args, ['default', 1])->value ?? null,
+        ];
+    }
+
+    private function makeEnumParameter(Scope $scope, Node $node)
+    {
+        if (! $className = TypeHelper::getArgType($scope, $node->args, ['default', 1])->value ?? null) {
+            return [null, null];
+        }
+
+        return [
+            new ObjectType($className),
+            null,
+        ];
+    }
+
+    private function makeQueryParameter(Scope $scope, Node $node, Parameter $parameter)
+    {
+        $parameter->setAttribute('isInQuery', true);
+
+        return [
+            new MixedType,
+            TypeHelper::getArgType($scope, $node->args, ['default', 1])->value ?? null,
+        ];
+    }
+
+    private function makeDescriptionFromComments(Node\Stmt\Expression $node)
+    {
+        /*
+         * @todo: consider adding only @param annotation support,
+         * so when description is taken only if comment is marked with @param
+         */
+        if ($phpDoc = $node->getAttribute('parsedPhpDoc')) {
+            return trim($phpDoc->getAttribute('summary').' '.$phpDoc->getAttribute('description'));
+        }
+
+        if ($node->getComments()) {
+            $docText = collect($node->getComments())
+                ->map(fn (Comment $c) => $c->getReformattedText())
+                ->join("\n");
+
+            return (string) Str::of($docText)->replace(['//', ' * ', '/**', '/*', '*/'], '')->trim();
+        }
+
+        return '';
+    }
+
+    private function shouldIgnoreParameter(Node\Stmt\Expression $node)
+    {
+        /** @var PhpDocNode|null $phpDoc */
+        $phpDoc = $node->getAttribute('parsedPhpDoc');
+
+        return (bool) $phpDoc?->getTagsByName('@ignoreParam');
+    }
+
+    private function getParameterDefaultFromPhpDoc(Node\Stmt\Expression $node)
+    {
+        /** @var PhpDocNode|null $phpDoc */
+        $phpDoc = $node->getAttribute('parsedPhpDoc');
+
+        return ExamplesExtractor::make($phpDoc, '@default')->extract()[0] ?? null;
+    }
+
+    private function checkExplicitParameterPlacementInQuery(Node\Stmt\Expression $node, Parameter $parameter)
+    {
+        /** @var PhpDocNode|null $phpDoc */
+        $phpDoc = $node->getAttribute('parsedPhpDoc');
+
+        if ((bool) $phpDoc?->getTagsByName('@query')) {
+            $parameter->setAttribute('isInQuery', true);
+        }
+    }
+}
diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php
index 5333f41..b61d491 100644
--- a/src/Support/OperationExtensions/RequestBodyExtension.php
+++ b/src/Support/OperationExtensions/RequestBodyExtension.php
@@ -26,12 +26,26 @@ class RequestBodyExtension extends OperationExtension
     {
         $description = Str::of($routeInfo->phpDoc()->getAttribute('description'));
 
+        /*
+         * Making sure to analyze the route.
+         * @todo rename the method
+         */
+        $routeInfo->getMethodType();
+
         try {
             $bodyParams = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode());
 
-            $mediaType = $this->getMediaType($operation, $routeInfo, $bodyParams);
+            $allParams = [...$bodyParams, ...array_values($routeInfo->requestParametersFromCalls->data)];
+            [$queryParams, $bodyParams] = collect($allParams)
+                ->partition(function (Parameter $parameter) {
+                    return $parameter->getAttribute('isInQuery');
+                });
+            $queryParams = $queryParams->toArray();
+            $bodyParams = $bodyParams->toArray();
+
+            $mediaType = $this->getMediaType($operation, $routeInfo, $allParams);
 
-            if (count($bodyParams)) {
+            if (count($allParams)) {
                 if (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
                     $operation->addRequestBodyObject(
                         RequestBodyObject::make()->setContent($mediaType, Schema::createFromParameters($bodyParams))
@@ -39,6 +53,7 @@ class RequestBodyExtension extends OperationExtension
                 } else {
                     $operation->addParameters($bodyParams);
                 }
+                $operation->addParameters($queryParams);
             } elseif (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
                 $operation
                     ->addRequestBodyObject(
diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php b/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
index db6c1ca..75eca0e 100644
--- a/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
+++ b/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
@@ -89,6 +89,14 @@ class RulesToParameter
             $parameter->example($examples[0]);
         }
 
+        if ($default = ExamplesExtractor::make($this->docNode, '@default')->extract(preferString: $parameter->schema->type instanceof StringType)) {
+            $parameter->default($default[0]);
+        }
+
+        if ($this->docNode->getTagsByName('@query')) {
+            $parameter->setAttribute('isInQuery', true);
+        }
+
         return $parameter;
     }
 
diff --git a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php
index e9a0e9a..c9551c1 100644
--- a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php
+++ b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php
@@ -32,6 +32,7 @@ class ValidateCallExtractor
             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'
         );
         $validationRules = $callToValidate->args[0] ?? null;
diff --git a/src/Support/RouteInfo.php b/src/Support/RouteInfo.php
index ecc3725..1a09f02 100644
--- a/src/Support/RouteInfo.php
+++ b/src/Support/RouteInfo.php
@@ -6,6 +6,8 @@ use Dedoc\Scramble\Infer;
 use Dedoc\Scramble\Infer\Reflector\MethodReflector;
 use Dedoc\Scramble\Infer\Services\FileParser;
 use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
+use Dedoc\Scramble\Support\IndexBuilders\Bag;
+use Dedoc\Scramble\Support\IndexBuilders\RequestParametersBuilder;
 use Dedoc\Scramble\Support\Type\ArrayItemType_;
 use Dedoc\Scramble\Support\Type\BooleanType;
 use Dedoc\Scramble\Support\Type\FloatType;
@@ -41,11 +43,14 @@ class RouteInfo
 
     private Infer $infer;
 
+    public readonly Bag $requestParametersFromCalls;
+
     public function __construct(Route $route, FileParser $fileParser, Infer $infer)
     {
         $this->route = $route;
         $this->parser = $fileParser;
         $this->infer = $infer;
+        $this->requestParametersFromCalls = new Bag();
     }
 
     public function isClassBased(): bool
@@ -209,6 +214,9 @@ class RouteInfo
             ->getMethodReturnType($this->methodName());
     }
 
+    /**
+     * @todo Maybe better name is needed as this method performs method analysis, indexes building, etc.
+     */
     public function getMethodType(): ?FunctionType
     {
         if (! $this->isClassBased() || ! $this->reflectionMethod()) {
@@ -221,7 +229,9 @@ class RouteInfo
             /*
              * Here the final resolution of the method types may happen.
              */
-            $this->methodType = $def->getMethodDefinition($this->methodName())->type;
+            $this->methodType = $def->getMethodDefinition($this->methodName(), indexBuilders: [
+                new RequestParametersBuilder($this->requestParametersFromCalls),
+            ])->type;
         }
 
         return $this->methodType;
diff --git a/src/Support/Type/Literal/LiteralFloatType.php b/src/Support/Type/Literal/LiteralFloatType.php
new file mode 100644
index 0000000..7474b3e
--- /dev/null
+++ b/src/Support/Type/Literal/LiteralFloatType.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Dedoc\Scramble\Support\Type\Literal;
+
+use Dedoc\Scramble\Support\Type\FloatType;
+
+class LiteralFloatType extends FloatType
+{
+    public float $value;
+
+    public function __construct(float $value)
+    {
+        $this->value = $value;
+    }
+
+    public function toString(): string
+    {
+        return parent::toString()."($this->value)";
+    }
+}


Test patch:
diff --git a/tests/Support/OperationExtensions/RequestBodyExtensionTest.php b/tests/Support/OperationExtensions/RequestBodyExtensionTest.php
index f50674c..ba7fdff 100644
--- a/tests/Support/OperationExtensions/RequestBodyExtensionTest.php
+++ b/tests/Support/OperationExtensions/RequestBodyExtensionTest.php
@@ -80,3 +80,202 @@ class RequestBodyExtensionTest__automaticall_infers_form_data
         $request->validate(['foo' => 'file']);
     }
 }
+
+it('extracts parameters, their defaults, and descriptions from calling request parameters retrieving methods with scalar types', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_scalar_types::class, 'index']);
+    });
+
+    expect($schema = $openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema'])
+        ->toHaveLength(2)
+        ->and($schema['properties'])
+        ->toBe([
+            'count' => [
+                'type' => 'integer',
+                'description' => 'How many things are there.',
+                'default' => 10,
+            ],
+            'weight' => [
+                'type' => 'number',
+                'default' => 0.5,
+            ],
+            'is_foo' => [
+                'type' => 'boolean',
+                'default' => false,
+            ],
+            'name' => [
+                'type' => 'string',
+                'default' => 'John Doe',
+            ],
+        ])
+        ->and($schema['required'] ?? null)
+        ->toBeNull();
+});
+class RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_scalar_types
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        // How many things are there.
+        $request->integer('count', 10);
+
+        $request->float('weight', 0.5);
+
+        $request->boolean('is_foo');
+
+        $request->string('name', 'John Doe');
+    }
+}
+
+it('extracts parameters, their defaults, and descriptions from calling request parameters retrieving methods with enum', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_enum::class, 'index']);
+    });
+
+    expect($properties = $openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema']['properties'])
+        ->toHaveLength(1)
+        ->and($properties['status'])
+        ->toBe([
+            '$ref' => '#/components/schemas/RequestBodyExtensionTest__Status_Params_Extraction',
+        ])
+        ->and($openApiDocument['components']['schemas']['RequestBodyExtensionTest__Status_Params_Extraction'])
+        ->toBe([
+            'type' => 'string',
+            'enum' => [
+                'clubs',
+                'diamonds',
+                'hearts',
+                'spades',
+            ],
+            'title' => 'RequestBodyExtensionTest__Status_Params_Extraction',
+        ]);
+});
+class RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_enum
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        $request->enum('status', RequestBodyExtensionTest__Status_Params_Extraction::class);
+    }
+}
+enum RequestBodyExtensionTest__Status_Params_Extraction: string
+{
+    case Clubs = 'clubs';
+    case Diamonds = 'diamonds';
+    case Hearts = 'hearts';
+    case Spades = 'spades';
+}
+
+it('extracts parameters, their defaults, and descriptions from calling request parameters retrieving methods with query', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_query::class, 'index']);
+    });
+
+    expect($openApiDocument['paths']['/test']['post']['parameters'])
+        ->toHaveLength(1)
+        ->toBe([[
+            'name' => 'in_query',
+            'in' => 'query',
+            'schema' => [
+                'type' => 'string',
+            ],
+            'default' => 'foo',
+        ]]);
+});
+class RequestBodyExtensionTest__extracts_parameters_from_retrieving_methods_with_query
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        $request->query('in_query', 'foo');
+    }
+}
+
+it('ignores parameter with @ignoreParam doc', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__ignores_parameter_with_ignore_param_doc::class, 'index']);
+    });
+
+    expect($openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema']['properties'] ?? [])
+        ->toHaveLength(0);
+});
+class RequestBodyExtensionTest__ignores_parameter_with_ignore_param_doc
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        /** @ignoreParam */
+        $request->integer('foo', 10);
+    }
+}
+
+it('uses and overrides default param value when it is provided manually in doc', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__uses_and_overrides_default_param_value_when_it_is_provided_manually_in_doc::class, 'index']);
+    });
+
+    expect($openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema']['properties']['foo']['default'])
+        ->toBe(15);
+});
+class RequestBodyExtensionTest__uses_and_overrides_default_param_value_when_it_is_provided_manually_in_doc
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        /** @default 15 */
+        $request->integer('foo', 10);
+    }
+}
+
+it('allows explicitly specifying parameter placement in query manually in doc', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__allows_explicitly_specifying_parameter_placement_in_query_manually_in_doc::class, 'index']);
+    });
+
+    expect($openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema']['properties'] ?? [])
+        ->toBeEmpty()
+        ->and($openApiDocument['paths']['/test']['post']['parameters'])
+        ->toBe([[
+            'name' => 'foo',
+            'in' => 'query',
+            'schema' => ['type' => 'integer'],
+            'default' => 10,
+        ]]);
+});
+class RequestBodyExtensionTest__allows_explicitly_specifying_parameter_placement_in_query_manually_in_doc
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        /** @query */
+        $request->integer('foo', 10);
+    }
+}
+
+it('allows specifying query position and default for params inferred from validation rules using validate method', function () {
+    $openApiDocument = generateForRoute(function () {
+        return RouteFacade::post('api/test', [RequestBodyExtensionTest__allows_specifying_query_position_and_default_for_params_inferred_from_validation_rules_using_validate_method::class, 'index']);
+    });
+
+    expect($openApiDocument['paths']['/test']['post']['requestBody']['content']['application/json']['schema']['properties'])
+        ->toBe([
+            'per_page' => [
+                'type' => 'integer',
+                'default' => 10,
+            ],
+        ])
+        ->and($openApiDocument['paths']['/test']['post']['parameters'])
+        ->toBe([[
+            'name' => 'all',
+            'in' => 'query',
+            'schema' => [
+                'type' => 'boolean',
+            ],
+        ]]);
+});
+class RequestBodyExtensionTest__allows_specifying_query_position_and_default_for_params_inferred_from_validation_rules_using_validate_method
+{
+    public function index(Illuminate\Http\Request $request)
+    {
+        $request->validate([
+            /** @default 10 */
+            'per_page' => 'integer',
+            /** @query */
+            'all' => 'boolean',
+        ]);
+    }
+}