Skip to content

Commit c378484

Browse files
committed
parsing logic
1 parent f14d274 commit c378484

15 files changed

+750
-14
lines changed

src/Commands/Process.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ protected function extract(array $matches): array
7171
$controller = new ReflectionClass($controllerName);
7272
$method = u::getReflectedRouteMethod([$controllerName, $methodName]);
7373
$endpointData = $extractor->processRoute($route, $method, $controller);
74-
ray($endpointData);
74+
7575
c::success('Processed route: ' . c::getRouteRepresentation($route));
7676
} catch (\Exception $exception) {
7777
$this->encounteredErrors = true;

src/Extractor.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,42 @@ class Extractor
1414
{
1515
use FindsFormRequestForMethod;
1616

17+
protected array $tags = [];
18+
1719
public function __construct(public DocumentationConfig $config)
1820
{
1921
}
2022

2123
public function processRoute(Route $route, ReflectionFunctionAbstract $method, ?ReflectionClass $controller): array
2224
{
23-
$tags = [];
2425
if ($formRequestClass = $this->getFormRequestReflectionClass($method)) {
25-
$tagsFromFormRequest = $this->parseData(new DocBlock($formRequestClass->getDocComment()));
26-
$tags[$formRequestClass] = $tagsFromFormRequest;
26+
if (!isset($this->tags[$formRequestClass->name])) {
27+
$tagsFromFormRequest = $this->parseData(new DocBlock($formRequestClass->getDocComment()), 'formrequest');
28+
$this->tags[$formRequestClass->getFileName()] = $tagsFromFormRequest;
29+
}
2730
}
2831

29-
$tags[$controller->name] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['class']);
32+
if ($controller->name === "Closure") {
33+
$this->tags[$method->getFileName() . '|' . $method->getStartLine()] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['method'], 'method');
34+
} else {
35+
$this->tags[$controller->getFileName()] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['class'], 'class');
3036

31-
$tags[$controller->name.'/'.$method->name] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['method']);
37+
$this->tags[$controller->getFileName() . '|' . $method->name] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['method'], 'method');
38+
}
3239

33-
return $tags;
40+
return $this->tags;
3441
}
3542

36-
protected function parseData(DocBlock $docblock)
43+
protected function parseData(DocBlock $docblock, string $scope)
3744
{
3845
/** @var \Mpociot\Reflection\DocBlock\Tag[] $tags */
3946
$tags = $docblock->getTags() ?? [];
4047

41-
$parsed = [];
42-
foreach ($tags as $tag) {
43-
$parsed[] = match(strtolower($tag->getName())) {
44-
'bodyparam' => $tag->getContent()
45-
};
46-
}
48+
$parsed = collect($tags)
49+
->map(fn($tag) => TagParser::parse($tag, $tags, $scope, $docblock))
50+
->flatten(1);
4751

52+
if ($parsed->count()) ray($parsed->all());
4853
return $parsed;
4954
}
5055
}

