Skip to content

Commit 2dd4b4e

Browse files
authored
Merge pull request #15 from ensi-platform/task-93471
#93471
2 parents 1f492d9 + 07de1ed commit 2dd4b4e

File tree

11 files changed

+378
-20
lines changed

11 files changed

+378
-20
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,24 @@ Enums generator does NOT support `allOf`, `anyOf` and `oneOf` at the moment.
9999
Generates Pest test file for each `x-lg-handler`
100100
You can exclude oas3 path from test generation using `x-lg-skip-tests-generation: true`.
101101
If a test file already exists it is NOT overriden.
102-
Test file class IS meant to be modified after generation.
102+
Test file class IS meant to be modified after generation.
103+
104+
### 'resources' => ResourcesGenerator::class
105+
106+
Generates Resource file for `x-lg-handler`
107+
Resource properties are generated relative to field in response, which can be set in the config
108+
```php
109+
'resources' => [
110+
'response_key' => 'data'
111+
],
112+
```
113+
You can also specify `response_key` for resource: add `x-lg-resource-response-key: data` in object.
114+
When specifying `response_key`, you can use the "dot" syntax to specify nesting, for example `data.field`
115+
You can exclude resource generation using `x-lg-skip-resource-generation: true` in route.
116+
You can rename resource Class using `x-lg-resource-class-name: FooResource` in object.
117+
If a resource file already exists it is NOT overriden.
118+
Resource file contains a set of fields according to the specification.
119+
You also need to specify mixin DocBlock to autocomplete resource.
103120

104121
## Contributing
105122

config/openapi-server-generator.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Ensi\LaravelOpenApiServerGenerator\Generators\EnumsGenerator;
55
use Ensi\LaravelOpenApiServerGenerator\Generators\PestTestsGenerator;
66
use Ensi\LaravelOpenApiServerGenerator\Generators\RequestsGenerator;
7+
use Ensi\LaravelOpenApiServerGenerator\Generators\ResourcesGenerator;
78
use Ensi\LaravelOpenApiServerGenerator\Generators\RoutesGenerator;
89

910
return [
@@ -33,6 +34,9 @@
3334
'pest_tests' => [
3435
'namespace' => ["Controllers" => "Tests"],
3536
],
37+
'resources' => [
38+
'response_key' => 'data',
39+
],
3640
],
3741
],
3842

@@ -52,6 +56,7 @@
5256
'requests' => RequestsGenerator::class,
5357
'routes' => RoutesGenerator::class,
5458
'pest_tests' => PestTestsGenerator::class,
59+
'resources' => ResourcesGenerator::class,
5560
],
5661

