Skip to content

Commit 5ffd96a

Browse files
authored
Merge pull request #22 from ensi-platform/task-103127
#103127
2 parents 2dd4b4e + a016296 commit 5ffd96a

File tree

8 files changed

+234
-2
lines changed

8 files changed

+234
-2
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,23 @@ You can also specify `response_key` for resource: add `x-lg-resource-response-ke
114114
When specifying `response_key`, you can use the "dot" syntax to specify nesting, for example `data.field`
115115
You can exclude resource generation using `x-lg-skip-resource-generation: true` in route.
116116
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.
117+
If a resource file already exists it is NOT overridden.
118118
Resource file contains a set of fields according to the specification.
119119
You also need to specify mixin DocBlock to autocomplete resource.
120120

121+
### 'policies' => PoliciesGenerator::class
122+
123+
Generates Laravel Policies for routes.
124+
Destination must be configured with array as namespace instead of string. E.g:
125+
```php
126+
'policies' => [
127+
'namespace' => ["Controllers" => "Policies"]
128+
],
129+
```
130+
* The path must contain a 403 response or the policy will not be generated.
131+
* You can exclude policy generation using `x-lg-skip-policy-generation: true` in route.
132+
* If a policy file already exists it is NOT overridden.
133+
121134
## Contributing
122135

123136
Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.

config/openapi-server-generator.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Ensi\LaravelOpenApiServerGenerator\Generators\ControllersGenerator;
44
use Ensi\LaravelOpenApiServerGenerator\Generators\EnumsGenerator;
55
use Ensi\LaravelOpenApiServerGenerator\Generators\PestTestsGenerator;
6+
use Ensi\LaravelOpenApiServerGenerator\Generators\PoliciesGenerator;
67
use Ensi\LaravelOpenApiServerGenerator\Generators\RequestsGenerator;
78
use Ensi\LaravelOpenApiServerGenerator\Generators\ResourcesGenerator;
89
use Ensi\LaravelOpenApiServerGenerator\Generators\RoutesGenerator;
@@ -37,6 +38,9 @@
3738
'resources' => [
3839
'response_key' => 'data',
3940
],
41+
'policies' => [
42+
'namespace' => ["Controllers" => "Policies"],
43+
],
4044
],
4145
],
4246

@@ -57,6 +61,7 @@
5761
'routes' => RoutesGenerator::class,
5862
'pest_tests' => PestTestsGenerator::class,
5963
'resources' => ResourcesGenerator::class,
64+
'policies' => PoliciesGenerator::class,
6065
],
6166

6267
/**
@@ -69,6 +74,7 @@
6974
'routes',
7075
'pest_tests',
7176
'resources',
77+
'policies',
7278
],
7379

7480
'extra_templates_path' => resource_path('openapi-server-generator/templates'),
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Generators;
4+
5+
use cebe\openapi\SpecObjectInterface;
6+
use Ensi\LaravelOpenApiServerGenerator\DTO\ParsedRouteHandler;
7+
use InvalidArgumentException;
8+
use RuntimeException;
9+
use stdClass;
10+
11+
class PoliciesGenerator extends BaseGenerator implements GeneratorInterface
12+
{
13+
public function generate(SpecObjectInterface $specObject): void
14+
{
15+
$namespaceData = $this->options['policies']['namespace'] ?? null;
16+
if (!is_array($namespaceData)) {
17+
throw new InvalidArgumentException("PoliciesGenerator must be configured with array as 'namespace'");
18+
}
19+
20+
$policies = $this->extractPolicies($specObject, $namespaceData);
21+
$this->createPoliciesFiles($policies, $this->templatesManager->getTemplate('Policy.template'));
22+
}
23+
24+
protected function extractPolicies(SpecObjectInterface $specObject, array $namespaceData): array
25+
{
26+
$replaceFromNamespace = array_keys($namespaceData)[0];
27+
$replaceToNamespace = array_values($namespaceData)[0];
28+
29+
$openApiData = $specObject->getSerializableData();
30+
31+
$policies = [];
32+
$paths = $openApiData->paths ?: [];
33+
foreach ($paths as $routes) {
34+
foreach ($routes as $route) {
35+
if (!$this->routeValidation($route)) {
36+
continue;
37+
}
38+
39+
$handler = $this->routeHandlerParser->parse($route->{'x-lg-handler'});
40+
if (!$this->handlerValidation($handler)) {
41+
continue;
42+
}
43+
44+
try {
45+
$namespace = $this->getReplacedNamespace(
46+
$handler->namespace,
47+
$replaceFromNamespace,
48+
$replaceToNamespace
49+
);
50+
} catch (RuntimeException) {
51+
continue;
52+
}
53+
54+
$className = $handler->class . 'Policy';
55+
$methods = [$handler->method];
56+
57+
if (isset($policies["$namespace\\$className"])) {
58+
$policies["$namespace\\$className"]['methods'][] = $methods[0];
59+
} else {
60+
$policies["$namespace\\$className"] = compact('className', 'namespace', 'methods');
61+
}
62+
}
63+
}
64+
65+
return $policies;
66+
}
67+
68+
protected function createPoliciesFiles(array $policies, string $template): void
69+
{
70+
foreach ($policies as ['className' => $className, 'namespace' => $namespace, 'methods' => $methods]) {
71+
$filePath = $this->getNamespacedFilePath($className, $namespace);
72+
if ($this->filesystem->exists($filePath)) {
73+
continue;
74+
}
75+
76+
$this->filesystem->put(
77+
$filePath,
78+
$this->replacePlaceholders($template, [
79+
'{{ namespace }}' => $namespace,
80+
'{{ className }}' => $className,
81+
'{{ methods }}' => $this->convertMethodsToString($methods),
82+
])
83+
);
84+
}
85+
}
86+
87+
private function routeValidation(stdClass $route): bool
88+
{
89+
return match (true) {
90+
!empty($route->{'x-lg-skip-policy-generation'}),
91+
empty($route->{'x-lg-handler'}),
92+
empty($route->responses->{403}) => false,
93+
default => true
94+
};
95+
}
96+
97+
private function handlerValidation(ParsedRouteHandler $handler): bool
98+
{
99+
return match (true) {
100+
empty($handler->namespace),
101+
empty($handler->class),
102+
empty($handler->method) => false,
103+
default => true
104+
};
105+
}
106+
107+
private function convertMethodsToString(array $methods): string
108+
{
109+
$methodsStrings = [];
110+
111+
foreach ($methods as $method) {
112+
$methodsStrings[] = $this->replacePlaceholders(
113+
$this->templatesManager->getTemplate('PolicyGate.template'),
114+
['{{ method }}' => $method]
115+
);
116+
}
117+
118+
return implode("\n\n ", $methodsStrings);
119+
}
120+
}

templates/Policy.template

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace {{ namespace }};
4+
5+
use App\Models\User;
6+
use Illuminate\Auth\Access\HandlesAuthorization;
7+
use Illuminate\Auth\Access\Response;
8+
9+
class {{ className }}
10+
{
11+
use HandlesAuthorization;
12+
13+
{{ methods }}
14+
}

templates/PolicyGate.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public function {{ method }}(User $user): Response
2+
{
3+
return Response::allow();
4+
}

tests/GenerateServerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
$this->makeFilePath('/app/Http/Resources/ResourcesResource.php'),
5959
$this->makeFilePath('/app/Http/Resources/ResourcesDataDataResource.php'),
6060
$this->makeFilePath('/app/Http/Resources/ResourceRootResource.php'),
61+
62+
$this->makeFilePath('/app/Http/Controllers/PoliciesController.php'),
63+
$this->makeFilePath('/app/Http/Tests/PoliciesComponentTest.php'),
64+
$this->makeFilePath('/app/Http/Policies/PoliciesControllerPolicy.php'),
6165
], $putFiles);
6266
});
6367

tests/PolicyGenerationTest.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+
8+
use function Pest\Laravel\artisan;
9+
use function PHPUnit\Framework\assertEqualsCanonicalizing;
10+
use function PHPUnit\Framework\assertNotEqualsCanonicalizing;
11+
12+
test('Correct methods in generated policy', function () {
13+
/** @var TestCase $this */
14+
$mapping = Config::get('openapi-server-generator.api_docs_mappings');
15+
$mappingValue = current($mapping);
16+
$mapping = [$this->makeFilePath(__DIR__ . '/resources/index.yaml') => $mappingValue];
17+
Config::set('openapi-server-generator.api_docs_mappings', $mapping);
18+
19+
$filesystem = $this->mock(Filesystem::class);
20+
$filesystem->shouldReceive('exists')->andReturn(false);
21+
$filesystem->shouldReceive('get')->withArgs(function ($path) {
22+
return (bool)strstr($path, '.template');
23+
})->andReturnUsing(function ($path) {
24+
return file_get_contents($path);
25+
});
26+
$filesystem->shouldReceive('cleanDirectory', 'ensureDirectoryExists');
27+
28+
$policies = [];
29+
$filesystem->shouldReceive('put')->withArgs(function ($path, $content) use (&$policies) {
30+
if (str_contains($path, 'Policy.php')) {
31+
$policies[pathinfo($path, PATHINFO_BASENAME)] = $content;
32+
}
33+
34+
return true;
35+
});
36+
37+
artisan(GenerateServer::class);
38+
39+
foreach ($policies as $key => $content) {
40+
$methods = [];
41+
preg_match_all('~public function (.*)\(~', $content, $methods);
42+
$policies[$key] = $methods[1];
43+
}
44+
45+
assertEqualsCanonicalizing(['methodFoo', 'methodBar'], $policies['PoliciesControllerPolicy.php']);
46+
assertNotEqualsCanonicalizing(['methodWithoutForbiddenResponse'], $policies['PoliciesControllerPolicy.php']);
47+
});

tests/resources/index.yaml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ paths:
6060
x-lg-skip-controller-generation: true
6161
x-lg-skip-request-generation: true
6262
x-lg-skip-tests-generation: true
63+
x-lg-skip-policy-generation: true
6364
responses:
6465
"200":
6566
description: Успешный ответ
@@ -146,7 +147,30 @@ paths:
146147
application/json:
147148
schema:
148149
$ref: './schemas/test_resource_generation.yaml#/GenerateResourceWithoutPropertiesResponse'
149-
150+
/policies:test-generate-policy-method-foo:
151+
post:
152+
operationId: generatePolicyMethodFoo
153+
x-lg-handler: '\App\Http\Controllers\PoliciesController@methodFoo'
154+
x-lg-skip-request-generation: true
155+
responses:
156+
"403":
157+
description: Ошибка прав доступа
158+
/policies:test-generate-policy-method-bar:
159+
post:
160+
operationId: generatePolicyMethodBar
161+
x-lg-handler: '\App\Http\Controllers\PoliciesController@methodBar'
162+
x-lg-skip-request-generation: true
163+
responses:
164+
"403":
165+
description: Ошибка прав доступа
166+
/policies:test-generate-policy-method-without-forbidden-response:
167+
post:
168+
operationId: generatePolicyMethodWithoutForbiddenResponse
169+
x-lg-handler: '\App\Http\Controllers\PoliciesController@methodWithoutForbiddenResponse'
170+
x-lg-skip-request-generation: true
171+
responses:
172+
"200":
173+
description: Успешный ответ c контекстом
150174
components:
151175
responses:
152176
ServerError:

0 commit comments

Comments
 (0)