src/TagParser.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes;
4+
5+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\ApiResourceTagParser;
6+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\AuthTagParser;
7+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\GroupTagParser;
8+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\HeaderTagParser;
9+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\BodyParamTagParser;
10+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\QueryParamTagParser;
11+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\ResponseFieldTagParser;
12+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\ResponseFileTagParser;
13+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\ResponseTagParser;
14+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\SubgroupTagParser;
15+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\TransformerTagParser;
16+
use Knuckles\Scribe\Docblock2Attributes\TagParsers\UrlParamTagParser;
17+
use Mpociot\Reflection\DocBlock;
18+
use Mpociot\Reflection\DocBlock\Tag;
19+
20+
class TagParser
21+
{
22+
protected const TAG_PARSERS = [
23+
'header' => HeaderTagParser::class,
24+
'bodyparam' => BodyParamTagParser::class,
25+
'queryparam' => QueryParamTagParser::class,
26+
'urlparam' => UrlParamTagParser::class,
27+
'responsefield' => ResponseFieldTagParser::class,
28+
'response' => ResponseTagParser::class,
29+
'responsefile' => ResponseFileTagParser::class,
30+
'apiresource' => ApiResourceTagParser::class,
31+
'apiresourcecollection' => ApiResourceTagParser::class,
32+
'transformer' => TransformerTagParser::class,
33+
'transformercollection' => TransformerTagParser::class,
34+
'authenticated' => AuthTagParser::class,
35+
'unauthenticated' => AuthTagParser::class,
36+
'group' => GroupTagParser::class,
37+
'subgroup' => SubgroupTagParser::class,
38+
];
39+
40+
public static function parse(Tag $tag, array $allTags, string $scope, DocBlock $docBlock)
41+
{
42+
$name = strtolower($tag->getName());
43+
$parserClass = self::TAG_PARSERS[$name] ?? null;
44+
45+
if (!$parserClass) {
46+
return [];
47+
}
48+
49+
$parser = new $parserClass(trim($tag->getContent()), $allTags, $tag, $scope, $docBlock);
50+
51+
return $parser->parse();
52+
}
53+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes\TagParsers;
4+
5+
use Illuminate\Support\Arr;
6+
use Knuckles\Scribe\Extracting\ParamHelpers;
7+
use Knuckles\Scribe\Tools\AnnotationParser as a;
8+
use Knuckles\Scribe\Tools\Utils;
9+
use Mpociot\Reflection\DocBlock\Tag;
10+
11+
class ApiResourceTagParser
12+
{
13+
use ParamHelpers;
14+
15+
public function __construct(protected string $tagContent, protected array $allTags, protected Tag $tag)
16+
{
17+
}
18+
19+
public function parse()
20+
{
21+
[$statusCode, $description, $apiResourceClass, $isCollection] = $this->getStatusCodeAndApiResourceClass();
22+
[$modelClass, $factoryStates, $relations, $pagination] = $this->getClassToBeTransformedAndAttributes();
23+
$additionalData = $this->getAdditionalData();
24+
25+
if (empty($pagination)) {
26+
[$simplePaginate, $paginate] = [null, null];
27+
} else {
28+
if (($pagination[1] ?? null) === 'simple') {
29+
[$simplePaginate, $paginate] = [$pagination[0], null];
30+
} else {
31+
[$paginate, $simplePaginate] = [$pagination[0], null];
32+
33+
}
34+
}
35+
36+
return [
37+
[
38+
'type' => 'apiresource',
39+
'data' => [
40+
'status' => (int)$statusCode,
41+
'description' => $description,
42+
'name' => $apiResourceClass,
43+
'model' => $modelClass,
44+
'collection' => $isCollection,
45+
'factoryStates' => $factoryStates,
46+
'with' => $relations,
47+
'additionalData' => $additionalData,
48+
'paginate' => $paginate,
49+
'simplePaginate' => $simplePaginate,
50+
],
51+
],
52+
];
53+
}
54+
55+
private function getStatusCodeAndApiResourceClass(): array
56+
{
57+
preg_match('/^(\d{3})?\s?([\s\S]*)$/', $this->tagContent, $result);
58+
59+
$status = $result[1] ?: 200;
60+
$content = $result[2];
61+
62+
['attributes' => $attributes, 'content' => $content] = a::parseIntoContentAndAttributes($content, ['status', 'scenario']);
63+
64+
$status = $attributes['status'] ?: $status;
65+
$apiResourceClass = $content;
66+
$description = ($status && $attributes['scenario']) ? "$status, {$attributes['scenario']}" : "";
67+
68+
$isCollection = strtolower($this->tag->getName()) == 'apiresourcecollection';
69+
return [(int)$status, $description, $apiResourceClass, $isCollection];
70+
}
71+
72+
private function getClassToBeTransformedAndAttributes(): array
73+
{
74+
$modelTag = Arr::first(Utils::filterDocBlockTags($this->allTags, 'apiresourcemodel'));
75+
76+
$modelClass = null;
77+
$states = [];
78+
$relations = [];
79+
$pagination = [];
80+
81+
if ($modelTag) {
82+
['content' => $modelClass, 'attributes' => $attributes] = a::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with', 'paginate']);
83+
$states = $attributes['states'] ? explode(',', $attributes['states']) : [];
84+
$relations = $attributes['with'] ? explode(',', $attributes['with']) : [];
85+
$pagination = $attributes['paginate'] ? explode(',', $attributes['paginate']) : [];
86+
}
87+
88+
return [$modelClass, $states, $relations, $pagination];
89+
}
90+
91+
private function getAdditionalData(): array
92+
{
93+
$tag = Arr::first(Utils::filterDocBlockTags($this->allTags, 'apiresourceadditional'));
94+
return $tag ? a::parseIntoAttributes($tag->getContent()) : [];
95+
}
96+
}

src/TagParsers/AuthTagParser.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes\TagParsers;
4+
5+
use Illuminate\Support\Arr;
6+
use Knuckles\Scribe\Extracting\ParamHelpers;
7+
use Knuckles\Scribe\Tools\AnnotationParser as a;
8+
use Knuckles\Scribe\Tools\Utils;
9+
use Mpociot\Reflection\DocBlock\Tag;
10+
11+
class AuthTagParser
12+
{
13+
use ParamHelpers;
14+
15+
public function __construct(protected string $tagContent, protected array $allTags, protected Tag $tag)
16+
{
17+
}
18+
19+
public function parse()
20+
{
21+
return [
22+
[
23+
'type' => strtolower($this->tag->getName()) === 'authenticated'
24+
? 'authenticated' : 'unauthenticated',
25+
],
26+
];
27+
}
28+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes\TagParsers;
4+
5+
use Knuckles\Scribe\Extracting\ParamHelpers;
6+
7+
class BodyParamTagParser
8+
{
9+
use ParamHelpers;
10+
11+
public function __construct(protected string $tagContent)
12+
{
13+
}
14+
15+
public function parse()
16+
{
17+
// Format:
18+
// @bodyParam <name> <type> <"required" (optional)> <description>
19+
// Examples:
20+
// @bodyParam text string required The text.
21+
// @bodyParam user_id integer The ID of the user.
22+
preg_match('/(.+?)\s+(.+?)\s+(required\s+)?([\s\S]*)/', $this->tagContent, $parsedContent);
23+
24+
if (empty($parsedContent)) {
25+
// This means only name and type were supplied
26+
[$name, $type] = preg_split('/\s+/', $this->tagContent);
27+
$required = false;
28+
$description = '';
29+
} else {
30+
[$_, $name, $type, $required, $description] = $parsedContent;
31+
$description = trim(str_replace(['No-example.', 'No-example'], '', $description));
32+
if ($description == 'required') {
33+
$required = $description;
34+
$description = '';
35+
}
36+
$required = trim($required) === 'required';
37+
}
38+
39+
$type = static::normalizeTypeName($type);
40+
[$description, $example] = $this->parseExampleFromParamDescription($description, $type);
41+
42+
$noExample = $this->shouldExcludeExample($this->tagContent);
43+
if ($noExample) {
44+
$example = 'No-example';
45+
}
46+
47+
return [
48+
[
49+
'type' => 'bodyparam',
50+
'data' => compact('name', 'type', 'description', 'required', 'example'),
51+
],
52+
];
53+
}
54+
}

src/TagParsers/GroupTagParser.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes\TagParsers;
4+
5+
use Illuminate\Support\Arr;
6+
use Knuckles\Scribe\Extracting\ParamHelpers;
7+
use Knuckles\Scribe\Tools\AnnotationParser as a;
8+
use Knuckles\Scribe\Tools\Utils;
9+
use Mpociot\Reflection\DocBlock;
10+
use Mpociot\Reflection\DocBlock\Tag;
11+
12+
class GroupTagParser
13+
{
14+
use ParamHelpers;
15+
16+
public function __construct(
17+
protected string $tagContent, protected array $allTags, protected Tag $tag,
18+
protected string $scope, protected DocBlock $docBlock
19+
)
20+
{
21+
}
22+
23+
public function parse()
24+
{
25+
$endpointGroupParts = explode("\n", $this->tagContent);
26+
$endpointGroupName = array_shift($endpointGroupParts);
27+
$endpointGroupDescription = trim(implode("\n", $endpointGroupParts));
28+
29+
// If the endpoint has no title (the methodDocBlock's "short description"),
30+
// we'll assume the endpointGroupDescription is actually the title
31+
// Something like this:
32+
// /**
33+
// * Fetch cars. <-- This is endpoint title.
34+
// * @group Cars <-- This is group name.
35+
// * APIs for cars. <-- This is group description (not required).
36+
// **/
37+
// VS
38+
// /**
39+
// * @group Cars <-- This is group name.
40+
// * Fetch cars. <-- This is endpoint title, NOT group description.
41+
// **/
42+
43+
if ($this->scope == 'method' && empty($this->docBlock->getShortDescription())) {
44+
return [
45+
[
46+
'type' => 'group',
47+
'data' => [
48+
'name' => $endpointGroupName,
49+
'description' => '',
50+
],
51+
],
52+
[
53+
'type' => 'endpoint',
54+
'data' => [
55+
'name' => $endpointGroupDescription,
56+
'description' => '',
57+
],
58+
],
59+
];
60+
}
61+
62+
63+
return [
64+
[
65+
'type' => 'group',
66+
'data' => [
67+
'name' => $endpointGroupName,
68+
'description' => $endpointGroupDescription,
69+
],
70+
],
71+
];
72+
}
73+
}

0 commit comments

Comments
 (0)