5762
/**
@@ -63,6 +68,7 @@
6368
'requests',
6469
'routes',
6570
'pest_tests',
71+
'resources',
6672
],
6773

6874
'extra_templates_path' => resource_path('openapi-server-generator/templates'),

src/Generators/BaseGenerator.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,32 @@ protected function trimPath(string $path): string
5353
protected function getReplacedNamespace(?string $baseNamespace, string $replaceFromNamespace, string $replaceToNamespace): ?string
5454
{
5555
if ($baseNamespace) {
56-
if (!str_contains($baseNamespace, $replaceFromNamespace)) {
57-
throw new RuntimeException("Can't replace namespace");
58-
}
56+
return $this->replace($baseNamespace, $replaceFromNamespace, $replaceToNamespace)
57+
?? throw new RuntimeException("Can't replace namespace");
58+
}
5959

60-
return str_replace($replaceFromNamespace, $replaceToNamespace, $baseNamespace);
60+
return null;
61+
}
62+
63+
protected function getReplacedClassName(?string $baseClassName, string $replaceFromClassName, string $replaceToClassName): ?string
64+
{
65+
if ($baseClassName) {
66+
return $this->replace($baseClassName, $replaceFromClassName, $replaceToClassName)
67+
?? throw new RuntimeException("Can't replace class name");
6168
}
6269

6370
return null;
6471
}
6572

73+
protected function replace(string $base, string $from, string $to): ?string
74+
{
75+
if (!str_contains($base, $from)) {
76+
return null;
77+
}
78+
79+
return str_replace($from, $to, $base);
80+
}
81+
6682
protected function getNamespacedFilePath(string $fileName, ?string $namespace): string
6783
{
6884
$toDir = $this->psr4PathConverter->namespaceToPath($namespace);
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Generators;
4+
5+
use cebe\openapi\SpecObjectInterface;
6+
use RuntimeException;
7+
use stdClass;
8+
9+
class ResourcesGenerator extends BaseGenerator implements GeneratorInterface
10+
{
11+
public function generate(SpecObjectInterface $specObject): void
12+
{
13+
$resources = $this->extractResources($specObject);
14+
$this->createResourcesFiles($resources, $this->templatesManager->getTemplate('Resource.template'));
15+
}
16+
17+
protected function extractResources(SpecObjectInterface $specObject): array
18+
{
19+
$replaceFrom = 'Controller';
20+
$replaceTo = 'Resource';
21+
22+
$openApiData = $specObject->getSerializableData();
23+
24+
$resources = [];
25+
$paths = $openApiData->paths ?: [];
26+
foreach ($paths as $routes) {
27+
foreach ($routes as $route) {
28+
if (!empty($route->{'x-lg-skip-resource-generation'})) {
29+
continue;
30+
}
31+
32+
if (empty($route->{'x-lg-handler'})) {
33+
continue;
34+
}
35+
36+
$response = $route->responses->{201} ?? $route->responses->{200} ?? null;
37+
if (!$response) {
38+
continue;
39+
}
40+
41+
$responseSchema = $response->content?->{'application/json'}?->schema ?? null;
42+
if (!$responseSchema) {
43+
continue;
44+
}
45+
46+
$handler = $this->routeHandlerParser->parse($route->{'x-lg-handler'});
47+
48+
try {
49+
$namespace = $this->getReplacedNamespace($handler->namespace, $replaceFrom, $replaceTo);
50+
$className = $responseSchema->{'x-lg-resource-class-name'} ?? $this->getReplacedClassName($handler->class, $replaceFrom, $replaceTo);
51+
} catch (RuntimeException) {
52+
continue;
53+
}
54+
55+
if (isset($resources["$namespace\\$className"])) {
56+
continue;
57+
}
58+
59+
$responseData = $responseSchema;
60+
61+
$responseKey = $responseSchema->{'x-lg-resource-response-key'} ??
62+
$this->options['resources']['response_key'] ??
63+
null;
64+
if ($responseKey) {
65+
$responseKeyParts = explode('.', $responseKey);
66+
foreach ($responseKeyParts as $responseKeyPart) {
67+
$flag = false;
68+
do_with_all_of($responseData, function (stdClass $p) use (&$responseData, &$flag, $responseKeyPart) {
69+
if (std_object_has($p, 'properties')) {
70+
if (std_object_has($p->properties, $responseKeyPart)) {
71+
$responseData = $p->properties->$responseKeyPart;
72+
$flag = true;
73+
}
74+
}
75+
});
76+
if (!$flag) {
77+
$responseData = null;
78+
79+
break;
80+
}
81+
}
82+
}
83+
84+
if (!$responseData) {
85+
continue;
86+
}
87+
88+
$properties = $this->convertToString($this->getProperties($responseData));
89+
90+
if (empty($properties)) {
91+
continue;
92+
}
93+
94+
$resources["$namespace\\$className"] = compact('className', 'namespace', 'properties');
95+
}
96+
}
97+
98+
return $resources;
99+
}
100+
101+
protected function createResourcesFiles(array $resources, string $template): void
102+
{
103+
foreach ($resources as ['className' => $className, 'namespace' => $namespace, 'properties' => $properties]) {
104+
$filePath = $this->getNamespacedFilePath($className, $namespace);
105+
if ($this->filesystem->exists($filePath)) {
106+
continue;
107+
}
108+
109+
$this->filesystem->put(
110+
$filePath,
111+
$this->replacePlaceholders($template, [
112+
'{{ namespace }}' => $namespace,
113+
'{{ className }}' => $className,
114+
'{{ properties }}' => $properties,
115+
])
116+
);
117+
}
118+
}
119+
120+
private function getProperties(stdClass $object): array
121+
{
122+
$properties = [];
123+
124+
do_with_all_of($object, function (stdClass $p) use (&$properties) {
125+
if (std_object_has($p, 'properties')) {
126+
$properties = array_merge($properties, array_keys(get_object_vars($p->properties)));
127+
}
128+
129+
if (std_object_has($p, 'items')) {
130+
$properties = array_merge($properties, $this->getProperties($p->items));
131+
}
132+
});
133+
134+
return $properties;
135+
}
136+
137+
private function convertToString(array $properties): string
138+
{
139+
$propertyStrings = [];
140+
141+
foreach ($properties as $property) {
142+
$propertyStrings[] = "'$property' => \$this->$property,";
143+
}
144+
145+
return implode("\n ", $propertyStrings);
146+
}
147+
}

src/helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function do_with_all_of(stdClass $object, callable $fn): void
1515
$fn($object);
1616
if (std_object_has($object, 'allOf')) {
1717
foreach ($object->allOf as $allOfItem) {
18-
$fn($allOfItem);
18+
do_with_all_of($allOfItem, $fn);
1919
}
2020
}
2121
}

templates/Resource.template

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace {{ namespace }};
4+
5+
use Illuminate\Http\Resources\Json\JsonResource;
6+
use Illuminate\Http\Request;
7+
8+
/**
9+
* Class {{ className }}
10+
* @package {{ namespace }}
11+
*
12+
* @mixin todo
13+
*/
14+
class {{ className }} extends JsonResource
15+
{
16+
/**
17+
* Transform the resource into an array.
18+
*
19+
* @param Request $request
20+
* @return array
21+
*/
22+
public function toArray($request): array
23+
{
24+
// todo
25+
return [
26+
{{ properties }}
27+
];
28+
}
29+
}

