Skip to content

Commit

Permalink
Merge pull request #28 from ensi-platform/task-104744
Browse files Browse the repository at this point in the history
#104744
  • Loading branch information
MsNatali authored Jul 11, 2023
2 parents 7a24470 + 6a0d2fb commit 63dc3c9
Show file tree
Hide file tree
Showing 24 changed files with 695 additions and 42 deletions.
17 changes: 16 additions & 1 deletion src/Commands/GenerateServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@

class GenerateServer extends Command
{
public const SUPPORTED_ENTITIES = [
'controllers',
'enums',
'requests',
'routes',
'pest_tests',
'resources',
'policies',
];

/** var @string */
protected $signature = 'openapi:generate-server {--e|entities=}';

Expand Down Expand Up @@ -55,7 +65,12 @@ public function handleMapping(string $sourcePath, array $optionsPerEntity)
{
$specObject = $this->parseSpec($sourcePath);

foreach ($this->config['supported_entities'] as $entity => $generatorClass) {
foreach (static::SUPPORTED_ENTITIES as $entity) {
$generatorClass = $this->config['supported_entities'][$entity] ?? null;
if (!isset($generatorClass)) {
continue;
}

if (!$this->shouldEntityBeGenerated($entity)) {
continue;
}
Expand Down
29 changes: 29 additions & 0 deletions src/Data/Controllers/ControllersStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Ensi\LaravelOpenApiServerGenerator\Data\Controllers;

class ControllersStorage
{
/** @var array Recently created controllers */
protected array $controllers = [];

public function markNewControllerMethod(
string $serversUrl,
string $path,
string $method,
array $responseCodes
): void {
$this->controllers[$serversUrl][$path][$method] = $responseCodes;
}

public function isExistControllerMethod(
string $serversUrl,
string $path,
string $method,
int $responseCode
): bool {
$codes = $this->controllers[$serversUrl][$path][$method] ?? [];

return !in_array($responseCode, $codes);
}
}
4 changes: 4 additions & 0 deletions src/Generators/BaseGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Ensi\LaravelOpenApiServerGenerator\Generators;

use Ensi\LaravelOpenApiServerGenerator\Data\Controllers\ControllersStorage;
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
use Ensi\LaravelOpenApiServerGenerator\Utils\PhpDocGenerator;
use Ensi\LaravelOpenApiServerGenerator\Utils\PSR4PathConverter;
use Ensi\LaravelOpenApiServerGenerator\Utils\RouteHandlerParser;
Expand All @@ -22,6 +24,8 @@ public function __construct(
protected RouteHandlerParser $routeHandlerParser,
protected TypesMapper $typesMapper,
protected PhpDocGenerator $phpDocGenerator,
protected ClassParser $classParser,
protected ControllersStorage $controllersStorage,
) {
}

Expand Down
109 changes: 88 additions & 21 deletions src/Generators/ControllersGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
namespace Ensi\LaravelOpenApiServerGenerator\Generators;

use cebe\openapi\SpecObjectInterface;
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
use stdClass;

class ControllersGenerator extends BaseGenerator implements GeneratorInterface
{
public const REQUEST_NAMESPACE = 'Illuminate\Http\Request';
public const RESPONSABLE_NAMESPACE = 'Illuminate\Contracts\Support\Responsable';
public const DELIMITER = "\n ";

private array $methodsWithRequests = ['PATCH', 'POST', 'PUT', 'DELETE'];

private string $serversUrl;

public function generate(SpecObjectInterface $specObject): void
{
$openApiData = $specObject->getSerializableData();
$this->serversUrl = $openApiData?->servers[0]?->url ?? '';

$controllers = $this->extractControllers($specObject);
$this->createControllersFiles($controllers, $this->templatesManager->getTemplate('Controller.template'));
$this->createControllersFiles($controllers);
}

private function extractControllers(SpecObjectInterface $specObject): array
Expand All @@ -21,7 +31,7 @@ private function extractControllers(SpecObjectInterface $specObject): array

$controllers = [];
$paths = $openApiData->paths ?: [];
foreach ($paths as $routes) {
foreach ($paths as $path => $routes) {
foreach ($routes as $method => $route) {
$requestClassName = null;
$methodWithRequest = in_array(strtoupper($method), $this->methodsWithRequests);
Expand Down Expand Up @@ -56,14 +66,20 @@ private function extractControllers(SpecObjectInterface $specObject): array
list($requestClassName, $requestNamespace) = $this->getActualClassNameAndNamespace($requestClassName, $requestNamespace);
$requestNamespace .= '\\' . ucfirst($requestClassName);

$controllers[$fqcn]['requestsNamespaces'][] = $requestNamespace;
} elseif ($methodWithRequest) {
$controllers[$fqcn]['requestsNamespaces'][] = 'Illuminate\Http\Request';
$controllers[$fqcn]['requestsNamespaces'][$requestNamespace] = $requestNamespace;
}

$responses = $route->responses ?? null;
$controllers[$fqcn]['actions'][] = [
'name' => $handler->method ?: '__invoke',
'with_request_namespace' => $methodWithRequest && !empty($route->{'x-lg-skip-request-generation'}),
'parameters' => array_merge($this->extractPathParameters($route), $this->getActionExtraParameters($methodWithRequest, $requestClassName)),

'route' => [
'method' => $method,
'path' => $path,
'responseCodes' => $responses ? array_keys(get_object_vars($responses)) : [],
],
];
}
}
Expand All @@ -73,7 +89,7 @@ private function extractControllers(SpecObjectInterface $specObject): array

private function extractPathParameters(stdClass $route): array
{
$oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path");
$oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path");

return array_map(fn (stdClass $param) => [
'name' => $param->name,
Expand All @@ -93,55 +109,106 @@ private function getActionExtraParameters(bool $methodWithRequest, $requestClass
return [];
}

private function createControllersFiles(array $controllers, string $template): void
private function createControllersFiles(array $controllers): void
{
foreach ($controllers as $controller) {
$namespace = $controller['namespace'];
$className = $controller['className'];

$filePath = $this->getNamespacedFilePath($className, $namespace);
if ($this->filesystem->exists($filePath)) {
$controllerExists = $this->filesystem->exists($filePath);
if (!$controllerExists) {
$this->createEmptyControllerFile($filePath, $controller);
}

$class = $this->classParser->parse("$namespace\\$className");

$newMethods = $this->convertMethodsToString($class, $controller['actions'], $controller['requestsNamespaces']);
if (!empty($newMethods)) {
$controller['requestsNamespaces'][static::RESPONSABLE_NAMESPACE] = static::RESPONSABLE_NAMESPACE;
} elseif ($controllerExists) {
continue;
}

$this->putWithDirectoryCheck(
$filePath,
$this->replacePlaceholders($template, [
'{{ namespace }}' => $namespace,
'{{ className }}' => $className,
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
'{{ methods }}' => $this->convertMethodsToString($controller['actions']),
])
);
$content = $class->getContentWithAdditionalMethods($newMethods, $controller['requestsNamespaces']);

$this->writeControllerFile($filePath, $controller, $content);
}
}

protected function writeControllerFile(string $filePath, array $controller, string $classContent): void
{
$this->putWithDirectoryCheck(
$filePath,
$this->replacePlaceholders(
$this->templatesManager->getTemplate('ControllerExists.template'),
[
'{{ namespace }}' => $controller['namespace'],
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
'{{ classContent }}' => $classContent,
]
)
);
}

protected function createEmptyControllerFile(string $filePath, array $controller): void
{
$this->putWithDirectoryCheck(
$filePath,
$this->replacePlaceholders(
$this->templatesManager->getTemplate('ControllerEmpty.template'),
[
'{{ namespace }}' => $controller['namespace'],
'{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']),
'{{ className }}' => $controller['className'],
]
)
);
}

private function formatActionParamsAsString(array $params): string
{
return implode(', ', array_map(fn (array $param) => $param['type'] . " $" . $param['name'], $params));
}

private function convertMethodsToString(array $methods): string
private function convertMethodsToString(ClassParser $class, array $methods, array &$namespaces): string
{
$methodsStrings = [];

foreach ($methods as $method) {
if ($class->hasMethod($method['name'])) {
continue;
}

if ($method['with_request_namespace']) {
$namespaces[static::REQUEST_NAMESPACE] = static::REQUEST_NAMESPACE;
}

$methodsStrings[] = $this->replacePlaceholders(
$this->templatesManager->getTemplate('ControllerMethod.template'),
[
'{{ method }}' => $method['name'],
'{{ params }}' => $this->formatActionParamsAsString($method['parameters']),
]
);

$this->controllersStorage->markNewControllerMethod(
serversUrl: $this->serversUrl,
path: $method['route']['path'],
method: $method['route']['method'],
responseCodes: $method['route']['responseCodes'],
);
}

return implode("\n\n ", $methodsStrings);
$prefix = !empty($methodsStrings) ? static::DELIMITER : '';

return $prefix . implode(static::DELIMITER, $methodsStrings);
}

protected function formatRequestNamespaces(array $namespaces): string
{
$namespaces = array_unique($namespaces);
sort($namespaces);
$namespaces = array_values($namespaces);
sort($namespaces, SORT_STRING | SORT_FLAG_CASE);

return implode("\n", array_map(fn (string $namespaces) => "use {$namespaces};", $namespaces));
}
Expand Down
19 changes: 14 additions & 5 deletions src/Generators/PestTestsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,30 @@ private function getPhpHttpTestMethodCommon(string $httpMethod): string
return $httpMethod;
}

protected function convertRoutesToTestsString(array $routes, string $serversUrl): string
protected function convertRoutesToTestsString(array $routes, string $serversUrl, bool $onlyNewMethods = false): string
{
$testsFunctions = [
"uses()->group('component');",
];
$testsFunctions = $onlyNewMethods ? [] : ["uses()->group('component');"];

foreach ($routes as $route) {
foreach ($route['responseCodes'] as $responseCode) {
if ($responseCode < 200 || $responseCode >= 500) {
continue;
}

$methodExists = $this->controllersStorage->isExistControllerMethod(
serversUrl: $serversUrl,
path: $route['path'],
method: $route['method'],
responseCode: $responseCode,
);

if ($onlyNewMethods && $methodExists) {
continue;
}

$url = $serversUrl . $route['path'];
$testName = strtoupper($route['method']) . ' ' . $url. ' ' . $responseCode;
$phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']);
$phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']);
$testsFunctions[] = <<<FUNC
test('{$testName}', function () {
Expand Down
24 changes: 23 additions & 1 deletion src/Generators/PoliciesGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use cebe\openapi\SpecObjectInterface;
use Ensi\LaravelOpenApiServerGenerator\DTO\ParsedRouteHandler;
use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser;
use InvalidArgumentException;
use RuntimeException;
use stdClass;
Expand Down Expand Up @@ -70,6 +71,13 @@ protected function createPoliciesFiles(array $policies, string $template): void
foreach ($policies as ['className' => $className, 'namespace' => $namespace, 'methods' => $methods]) {
$filePath = $this->getNamespacedFilePath($className, $namespace);
if ($this->filesystem->exists($filePath)) {
$class = $this->classParser->parse("$namespace\\$className");

$newPolicies = $this->convertMethodsToString($methods, $class);
if (!empty($newPolicies)) {
$class->addMethods($newPolicies);
}

continue;
}

Expand Down Expand Up @@ -104,17 +112,31 @@ private function handlerValidation(ParsedRouteHandler $handler): bool
};
}

private function convertMethodsToString(array $methods): string
private function convertMethodsToString(array $methods, ?ClassParser $class = null): string
{
$methodsStrings = [];

foreach ($methods as $method) {
if ($class?->hasMethod($method)) {
continue;
}

$methodsStrings[] = $this->replacePlaceholders(
$this->templatesManager->getTemplate('PolicyGate.template'),
['{{ method }}' => $method]
);
}

if ($class) {
$existMethods = $class->getMethods();
foreach ($existMethods as $methodName => $method) {
if (!in_array($methodName, $methods) && !$class->isTraitMethod($methodName)) {
$className = $class->getClassName();
console_warning("Warning: метод {$className}::{$methodName} отсутствует в спецификации или не может возвращать 403 ошибку");
}
}
}

return implode("\n\n ", $methodsStrings);
}
}
7 changes: 1 addition & 6 deletions src/Generators/RoutesGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,7 @@ private function formatControllerNamespaces(array $controllerNamespaces): string
}
}

uasort($namespaces, function (string $first, string $second) {
$firstNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $first)));
$secondNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $second)));

return strcasecmp($firstNamespace, $secondNamespace);
});
sort($namespaces, SORT_STRING | SORT_FLAG_CASE);

return implode("\n", array_map(fn (string $namespace) => "use {$namespace};", $namespaces));
}
Expand Down
Loading

0 comments on commit 63dc3c9

Please sign in to comment.