tests/GenerateServerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
$this->makeFilePath('/app/Http/Requests/TestFullGenerateRequest.php'),
4242
$this->makeFilePath('/app/Http/Tests/ResourcesComponentTest.php'),
4343
$this->makeFilePath('/app/Http/Requests/TestFooRenameRequest.php'),
44+
4445
$this->makeFilePath('/app/Http/Requests/LaravelValidationsApplicationJsonRequest.php'),
4546
$this->makeFilePath('/app/Http/Requests/LaravelValidationsMultipartFormDataRequest.php'),
4647
$this->makeFilePath('/app/Http/Requests/LaravelValidationsNonAvailableContentTypeRequest.php'),
@@ -53,6 +54,10 @@
5354

5455
$this->makeFilePath('/app/Http/ApiV1/OpenApiGenerated/Enums/TestIntegerEnum.php'),
5556
$this->makeFilePath('/app/Http/ApiV1/OpenApiGenerated/Enums/TestStringEnum.php'),
57+
58+
$this->makeFilePath('/app/Http/Resources/ResourcesResource.php'),
59+
$this->makeFilePath('/app/Http/Resources/ResourcesDataDataResource.php'),
60+
$this->makeFilePath('/app/Http/Resources/ResourceRootResource.php'),
5661
], $putFiles);
5762
});
5863

tests/ResourceGenerationTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use Ensi\LaravelOpenApiServerGenerator\Commands\GenerateServer;
4+
use Ensi\LaravelOpenApiServerGenerator\Tests\TestCase;
5+
use Illuminate\Filesystem\Filesystem;
6+
use Illuminate\Support\Facades\Config;
7+
use function Pest\Laravel\artisan;
8+
use function PHPUnit\Framework\assertEqualsCanonicalizing;
9+
10+
test('Test allOff and ref keywords', function () {
11+
/** @var TestCase $this */
12+
$mapping = Config::get('openapi-server-generator.api_docs_mappings');
13+
$mappingValue = current($mapping);
14+
$mapping = [$this->makeFilePath(__DIR__ . '/resources/index.yaml') => $mappingValue];
15+
Config::set('openapi-server-generator.api_docs_mappings', $mapping);
16+
17+
$filesystem = $this->mock(Filesystem::class);
18+
$filesystem->shouldReceive('exists')->andReturn(false);
19+
$filesystem->shouldReceive('get')->withArgs(function ($path) {
20+
return (bool)strstr($path, '.template');
21+
})->andReturnUsing(function ($path) {
22+
return file_get_contents($path);
23+
});
24+
$filesystem->shouldReceive('cleanDirectory', 'ensureDirectoryExists');
25+
$resources = [];
26+
$filesystem->shouldReceive('put')->withArgs(function ($path, $content) use (&$resources) {
27+
if (str_contains($path, 'Resource.php')) {
28+
$resources[pathinfo($path, PATHINFO_BASENAME)] = $content;
29+
}
30+
31+
return true;
32+
});
33+
34+
35+
artisan(GenerateServer::class);
36+
37+
// С помощью регулярки достаем все выражения в кавычках
38+
foreach ($resources as $key => $content) {
39+
$matches = [];
40+
preg_match_all('~[\'](.*)[\']~', $content, $matches);
41+
$resources[$key] = $matches[1];
42+
}
43+
44+
assertEqualsCanonicalizing(['foo', 'bar'], $resources['ResourcesResource.php']);
45+
assertEqualsCanonicalizing(['foo', 'bar'], $resources['ResourcesDataDataResource.php']);
46+
assertEqualsCanonicalizing(['data'], $resources['ResourceRootResource.php']);
47+
});

0 commit comments

Comments
 (0)