From b8fa502fee9cece62cb3614ce5925618579992b9 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Feb 2023 08:44:28 +0100 Subject: [PATCH] Introduce representation layer for reduced complexity This is a rewrite --- bin/openapi-client-generator.source | 2 +- composer.lock | 51 +- src/Files.php | 48 ++ src/Gatherer/Client.php | 42 ++ src/Gatherer/Hydrator.php | 27 + src/Gatherer/Operation.php | 102 ++++ src/Gatherer/OperationHydrator.php | 34 ++ src/Gatherer/Path.php | 44 ++ src/Gatherer/Property.php | 98 ++++ src/Gatherer/Schema.php | 41 ++ src/Gatherer/WebHook.php | 58 +++ src/Gatherer/WebHookHydrator.php | 34 ++ src/Generator.php | 248 ++++----- src/Generator/Client.php | 483 +++++++++--------- src/Generator/ClientInterface.php | 142 ++--- src/Generator/Clients.php | 116 ----- src/Generator/Hydrator.php | 41 ++ src/Generator/Hydrators.php | 267 ++++++++++ src/Generator/Operation.php | 324 ++++++------ src/Generator/Path.php | 72 --- src/Generator/Schema.php | 227 +------- src/Generator/WebHook.php | 232 ++++----- src/Generator/WebHooks.php | 335 +++++++++--- .../Schema.php} | 28 +- src/Representation/Client.php | 15 + src/Representation/Header.php | 14 + src/Representation/Hydrator.php | 14 + src/Representation/Operation.php | 28 + src/Representation/OperationRequestBody.php | 14 + src/Representation/OperationResponse.php | 16 + src/Representation/Parameter.php | 15 + src/Representation/Path.php | 16 + src/Representation/Property.php | 15 + src/Representation/PropertyType.php | 12 + src/Representation/Schema.php | 19 + src/Representation/WebHook.php | 19 + src/Utils.php | 64 +++ 37 files changed, 2138 insertions(+), 1219 deletions(-) create mode 100644 src/Files.php create mode 100644 src/Gatherer/Client.php create mode 100644 src/Gatherer/Hydrator.php create mode 100644 src/Gatherer/Operation.php create mode 100644 src/Gatherer/OperationHydrator.php create mode 100644 src/Gatherer/Path.php create mode 100644 src/Gatherer/Property.php create mode 100644 src/Gatherer/Schema.php create mode 100644 src/Gatherer/WebHook.php create mode 100644 src/Gatherer/WebHookHydrator.php delete mode 100644 src/Generator/Clients.php create mode 100644 src/Generator/Hydrator.php create mode 100644 src/Generator/Hydrators.php delete mode 100644 src/Generator/Path.php rename src/{SchemaRegistry.php => Registry/Schema.php} (60%) create mode 100644 src/Representation/Client.php create mode 100644 src/Representation/Header.php create mode 100644 src/Representation/Hydrator.php create mode 100644 src/Representation/Operation.php create mode 100644 src/Representation/OperationRequestBody.php create mode 100644 src/Representation/OperationResponse.php create mode 100644 src/Representation/Parameter.php create mode 100644 src/Representation/Path.php create mode 100644 src/Representation/Property.php create mode 100644 src/Representation/PropertyType.php create mode 100644 src/Representation/Schema.php create mode 100644 src/Representation/WebHook.php create mode 100644 src/Utils.php diff --git a/bin/openapi-client-generator.source b/bin/openapi-client-generator.source index e5db6af..c781984 100755 --- a/bin/openapi-client-generator.source +++ b/bin/openapi-client-generator.source @@ -17,7 +17,7 @@ use Symfony\Component\Yaml\Yaml; */ exit((function (string $configuration): int { $yaml = Yaml::parseFile($configuration); - (new Generator($yaml['spec']))->generate($yaml['namespace'] . '\\', $yaml['destination']); + (new Generator($yaml['spec']))->generate($yaml['namespace'] . '\\', dirname($configuration) . DIRECTORY_SEPARATOR . $yaml['destination']); return 0; })($configuration)); diff --git a/composer.lock b/composer.lock index 9b37087..5a9ac55 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/php-api-clients/contracts.git", - "reference": "96729b2eb5f1c79b470de21bf4ec278c957d6260" + "reference": "2f90f252e43051c896bbc4433f1257bed6e32fb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-api-clients/contracts/zipball/96729b2eb5f1c79b470de21bf4ec278c957d6260", - "reference": "96729b2eb5f1c79b470de21bf4ec278c957d6260", + "url": "https://api.github.com/repos/php-api-clients/contracts/zipball/2f90f252e43051c896bbc4433f1257bed6e32fb4", + "reference": "2f90f252e43051c896bbc4433f1257bed6e32fb4", "shasum": "" }, "require": { @@ -45,7 +45,7 @@ "issues": "https://github.com/php-api-clients/contracts/issues", "source": "https://github.com/php-api-clients/contracts/tree/main" }, - "time": "2023-01-29T13:53:21+00:00" + "time": "2023-02-22T21:38:15+00:00" }, { "name": "cebe/php-openapi", @@ -1861,20 +1861,20 @@ }, { "name": "respect/validation", - "version": "2.2.3", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/Respect/Validation.git", - "reference": "4c21a7ffc9a4915673cb2c2843963919e664e627" + "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Respect/Validation/zipball/4c21a7ffc9a4915673cb2c2843963919e664e627", - "reference": "4c21a7ffc9a4915673cb2c2843963919e664e627", + "url": "https://api.github.com/repos/Respect/Validation/zipball/d304ace5325efd7180daffb1f8627bb0affd4e3a", + "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0 || ^8.1 || ^8.2", "respect/stringifier": "^0.2.0", "symfony/polyfill-mbstring": "^1.2" }, @@ -1882,23 +1882,20 @@ "egulias/email-validator": "^3.0", "malukenho/docheader": "^0.1", "mikey179/vfsstream": "^1.6", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^9.3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.6", "psr/http-message": "^1.0", "respect/coding-standard": "^3.0", - "squizlabs/php_codesniffer": "^3.5", - "symfony/validator": "^3.0||^4.0", - "zendframework/zend-validator": "^2.1" + "squizlabs/php_codesniffer": "^3.7", + "symfony/validator": "^3.0||^4.0" }, "suggest": { "egulias/email-validator": "Strict (RFC compliant) email validation", "ext-bcmath": "Arbitrary Precision Mathematics", "ext-fileinfo": "File Information", - "ext-mbstring": "Multibyte String Functions", - "symfony/validator": "Use Symfony validator through Respect\\Validation", - "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation" + "ext-mbstring": "Multibyte String Functions" }, "type": "library", "autoload": { @@ -1925,9 +1922,9 @@ ], "support": { "issues": "https://github.com/Respect/Validation/issues", - "source": "https://github.com/Respect/Validation/tree/2.2.3" + "source": "https://github.com/Respect/Validation/tree/2.2.4" }, - "time": "2021-03-19T14:12:45+00:00" + "time": "2023-02-15T01:05:24+00:00" }, { "name": "ringcentral/psr7", @@ -2355,16 +2352,16 @@ }, { "name": "twig/twig", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "3ffcf4b7d890770466da3b2666f82ac054e7ec72" + "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/3ffcf4b7d890770466da3b2666f82ac054e7ec72", - "reference": "3ffcf4b7d890770466da3b2666f82ac054e7ec72", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", + "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", "shasum": "" }, "require": { @@ -2415,7 +2412,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.0" + "source": "https://github.com/twigphp/Twig/tree/v3.5.1" }, "funding": [ { @@ -2427,7 +2424,7 @@ "type": "tidelift" } ], - "time": "2022-12-27T12:28:18+00:00" + "time": "2023-02-08T07:49:20+00:00" }, { "name": "webmozart/assert", diff --git a/src/Files.php b/src/Files.php new file mode 100644 index 0000000..f50976a --- /dev/null +++ b/src/Files.php @@ -0,0 +1,48 @@ + + */ + public static function listExistingFiles(string $path): iterable + { + if (!file_exists($path)) { + yield from []; + return; + } + + foreach (scandir($path) as $node) { + if ($node === '.' || $node === '..') { + continue; + } + + if (is_file($path . $node)) { + yield $path . $node => md5_file($path . $node); + } + + if (is_dir($path . $node)) { + yield from self::listExistingFiles($path . $node . DIRECTORY_SEPARATOR); + } + } + } +} diff --git a/src/Gatherer/Client.php b/src/Gatherer/Client.php new file mode 100644 index 0000000..82ebdf9 --- /dev/null +++ b/src/Gatherer/Client.php @@ -0,0 +1,42 @@ +servers ?? [] as $server) { + if (!($server instanceof Server)) { + continue; + } + + if (strlen($server->url) === 0) { + continue; + } + + $baseUrl = $server->url; + break; + } + + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Client( + $baseUrl, + $paths, + ); + } +} diff --git a/src/Gatherer/Hydrator.php b/src/Gatherer/Hydrator.php new file mode 100644 index 0000000..0ed11c4 --- /dev/null +++ b/src/Gatherer/Hydrator.php @@ -0,0 +1,27 @@ +parameters as $parameter) { + if ($parameter->name === 'per_page') { + $hasPerPageParameter = true; + } + if ($parameter->name === 'page') { + $hasPageParameter = true; + } + $parameterType = str_replace([ + 'integer', + 'any', + 'boolean', + ], [ + 'int', + 'mixed', + 'bool', + ], implode('|', is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type])); + + $parameters[] = new Parameter( + $parameter->name, + (string)$parameter->description, + $parameterType, + $parameter->in, + $parameter->schema->default, + ); + } + + $classNameSanitized = str_replace('/', '\\', Utils::className($className)); + $requestBody = []; + if ($operation->requestBody !== null) { + foreach ($operation->requestBody->content as $contentType => $requestBodyDetails) { + $requestBodyClassname = $schemaRegistry->get($requestBodyDetails->schema, $classNameSanitized . '\\Request\\' . Utils::className(str_replace('/', '', $contentType))); + $requestBody[] = new OperationRequestBody( + $contentType, + Schema::gather($requestBodyClassname, $requestBodyDetails->schema, $schemaRegistry), + ); + } + } + $response = []; + foreach ($operation->responses as $code => $spec) { + foreach ($spec->content as $contentType => $contentTypeMediaType) { + $responseClassname = $schemaRegistry->get($contentTypeMediaType->schema, 'Operation\\' . $classNameSanitized . '\\Response\\' . Utils::className(str_replace('/', '', $contentType) . '\\H' . $code)); + $response[] = new OperationResponse( + $code, + $contentType, + $spec->description, + Schema::gather($responseClassname, $contentTypeMediaType->schema, $schemaRegistry), + ); + $returnType[] = $responseClassname; + } + } + + if (count($returnType) === 0) { + $returnType[] = '\\' . ResponseInterface::class; + } + + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Operation( + Utils::fixKeyword($className), + $classNameSanitized, + lcfirst(trim(Utils::basename($className),'\\')), + trim(Utils::dirname($className),'\\'), + $operation->operationId, + $method, + $path, + $hasPageParameter === true && $hasPerPageParameter === true, // This is very GitHub specific!!! + array_unique($returnType), + [ + ...array_filter($parameters, static fn (Parameter $parameter): bool => $parameter->default === null), + ...array_filter($parameters, static fn (Parameter $parameter): bool => $parameter->default !== null), + ], + $requestBody, + $response, + ); + } +} diff --git a/src/Gatherer/OperationHydrator.php b/src/Gatherer/OperationHydrator.php new file mode 100644 index 0000000..b412c49 --- /dev/null +++ b/src/Gatherer/OperationHydrator.php @@ -0,0 +1,34 @@ +response as $response) { + $schemaClasses[] = $response->schema; + } + } + + return Hydrator::gather( + 'Operation\\' . $className, + ...$schemaClasses, + ); + } +} diff --git a/src/Gatherer/Path.php b/src/Gatherer/Path.php new file mode 100644 index 0000000..094bf59 --- /dev/null +++ b/src/Gatherer/Path.php @@ -0,0 +1,44 @@ +getOperations() as $method => $operation) { + $operationClassName = Utils::className($operation->operationId); + if (strlen($operationClassName) === 0) { + continue; + } + + $operations[] = Operation::gather( + $operationClassName, + $method, + $path, + $operation, + $schemaRegistry, + ); + } + + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Path( + $className, + OperationHydrator::gather( + $className, + ...$operations, + ), + $operations, + ); + } +} diff --git a/src/Gatherer/Property.php b/src/Gatherer/Property.php new file mode 100644 index 0000000..3248508 --- /dev/null +++ b/src/Gatherer/Property.php @@ -0,0 +1,98 @@ +type; + $nullable = !$required; + + if ( + is_array($type) && + count($type) === 2 && + ( + in_array(null, $type) || + in_array("null", $type) + ) + ) { + foreach ($type as $pt) { + if ($pt !== null && $pt !== "null") { + $type = $pt; + break; + } + } + + $nullable = true; + } + + if (is_string($type)) { + $type = str_replace([ + 'integer', + 'number', + 'any', + 'null', + 'boolean', + ], [ + 'int', + 'int', + '', + '', + 'bool', + ], $type); + } else { + $type = ''; + } + if ($type === '') { + $type = new PropertyType( + 'scalar', + 'mixed' + ); + $nullable = false; + } else if ($type === 'object') { + $type = new PropertyType( + 'object', + Schema::gather( + $schemaRegistry->get($property, $className . '\\' . Utils::className($propertyName)), + $property, + $schemaRegistry, + ) + ); + } else { + $type = new PropertyType( + 'scalar', + $type + ); + } + + if (!is_array($type)) { + $type = [$type]; + } + + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Property($propertyName, $property->description ?? '', $type, $nullable); + } +} diff --git a/src/Gatherer/Schema.php b/src/Gatherer/Schema.php new file mode 100644 index 0000000..1045e90 --- /dev/null +++ b/src/Gatherer/Schema.php @@ -0,0 +1,41 @@ +type === 'array'; + if ($isArray) { + $schema = $schema->items; + } + + $properties = []; + foreach ($schema->properties as $propertyName => $property) { + $properties[] = Property::gather( + $className, + $propertyName, + is_array($schema->required) && !in_array($propertyName, $schema->required, false), + $property, + $schemaRegistry + ); + } + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Schema( + $className, + $schema->title ?? '', + $schema->description ?? '', + $properties, + $schema, + $isArray, + ); + } +} diff --git a/src/Gatherer/WebHook.php b/src/Gatherer/WebHook.php new file mode 100644 index 0000000..fe111e4 --- /dev/null +++ b/src/Gatherer/WebHook.php @@ -0,0 +1,58 @@ +post->operationId); + + $headers = []; + foreach ($webhook->post?->parameters ?? [] as $header) { + if ($header->in !== 'header') { + continue; + } + + $headers[] = new Header($header->name, Schema::gather( + $schemaRegistry->get( + $header->schema, + 'WebHookHeader\\' . ucfirst(preg_replace('/\PL/u', '', $header->name)), + ), + $header->schema, + $schemaRegistry + )); + } + + return new \ApiClients\Tools\OpenApiClientGenerator\Representation\WebHook( + $event, + $webhook->post->summary ?? '', + $webhook->post->description ?? '', + $webhook->post->operationId, + $webhook->post->externalDocs?->url ?? '', + $headers, + iterator_to_array((static function (array $content, SchemaRegistry $schemaRegistry): iterable { + foreach ($content as $type => $schema) { + yield $type => Schema::gather( + $schemaRegistry->get($schema->schema, 'T' . time()), + $schema->schema, + $schemaRegistry, + ); + } + })($webhook->post->requestBody->content, $schemaRegistry)), + ); + } +} diff --git a/src/Gatherer/WebHookHydrator.php b/src/Gatherer/WebHookHydrator.php new file mode 100644 index 0000000..392444d --- /dev/null +++ b/src/Gatherer/WebHookHydrator.php @@ -0,0 +1,34 @@ +schema as $schema) { + $schemaClasses[] = $schema; + } + } + + return Hydrator::gather( + 'WebHook\\' . Utils::className($event), + ...$schemaClasses, + ); + } +} diff --git a/src/Generator.php b/src/Generator.php index 0406ccc..88b811a 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -2,21 +2,19 @@ namespace ApiClients\Tools\OpenApiClientGenerator; -use ApiClients\Tools\OpenApiClientGenerator\Generator\Authentication; +use ApiClients\Tools\OpenApiClientGenerator\Gatherer\OperationHydrator; +use ApiClients\Tools\OpenApiClientGenerator\Gatherer\WebHookHydrator; use ApiClients\Tools\OpenApiClientGenerator\Generator\Client; use ApiClients\Tools\OpenApiClientGenerator\Generator\ClientInterface; -use ApiClients\Tools\OpenApiClientGenerator\Generator\Clients; +use ApiClients\Tools\OpenApiClientGenerator\Generator\Hydrator; +use ApiClients\Tools\OpenApiClientGenerator\Generator\Hydrators; use ApiClients\Tools\OpenApiClientGenerator\Generator\Operation; -use ApiClients\Tools\OpenApiClientGenerator\Generator\Path; use ApiClients\Tools\OpenApiClientGenerator\Generator\Schema; use ApiClients\Tools\OpenApiClientGenerator\Generator\WebHook; -use ApiClients\Tools\OpenApiClientGenerator\Generator\WebHookInterface; use ApiClients\Tools\OpenApiClientGenerator\Generator\WebHooks; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; use cebe\openapi\Reader; use cebe\openapi\spec\OpenApi; -use EventSauce\ObjectHydrator\ObjectMapperCodeGenerator; -use Jawira\CaseConverter\Convert; -use League\ConstructFinder\ConstructFinder; use PhpParser\Node; use PhpParser\PrettyPrinter\Standard; @@ -32,28 +30,31 @@ public function __construct(string $specUrl) public function generate(string $namespace, string $destinationPath) { - $namespace = self::cleanUpNamespace($namespace); + $existingFiles = iterator_to_array(Files::listExistingFiles($destinationPath . DIRECTORY_SEPARATOR)); + $namespace = Utils::cleanUpNamespace($namespace); $codePrinter = new Standard(); foreach ($this->all($namespace, $destinationPath . DIRECTORY_SEPARATOR) as $file) { - $fileName = $destinationPath . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, substr($file->fqcn, strlen($namespace))); - @mkdir(dirname($fileName), 0744, true); - file_put_contents($fileName . '.php', ($file->contents instanceof Node ? $codePrinter->prettyPrintFile([$file->contents]) : $file->contents) . PHP_EOL); - include_once $fileName . '.php'; + $fileName = $destinationPath . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, substr($file->fqcn, strlen($namespace))) . '.php'; + $fileContents = ($file->contents instanceof Node ? $codePrinter->prettyPrintFile([ + new Node\Stmt\Declare_([ + new Node\Stmt\DeclareDeclare('strict_types', new Node\Scalar\LNumber(1)), + ]), + $file->contents, + ]) : $file->contents) . PHP_EOL; + if ((array_key_exists($fileName, $existingFiles) && md5($fileContents) !== $existingFiles[$fileName]) || !array_key_exists($fileName, $existingFiles)) { + @mkdir(dirname($fileName), 0744, true); + file_put_contents($fileName, $fileContents); + } + include_once $fileName; + if (array_key_exists($fileName, $existingFiles)) { + unset($existingFiles[$fileName]); + } } - } - public static function className(string $className): string - { - return str_replace(['{', '}', '-', '$', '_', '+', '*', '.'], ['Cb', 'Rcb', 'Dash', '_', '\\', 'Plus', 'Obelix', 'Dot'], (new Convert($className))->toPascal()) . (self::isKeyword(self::basename($className)) ? '_' : ''); - } - - private static function cleanUpNamespace(string $namespace): string - { - $namespace = str_replace('/', '\\', $namespace); - $namespace = str_replace('\\\\', '\\', $namespace); - - return $namespace; + foreach ($existingFiles as $existingFile => $_) { + unlink($existingFile); + } } /** @@ -63,173 +64,104 @@ private static function cleanUpNamespace(string $namespace): string */ private function all(string $namespace, string $rootPath): iterable { - $schemaClasses = []; + $schemas = []; $schemaRegistry = new SchemaRegistry(); if (count($this->spec->components->schemas ?? []) > 0) { foreach ($this->spec->components->schemas as $name => $schema) { - $schemaClassName = self::className($name); - if (strlen($schemaClassName) === 0) { - continue; - } - $schemaRegistry->addClassName($schemaClassName, $schema); + $schemaRegistry->addClassName(Utils::className($name), $schema); + $schemas[] = \ApiClients\Tools\OpenApiClientGenerator\Gatherer\Schema::gather(Utils::className($name), $schema, $schemaRegistry); } - foreach ($this->spec->components->schemas as $name => $schema) { - $schemaClassName = $schemaRegistry->get($schema, $schemaClassName); - if (strlen($schemaClassName) === 0) { - continue; - } + } - $schemaClasses[] = $namespace . 'Schema/' . $schemaClassName; - yield from Schema::generate( - $name, - self::dirname($namespace . 'Schema/' . $schemaClassName), - self::basename($namespace . 'Schema/' . $schemaClassName), - $schema, - $schemaRegistry, - $namespace . 'Schema' - ); + $webHooks = []; + if (count($this->spec->webhooks ?? []) > 0) { + foreach ($this->spec->webhooks as $webHook) { + $webHookje = \ApiClients\Tools\OpenApiClientGenerator\Gatherer\WebHook::gather($webHook, $schemaRegistry); + if (!array_key_exists($webHookje->event, $webHooks)) { + $webHooks[$webHookje->event] = []; + } + $webHooks[$webHookje->event][] = $webHookje; } } - $clients = []; + $paths = []; if (count($this->spec->paths ?? []) > 0) { foreach ($this->spec->paths as $path => $pathItem) { - $pathClassName = self::className($path); - if (strlen($pathClassName) === 0) { - continue; + if ($path === '/') { + $pathClassName = 'Root'; + } else { + $pathClassName = trim(Utils::className($path), '\\'); } - yield from Path::generate( - $path, - self::dirname($namespace . 'Path/' . $pathClassName), - $namespace, - self::basename($namespace . 'Path/' . $pathClassName), - $pathItem - ); - - foreach ($pathItem->getOperations() as $method => $operation) { - $operationClassName = self::className((new Convert($operation->operationId))->fromTrain()->toPascal()); - $operations[$method] = $operationClassName; - if (strlen($operationClassName) === 0) { - continue; - } - - yield from Operation::generate( - $path, - $method, - self::dirname($namespace . 'Operation/' . $operationClassName), - $namespace, - self::basename($namespace . 'Operation/' . $operationClassName), - $operation, - $schemaRegistry - ); - - [$operationGroup, $operationOperation] = explode('/', $operationClassName); - if (!array_key_exists($operationGroup, $clients)) { - $clients[$operationGroup] = []; - } - $clients[$operationGroup][$operationOperation] = [ - 'class' => $operationClassName, - 'operation' => $operation, - ]; + if (strlen($path) === 0 || strlen($pathClassName) === 0) { + continue; } - } - } - yield from (function (array $clients, string $namespace, SchemaRegistry $schemaRegistry): \Generator { - foreach ($clients as $operationGroup => $operations) { - yield from Clients::generate( - $operationGroup, - self::dirname($namespace . 'Operation/' . $operationGroup), - $namespace, - self::basename($namespace . 'Operation/' . $operationGroup), - $operations, - ); + $paths[] = \ApiClients\Tools\OpenApiClientGenerator\Gatherer\Path::gather($pathClassName, $path, $pathItem, $schemaRegistry); } - yield from ClientInterface::generate( - $namespace, - $clients, - $schemaRegistry, - ); - yield from Client::generate( - $namespace, - $clients, - $schemaRegistry, - ); - })($clients, $namespace, $schemaRegistry); - - if (count($this->spec->webhooks ?? []) > 0) { - $pathClassNameMapping = []; - foreach ($this->spec->webhooks as $path => $pathItem) { - $webHookClassName = self::className($path); - $pathClassNameMapping[$path] = $this->fqcn($namespace . 'WebHook/' . $webHookClassName); - if (strlen($webHookClassName) === 0) { - continue; - } + } - yield from WebHook::generate( - $path, - self::dirname($namespace . 'WebHook/' . $webHookClassName), + $hydrators = []; + $operations = []; + foreach ($paths as $path) { + $hydrators[] = $path->hydrator; + $operations = [...$operations, ...$path->operations]; + foreach ($path->operations as $operation) { + yield from Operation::generate( $namespace, - self::basename($namespace . 'WebHook/' . $webHookClassName), - $pathItem, + $operation, + $path->hydrator, $schemaRegistry, - $namespace ); } - - yield from WebHooks::generate( - self::dirname($namespace . 'WebHooks'), - $namespace, - $pathClassNameMapping, - ); } while ($schemaRegistry->hasUnknownSchemas()) { foreach ($schemaRegistry->unknownSchemas() as $schema) { - $schemaClasses[] = $namespace . 'Schema/' . $schema['className']; - yield from Schema::generate( - $schema['name'], - self::dirname($namespace . 'Schema/' . $schema['className']), - self::basename($namespace . 'Schema/' . $schema['className']), - $schema['schema'], - $schemaRegistry, - $namespace . 'Schema' - ); + $schemas[] = \ApiClients\Tools\OpenApiClientGenerator\Gatherer\Schema::gather($schema['className'], $schema['schema'], $schemaRegistry); } } - yield new File( - $namespace . 'Hydrator', - (new ObjectMapperCodeGenerator())->dump( - array_unique(array_filter(array_map(static fn (string $className): string => str_replace('/', '\\', $className), $schemaClasses), static fn (string $className): bool => count((new \ReflectionMethod($className, '__construct'))->getParameters()) > 0)), - $namespace . 'Hydrator' - ) - ); - } + foreach ($schemas as $schema) { + yield from Schema::generate( + $namespace, + $schema, + $schemaRegistry, + ); + } - private static function fqcn(string $fqcn): string - { - return str_replace('/', '\\', $fqcn); - } + $client = \ApiClients\Tools\OpenApiClientGenerator\Gatherer\Client::gather($this->spec, ...$paths); - public static function dirname(string $fqcn): string - { - $fqcn = str_replace('\\', '/', $fqcn); + yield from ClientInterface::generate( + $namespace, + $operations, + ); + yield from Client::generate( + $namespace, + $client, + ); - return self::cleanUpNamespace(dirname($fqcn)); - } + $webHooksHydrators = []; + foreach ($webHooks as $event => $webHook) { + $webHooksHydrators[$event] = $hydrators[] = WebHookHydrator::gather( + $event, + ...$webHook, + ); + yield from WebHook::generate( + $namespace, + $event, + $schemaRegistry, + ...$webHook, + ); + } - public static function basename(string $fqcn): string - { - $fqcn = str_replace('\\', '/', $fqcn); + yield from WebHooks::generate($namespace, $webHooksHydrators, $webHooks); - return self::cleanUpNamespace(basename($fqcn)); - } + foreach ($hydrators as $hydrator) { + yield from Hydrator::generate($namespace, $hydrator); + } - private static function isKeyword(string $name): bool - { - return in_array(strtolower($name), array('__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'self', 'parent', 'object'), false); + yield from Hydrators::generate($namespace, ...$hydrators); } } diff --git a/src/Generator/Client.php b/src/Generator/Client.php index 8b40987..1bc01d3 100644 --- a/src/Generator/Client.php +++ b/src/Generator/Client.php @@ -3,10 +3,13 @@ namespace ApiClients\Tools\OpenApiClientGenerator\Generator; use ApiClients\Contracts\HTTP\Headers\AuthenticationInterface; +use ApiClients\Contracts\OpenAPI\WebHooksInterface; use ApiClients\Tools\OpenApiClientGenerator\File; -use ApiClients\Tools\OpenApiClientGenerator\SchemaRegistry; +use ApiClients\Tools\OpenApiClientGenerator\Utils; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; use cebe\openapi\spec\Operation as OpenAPiOperation; use cebe\openapi\spec\PathItem; +use EventSauce\ObjectHydrator\ObjectMapper; use Jawira\CaseConverter\Convert; use League\OpenAPIValidation\Schema\Exception\SchemaMismatch; use PhpParser\Builder\Param; @@ -26,13 +29,18 @@ final class Client { /** - * @return iterable - * @throws \Jawira\CaseConverter\CaseConverterException + * @param string $namespace + * @return iterable */ - public static function generate(string $namespace, array $clients, SchemaRegistry $schemaRegistry): iterable + public static function generate(string $namespace, \ApiClients\Tools\OpenApiClientGenerator\Representation\Client $client): iterable { + $operations = []; + foreach ($client->paths as $path) { + $operations = [...$operations, ...$path->operations]; + } + $factory = new BuilderFactory(); - $stmt = $factory->namespace(rtrim($namespace, '\\')); + $stmt = $factory->namespace(trim($namespace, '\\')); $class = $factory->class('Client')->implement(new Node\Name('ClientInterface'))->makeFinal()->addStmt( $factory->property('authentication')->setType('\\' . AuthenticationInterface::class)->makeReadonly()->makePrivate() @@ -43,7 +51,15 @@ public static function generate(string $namespace, array $clients, SchemaRegistr )->addStmt( $factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() )->addStmt( - $factory->property('hydrator')->setType('\\' . $namespace . 'Hydrator')->makeReadonly()->makePrivate() + $factory->property('hydrator')->setType('array')->setDefault([])->makePrivate()->setDocComment(new Doc(implode(PHP_EOL, [ + '/**', + ' * @var array', + ' */', + ]))), + )->addStmt( + $factory->property('webHooks')->setType('WebHooks')->makeReadonly()->makePrivate() + )->addStmt( + $factory->property('hydrators')->setType('Hydrators')->makeReadonly()->makePrivate() )->addStmt( $factory->method('__construct')->makePublic()->addParam( (new Param('authentication'))->setType('\\' . AuthenticationInterface::class) @@ -57,18 +73,26 @@ public static function generate(string $namespace, array $clients, SchemaRegistr ) )->addParam( (new Param('browser'))->setType('\\' . Browser::class) - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'browser' - ), - new Node\Expr\New_( - new Node\Name('\\' . Browser::class), - [] - ), - ) - )->addStmt( + )->addStmt((static function (\ApiClients\Tools\OpenApiClientGenerator\Representation\Client $client): Node\Expr { + $assignExpr = new Node\Expr\Variable('browser'); + + if ($client->baseUrl !== null) { + $assignExpr = new Node\Expr\MethodCall( + $assignExpr, + 'withBase', + [ + new Arg( + new Node\Scalar\String_($client->baseUrl), + ), + ], + ); + } + + return new Node\Expr\Assign(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'browser' + ), $assignExpr); + })($client))->addStmt( new Node\Expr\Assign( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), @@ -104,96 +128,56 @@ public static function generate(string $namespace, array $clients, SchemaRegistr new Node\Expr\Assign( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), - 'hydrator' + 'hydrators' ), new Node\Expr\New_( - new Node\Name('\\' . $namespace . 'Hydrator'), + new Node\Name('Hydrators'), + [] + ), + ) + )->addStmt( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'webHooks' + ), + new Node\Expr\New_( + new Node\Name('WebHooks'), [ + new Node\Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + )), + new Node\Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrators' + )), ] ), ) ) ); - $rawCallReturnTypes = []; - $operationCalls = []; - $callReturnTypes = []; - - foreach ($clients as $operationGroup => $operations) { - $cn = str_replace('/', '\\', '\\' . $namespace . 'Operation/' . $operationGroup); - $casedOperationgroup = lcfirst($operationGroup); - foreach ($operations as $operationOperation => $operationDetails) { - $returnType = []; - foreach ($operationDetails['operation']->responses as $code => $spec) { - $contentTypeCases = []; - foreach ($spec->content as $contentType => $contentTypeSchema) { - $fallbackName = 'Operation\\' . $operationGroup . '\\Response\\' . (new Convert(str_replace('/', '\\', $contentType) . '\\H' . $code ))->toPascal(); - $object = '\\' . $namespace . 'Schema\\' . $schemaRegistry->get($contentTypeSchema->schema, $fallbackName); - $callReturnTypes[] = ($contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class . '<' : '') . $object . ($contentTypeSchema->schema->type === 'array' ? '>' : ''); - $rawCallReturnTypes[] = $contentTypeCases[] = $returnType[] = $contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class : $object; - } - if (count($contentTypeCases) === 0) { - $rawCallReturnTypes[] = $returnType[] = $callReturnTypes[] = 'int'; - } - } - $operationCalls[] = [ - 'operationGroupMethod' => $casedOperationgroup, - 'operationMethod' => lcfirst($operationOperation), - 'className' => str_replace('/', '\\', '\\' . $namespace . 'Operation\\' . $operationDetails['class']), - 'params' => iterator_to_array((function (array $operationDetails): iterable { - foreach ($operationDetails['operation']->parameters as $parameter) { - yield $parameter->name; - } - })($operationDetails)), - 'returnType' => $returnType, - ]; - } - $class->addStmt( - $factory->method($casedOperationgroup)->setReturnType($cn)->addStmt( - new Node\Stmt\Return_( - new Node\Expr\New_( - new Node\Name( - $cn - ), - [ - new Node\Arg(new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'requestSchemaValidator' - )), - new Node\Arg(new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'responseSchemaValidator' - )), - new Node\Arg(new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'hydrator' - )), - ] - ) - ) - )->makePublic() - ); - } - $class->addStmt( $factory->method('call')->makePublic()->setDocComment( new Doc(implode(PHP_EOL, [ '/**', - ' * @return ' . (function (array $operationCalls): string { - $count = count($operationCalls); + ' * @return ' . (function (string $namespace, array $operations): string { + $count = count($operations); $lastItem = $count - 1; $left = ''; $right = ''; for ($i = 0; $i < $count; $i++) { + $returnType = implode('|', array_map(static fn (string $className): string => strpos($className, '\\') === 0 ? $className : $namespace . 'Schema\\' . $className, array_unique($operations[$i]->returnType))); if ($i !== $lastItem) { - $left .= '($call is ' . $operationCalls[$i]['className'] . '::OPERATION_MATCH ? ' . implode('|', array_unique($operationCalls[$i]['returnType'])) . ' : '; + $left .= '($call is ' . $namespace . 'Operation\\' . $operations[$i]->classNameSanitized . '::OPERATION_MATCH ? ' . $returnType . ' : '; } else { - $left .= implode('|', array_unique($operationCalls[$i]['returnType'])); + $left .= $returnType; } $right .= ')'; } return $left . $right; - })($operationCalls), + })($namespace, $operations), ' */', ])) )->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->addStmt(new Node\Stmt\Return_( @@ -219,151 +203,211 @@ public static function generate(string $namespace, array $clients, SchemaRegistr $factory->method('callAsync')->makePublic()->setDocComment( new Doc(implode(PHP_EOL, [ '/**', - ' * @return ' . (function (array $operationCalls): string { - $count = count($operationCalls); + ' * @return ' . (function (string $namespace, array $operations): string { + $count = count($operations); $lastItem = $count - 1; $left = ''; $right = ''; for ($i = 0; $i < $count; $i++) { + $returnType = implode('|', array_map(static fn (string $className): string => strpos($className, '\\') === 0 ? $className : $namespace . 'Schema\\' . $className, array_unique($operations[$i]->returnType))); if ($i !== $lastItem) { - $left .= '($call is ' . $operationCalls[$i]['className'] . '::OPERATION_MATCH ? \\' . PromiseInterface::class . '<' . implode('|', array_unique($operationCalls[$i]['returnType'])) . '> : '; + $left .= '($call is ' . $namespace . 'Operation\\' . $operations[$i]->classNameSanitized . '::OPERATION_MATCH ? ' . '\\' . PromiseInterface::class . '<' . $returnType . '>' . ' : '; } else { - $left .= '\\' . PromiseInterface::class . '<' . implode('|', array_unique($operationCalls[$i]['returnType'])) . '>'; + $left .= '\\' . PromiseInterface::class . '<' . $returnType . '>'; } $right .= ')'; } return $left . $right; - })($operationCalls), + })($namespace, $operations), ' */', ])) )->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->addStmt(new Node\Stmt\Switch_( new Node\Expr\Variable('call'), - iterator_to_array((function (iterable $operationCalls) use ($factory): iterable { - foreach ($operationCalls as $operationCall) { - yield new Node\Stmt\Case_( - new Node\Expr\ClassConstFetch(new Node\Name($operationCall['className']), 'OPERATION_MATCH'), - [ - new Node\Stmt\Expression(new Node\Expr\Assign( - new Node\Expr\Variable('requestBodyData'), - new Node\Expr\Array_(), - )), - new Node\Stmt\Foreach_(new Node\Expr\FuncCall( - new Node\Name('\array_keys'), - [ - new Arg(new Node\Expr\Variable(new Node\Name('params'))), - ], - ), new Node\Expr\Variable(new Node\Name('param')), [ - 'stmts' => [ - new Node\Stmt\If_( - new Node\Expr\BinaryOp\NotEqual( - new Node\Expr\FuncCall( - new Node\Name('\in_array'), - [ - new Arg(new Node\Expr\Variable(new Node\Name('param'))), - new Arg(new Node\Expr\Array_( - iterator_to_array((function (array $params): iterable { - foreach ($params as $param) { - yield new Node\Expr\ArrayItem(new Node\Scalar\String_($param)); - } - })($operationCall['params'])), - )), - ], + iterator_to_array((function (string $namespace, array $paths) use ($factory): iterable { + foreach ($paths as $path) { + foreach ($path->operations as $operation) { + $operationClassname = $namespace . 'Operation\\' . Utils::className(str_replace('/', '\\', $operation->className)); + yield new Node\Stmt\Case_( + new Node\Expr\ClassConstFetch(new Node\Name($operationClassname), 'OPERATION_MATCH'), + [ + new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('requestBodyData'), + new Node\Expr\Array_(), + )), + new Node\Stmt\Foreach_(new Node\Expr\FuncCall( + new Node\Name('\array_keys'), + [ + new Arg(new Node\Expr\Variable(new Node\Name('params'))), + ], + ), new Node\Expr\Variable(new Node\Name('param')), [ + 'stmts' => [ + new Node\Stmt\If_( + new Node\Expr\BinaryOp\NotEqual( + new Node\Expr\FuncCall( + new Node\Name('\in_array'), + [ + new Arg(new Node\Expr\Variable(new Node\Name('param'))), + new Arg(new Node\Expr\Array_( + iterator_to_array((function (array $params): iterable { + foreach ($params as $param) { + yield new Node\Expr\ArrayItem(new Node\Scalar\String_($param->name)); + } + })($operation->parameters)), + )), + ], + ), + new Node\Expr\ConstFetch(new Node\Name('false')) ), - new Node\Expr\ConstFetch(new Node\Name('false')) - ), - [ - 'stmts' => [ - new Node\Stmt\Expression( - new Node\Expr\FuncCall( - new Node\Name('\array_push'), - [ - new Arg(new Node\Expr\Variable(new Node\Name('requestBodyData'))), - new Arg(new Node\Expr\Variable(new Node\Name('param'))), - ], + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\FuncCall( + new Node\Name('\array_push'), + [ + new Arg(new Node\Expr\Variable(new Node\Name('requestBodyData'))), + new Arg(new Node\Expr\Variable(new Node\Name('param'))), + ], + ), ), - ), + ], + ] + ), + ], + ]), + ...(implode('|', $operation->returnType) === ('\\' . ResponseInterface::class) ? [] : [new Node\Stmt\If_( + new Node\Expr\BinaryOp\Equal( + new Node\Expr\FuncCall( + new Node\Name('\array_key_exists'), + [ + new Arg(new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Hydrator\\' . $path->hydrator->className), + new Node\Name('class'), + )), + new Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + )), ], - ] - ), - ], - ]), - new Node\Stmt\Expression(new Node\Expr\Assign( - new Node\Expr\Variable('operation'), - new Node\Expr\MethodCall( - new Node\Expr\MethodCall( - new Node\Expr\Variable('this'), - $operationCall['operationGroupMethod'], - [], - ), - $operationCall['operationMethod'], - iterator_to_array((function (array $params): iterable { - foreach ($params as $param) { - yield new Arg(new Node\Expr\ArrayDimFetch(new Node\Expr\Variable(new Node\Name('params')), new Node\Scalar\String_($param))); - } - })($operationCall['params'])), - ) - )), - new Node\Stmt\Expression(new Node\Expr\Assign(new Node\Expr\Variable('request'), new Node\Expr\MethodCall(new Node\Expr\Variable('operation'), 'createRequest', [ - new Arg(new Node\Expr\Variable(new Node\Name('requestBodyData'))) - ]))), - new Node\Stmt\Return_(new Node\Expr\MethodCall( - new Node\Expr\MethodCall( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'browser' + ), + new Node\Expr\ConstFetch(new Node\Name('false')) ), - 'request', [ - new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getMethod'),), - new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getUri'),), - new Node\Arg( - new Node\Expr\MethodCall( + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\ArrayDimFetch(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Hydrator\\' . $path->hydrator->className), + new Node\Name('class'), + )), + new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrators' + ), + 'getObjectMapper' . ucfirst($path->hydrator->methodName), + ) + ), + ), + ], + ] + )]), + new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('operation'), + new Node\Expr\New_( + new Node\Name($operationClassname), + [ + ...(count($operation->requestBody) > 0 ? [ + new Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + )), + ] : []), + ...(implode('|', $operation->returnType) === ('\\' . ResponseInterface::class) ? [] : [ + new Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'responseSchemaValidator' + )), + new Arg(new Node\Expr\ArrayDimFetch(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Hydrator\\' . $path->hydrator->className), + new Node\Name('class'), + ))), + ]), + ...iterator_to_array((function (array $params): iterable { + foreach ($params as $param) { + yield new Arg(new Node\Expr\ArrayDimFetch(new Node\Expr\Variable(new Node\Name('params')), new Node\Scalar\String_($param->name))); + } + })($operation->parameters)), + ], + ) + )), + new Node\Stmt\Expression(new Node\Expr\Assign(new Node\Expr\Variable('request'), new Node\Expr\MethodCall(new Node\Expr\Variable('operation'), 'createRequest', [ + new Arg(new Node\Expr\Variable(new Node\Name('requestBodyData'))) + ]))), + new Node\Stmt\Return_(new Node\Expr\MethodCall( + new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'browser' + ), + 'request', + [ + new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getMethod'),), + new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getUri'),), + new Node\Arg( new Node\Expr\MethodCall( - new Node\Expr\Variable('request'), - 'withHeader', - [ - new Node\Arg(new Node\Scalar\String_('Authorization')), - new Node\Arg( - new Node\Expr\MethodCall( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'authentication' + new Node\Expr\MethodCall( + new Node\Expr\Variable('request'), + 'withHeader', + [ + new Node\Arg(new Node\Scalar\String_('Authorization')), + new Node\Arg( + new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'authentication' + ), + 'authHeader', ), - 'authHeader', ), - ), - ] + ] + ), + 'getHeaders' ), - 'getHeaders' ), - ), - new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getBody'),), + new Node\Arg(new Node\Expr\MethodCall(new Node\Expr\Variable('request'), 'getBody'),), + ] + ), + 'then', + [ + new Arg(new Node\Expr\Closure([ + 'stmts' => [ + new Node\Stmt\Return_(new Node\Expr\MethodCall(new Node\Expr\Variable('operation'), 'createResponse', [ + new Node\Expr\Variable('response') + ])), + ], + 'params' => [ + new Node\Param(new Node\Expr\Variable('response'), null, new Node\Name('\\' . ResponseInterface::class)) + ], + 'uses' => [ + new Node\Expr\Variable('operation'), + ], + 'returnType' => count($operation->returnType) > 0 ? new Node\UnionType(array_map(static fn(string $object): Node\Name => new Node\Name(strpos($object, '\\') === 0 ? $object : $namespace . 'Schema\\' . $object), array_unique($operation->returnType))) : null, + ])) ] - ), - 'then', - [ - new Arg(new Node\Expr\Closure([ - 'stmts' => [ - new Node\Stmt\Return_(new Node\Expr\MethodCall(new Node\Expr\Variable('operation'), 'createResponse', [ - new Node\Expr\Variable('response') - ])), - ], - 'params' => [ - new Node\Param(new Node\Expr\Variable('response'), null, new Node\Name('\\' . ResponseInterface::class)) - ], - 'uses' => [ - new Node\Expr\Variable('operation'), - ], - 'returnType' => count($operationCall['returnType']) > 0 ? new Node\UnionType(array_map(static fn (string $object): Node\Name => new Node\Name($object), array_unique($operationCall['returnType']))) : null, - ])) - ] - )), - new Node\Stmt\Break_(), - ] - ); -// yield new Node\Stmt\Echo_([new Node\Scalar\String_('/**' . @var_export($operationCall, true) . '*/')]); + )), + new Node\Stmt\Break_(), + ] + ); + // yield new Node\Stmt\Echo_([new Node\Scalar\String_('/**' . @var_export($operationCall, true) . '*/')]); + } } - })($operationCalls)) + })($namespace, $client->paths)) ))->addStmt( new Node\Stmt\Throw_( new Node\Expr\New_( @@ -374,33 +418,14 @@ public static function generate(string $namespace, array $clients, SchemaRegistr ); $class->addStmt( - $factory->method('hydrateObject')->makePublic()->setDocComment( - new Doc(implode(PHP_EOL, [ - '/**', - ' * @template H', - ' * @param class-string $className', - ' * @return H', - ' */', - ])) - )->setReturnType('object')->addParam( - (new Param('className'))->setType('string') - )->addParam( - (new Param('data'))->setType('array') - )->addStmt(new Node\Stmt\Return_( - new Node\Expr\MethodCall( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'hydrator' - ), - new Node\Name('hydrateObject'), - [ - new Node\Arg(new Node\Expr\Variable('className')), - new Node\Arg(new Node\Expr\Variable('data')), - ] - ) + $factory->method('webHooks')->makePublic()->setReturnType('\\' . WebHooksInterface::class)->addStmt(new Node\Stmt\Return_( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'webHooks' + ), )) ); - yield new File($namespace . '\\' . 'Client', $stmt->addStmt($class)->getNode()); + yield new File($namespace . 'Client', $stmt->addStmt($class)->getNode()); } } diff --git a/src/Generator/ClientInterface.php b/src/Generator/ClientInterface.php index b93e9b4..cd90339 100644 --- a/src/Generator/ClientInterface.php +++ b/src/Generator/ClientInterface.php @@ -3,8 +3,9 @@ namespace ApiClients\Tools\OpenApiClientGenerator\Generator; use ApiClients\Contracts\HTTP\Headers\AuthenticationInterface; +use ApiClients\Contracts\OpenAPI\WebHooksInterface; use ApiClients\Tools\OpenApiClientGenerator\File; -use ApiClients\Tools\OpenApiClientGenerator\SchemaRegistry; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; use cebe\openapi\spec\Operation as OpenAPiOperation; use cebe\openapi\spec\PathItem; use Jawira\CaseConverter\Convert; @@ -26,72 +27,74 @@ final class ClientInterface { /** - * @return iterable - * @throws \Jawira\CaseConverter\CaseConverterException + * @param string $namespace + * @param array<\ApiClients\Tools\OpenApiClientGenerator\Representation\Operation> $paths + * @return iterable */ - public static function generate(string $namespace, array $clients, SchemaRegistry $schemaRegistry): iterable + public static function generate(string $namespace, array $operations): iterable { $factory = new BuilderFactory(); - $stmt = $factory->namespace(rtrim($namespace, '\\')); + $stmt = $factory->namespace(trim($namespace, '\\')); $class = $factory->interface('ClientInterface'); - $rawCallReturnTypes = []; - $operationCalls = []; - $callReturnTypes = []; - - foreach ($clients as $operationGroup => $operations) { - $cn = str_replace('/', '\\', '\\' . $namespace . 'Operation/' . $operationGroup); - $casedOperationgroup = lcfirst($operationGroup); - foreach ($operations as $operationOperation => $operationDetails) { - $returnType = []; - foreach ($operationDetails['operation']->responses as $code => $spec) { - $contentTypeCases = []; - foreach ($spec->content as $contentType => $contentTypeSchema) { - $fallbackName = 'Operation\\' . $operationGroup . '\\Response\\' . (new Convert(str_replace('/', '\\', $contentType) . '\\H' . $code ))->toPascal(); - $object = '\\' . $namespace . 'Schema\\' . $schemaRegistry->get($contentTypeSchema->schema, $fallbackName); - $callReturnTypes[] = ($contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class . '<' : '') . $object . ($contentTypeSchema->schema->type === 'array' ? '>' : ''); - $rawCallReturnTypes[] = $contentTypeCases[] = $returnType[] = $contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class : $object; - } - if (count($contentTypeCases) === 0) { - $rawCallReturnTypes[] = $returnType[] = $callReturnTypes[] = 'int'; - } - } - $operationCalls[] = [ - 'operationGroupMethod' => $casedOperationgroup, - 'operationMethod' => lcfirst($operationOperation), - 'className' => str_replace('/', '\\', '\\' . $namespace . 'Operation\\' . $operationDetails['class']), - 'params' => iterator_to_array((function (array $operationDetails): iterable { - foreach ($operationDetails['operation']->parameters as $parameter) { - yield $parameter->name; - } - })($operationDetails)), - 'returnType' => $returnType, - ]; - } - $class->addStmt( - $factory->method($casedOperationgroup)->setReturnType($cn)->makePublic() - ); - } - +// $rawCallReturnTypes = []; +// $operationCalls = []; +// $callReturnTypes = []; +// +// foreach ($clients as $operationGroup => $operations) { +// $cn = str_replace('/', '\\', '\\' . $namespace . 'Operation/' . $operationGroup); +// $casedOperationgroup = lcfirst($operationGroup); +// foreach ($operations as $operationOperation => $operationDetails) { +// $returnType = []; +// foreach ($operationDetails['operation']->responses as $code => $spec) { +// $contentTypeCases = []; +// foreach ($spec->content as $contentType => $contentTypeSchema) { +// $fallbackName = 'Operation\\' . $operationGroup . '\\Response\\' . (new Convert(str_replace('/', '\\', $contentType) . '\\H' . $code ))->toPascal(); +// $object = '\\' . $namespace . 'Schema\\' . $schemaRegistry->get($contentTypeSchema->schema, $fallbackName); +// $callReturnTypes[] = ($contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class . '<' : '') . $object . ($contentTypeSchema->schema->type === 'array' ? '>' : ''); +// $rawCallReturnTypes[] = $contentTypeCases[] = $returnType[] = $contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class : $object; +// } +// if (count($contentTypeCases) === 0) { +// $rawCallReturnTypes[] = $returnType[] = $callReturnTypes[] = 'int'; +// } +// } +// $operationCalls[] = [ +// 'operationGroupMethod' => $casedOperationgroup, +// 'operationMethod' => lcfirst($operationOperation), +// 'className' => str_replace('/', '\\', '\\' . $namespace . 'Operation\\' . $operationDetails['class']), +// 'params' => iterator_to_array((function (array $operationDetails): iterable { +// foreach ($operationDetails['operation']->parameters as $parameter) { +// yield $parameter->name; +// } +// })($operationDetails)), +// 'returnType' => $returnType, +// ]; +// } +// $class->addStmt( +// $factory->method($casedOperationgroup)->setReturnType($cn)->makePublic() +// ); +// } +// $class->addStmt( $factory->method('call')->makePublic()->setDocComment( new Doc(implode(PHP_EOL, [ '/**', - ' * @return ' . (function (array $operationCalls): string { - $count = count($operationCalls); + ' * @return ' . (function (string $namespace, array $operations): string { + $count = count($operations); $lastItem = $count - 1; $left = ''; $right = ''; for ($i = 0; $i < $count; $i++) { + $returnType = implode('|', array_map(static fn (string $className): string => strpos($className, '\\') === 0 ? $className : $namespace . 'Schema\\' . $className, array_unique($operations[$i]->returnType))); if ($i !== $lastItem) { - $left .= '($call is ' . $operationCalls[$i]['className'] . '::OPERATION_MATCH ? ' . implode('|', array_unique($operationCalls[$i]['returnType'])) . ' : '; + $left .= '($call is ' . $namespace . 'Operation\\' . $operations[$i]->classNameSanitized . '::OPERATION_MATCH ? ' . $returnType . ' : '; } else { - $left .= implode('|', array_unique($operationCalls[$i]['returnType'])); + $left .= $returnType; } $right .= ')'; } return $left . $right; - })($operationCalls), + })($namespace, $operations), ' */', ])) )->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([])) @@ -101,42 +104,47 @@ public static function generate(string $namespace, array $clients, SchemaRegistr $factory->method('callAsync')->makePublic()->setDocComment( new Doc(implode(PHP_EOL, [ '/**', - ' * @return ' . (function (array $operationCalls): string { - $count = count($operationCalls); + ' * @return ' . (function (string $namespace,array $operations): string { + $count = count($operations); $lastItem = $count - 1; $left = ''; $right = ''; for ($i = 0; $i < $count; $i++) { + $returnType = implode('|', array_map(static fn (string $className): string => strpos($className, '\\') === 0 ? $className : $namespace . 'Schema\\' . $className, array_unique($operations[$i]->returnType))); if ($i !== $lastItem) { - $left .= '($call is ' . $operationCalls[$i]['className'] . '::OPERATION_MATCH ? \\' . PromiseInterface::class . '<' . implode('|', array_unique($operationCalls[$i]['returnType'])) . '> : '; + $left .= '($call is ' . $namespace . 'Operation\\' . $operations[$i]->classNameSanitized . '::OPERATION_MATCH ? ' . '\\' . PromiseInterface::class . '<' . $returnType . '>' . ' : '; } else { - $left .= '\\' . PromiseInterface::class . '<' . implode('|', array_unique($operationCalls[$i]['returnType'])) . '>'; + $left .= '\\' . PromiseInterface::class . '<' . $returnType . '>'; } $right .= ')'; } return $left . $right; - })($operationCalls), + })($namespace, $operations), ' */', ])) )->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([])) ); +// +// $class->addStmt( +// $factory->method('hydrateObject')->makePublic()->setDocComment( +// new Doc(implode(PHP_EOL, [ +// '/**', +// ' * @template H', +// ' * @param class-string $className', +// ' * @return H', +// ' */', +// ])) +// )->setReturnType('object')->addParam( +// (new Param('className'))->setType('string') +// )->addParam( +// (new Param('data'))->setType('array') +// ) +// ); $class->addStmt( - $factory->method('hydrateObject')->makePublic()->setDocComment( - new Doc(implode(PHP_EOL, [ - '/**', - ' * @template H', - ' * @param class-string $className', - ' * @return H', - ' */', - ])) - )->setReturnType('object')->addParam( - (new Param('className'))->setType('string') - )->addParam( - (new Param('data'))->setType('array') - ) + $factory->method('webHooks')->setReturnType('\\' . WebHooksInterface::class)->makePublic() ); - yield new File($namespace . '\\' . 'ClientInterface', $stmt->addStmt($class)->getNode()); + yield new File($namespace . 'ClientInterface', $stmt->addStmt($class)->getNode()); } } diff --git a/src/Generator/Clients.php b/src/Generator/Clients.php deleted file mode 100644 index e54268b..0000000 --- a/src/Generator/Clients.php +++ /dev/null @@ -1,116 +0,0 @@ - $operations - * @return iterable - */ - public static function generate(string $operationGroup, string $namespace, string $rootNamespace, string $className, array $operations): iterable - { - $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); - - $class = $factory->class($className)->makeFinal()->addStmt( - $factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() - )->addStmt( - $factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() - )->addStmt( - $factory->property('hydrator')->setType('\\' . $rootNamespace . 'Hydrator')->makeReadonly()->makePrivate() - )->addStmt( - $factory->method('__construct')->makePublic()->addParam( - (new Param('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'requestSchemaValidator' - ), - new Node\Expr\Variable('requestSchemaValidator'), - ) - )->addParam( - (new Param('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'responseSchemaValidator' - ), - new Node\Expr\Variable('responseSchemaValidator'), - ) - )->addParam( - (new Param('hydrator'))->setType('\\' . $rootNamespace . 'Hydrator') - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'hydrator' - ), - new Node\Expr\Variable('hydrator'), - ) - ) - ); - - foreach ($operations as $operationOperation => $operationDetails) { - $params = []; - $params[] = new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'requestSchemaValidator' - ); - $params[] = new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'responseSchemaValidator' - ); - $params[] = new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'hydrator' - ); - $cn = str_replace('/', '\\', '\\' . $namespace . '\\' . $operationDetails['class']); - $method = $factory->method(lcfirst($operationOperation))->setReturnType($cn)->makePublic(); - foreach ($operationDetails['operation']->parameters as $parameter) { - $params[] = new Node\Arg(new Node\Expr\Variable($parameter->name)); - $param = new Param($parameter->name); - if ($parameter->schema->type !== null) { - $param->setType( - str_replace([ - 'integer', - 'any', - 'boolean', - ], [ - 'int', - '', - 'bool', - ], implode('|', is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type])) - ); - } - if ($parameter->schema->default !== null) { - $param->setDefault($parameter->schema->default); - } - $method->addParam($param); - } - $class->addStmt($method->addStmt( - new Node\Stmt\Return_( - new Node\Expr\New_( - new Node\Name( - $cn - ), - $params - ) - ) - )); - } - - yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode()); - } -} diff --git a/src/Generator/Hydrator.php b/src/Generator/Hydrator.php new file mode 100644 index 0000000..2fd6d54 --- /dev/null +++ b/src/Generator/Hydrator.php @@ -0,0 +1,41 @@ + $operations + * @return iterable + */ + public static function generate(string $namespace, \ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator): iterable + { + $schemaClasses = []; + + foreach ($hydrator->schemas as $schema) { + $schemaClasses[] = trim($namespace, '\\') . '\\Schema\\' . $schema->className; + } + + if (count($schemaClasses) > 0) { + yield new File( + trim($namespace, '\\') . '\\HHydrator\\' . $hydrator->className, + (new ObjectMapperCodeGenerator())->dump( + array_unique(array_filter(array_map(static fn(string $className): string => str_replace('/', '\\', $className), $schemaClasses), static fn(string $className): bool => count((new \ReflectionMethod($className, '__construct'))->getParameters()) > 0)), + trim($namespace, '\\') . '\\Hydrator\\' . $hydrator->className + ) + ); + } + } +} diff --git a/src/Generator/Hydrators.php b/src/Generator/Hydrators.php new file mode 100644 index 0000000..4b28529 --- /dev/null +++ b/src/Generator/Hydrators.php @@ -0,0 +1,267 @@ +namespace(trim($namespace, '\\')); + + $class = $factory->class('Hydrators')->makeFinal()->implement('\\' . ObjectMapper::class); + + $usefullHydrators = []; + foreach ($hydrators as $hydrator) { + $usefullHydrators[$hydrator->className] = array_filter($hydrator->schemas, function (\ApiClients\Tools\OpenApiClientGenerator\Representation\Schema $schema) use (&$knownScehmas): bool { + if (array_key_exists($schema->className, $knownScehmas)) { + return false; + } + + $knownScehmas[$schema->className] = $schema->className; + return true; + }); + } + $hydrators = array_filter($hydrators, static fn (\ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator): bool => count($usefullHydrators[$hydrator->className]) > 0); + + foreach ($hydrators as $hydrator) { + $class->addStmt($factory->property($hydrator->methodName)->setType('?' . $namespace . 'Hydrator\\' . str_replace('/', '\\', $hydrator->className))->setDefault(null)->makePrivate()); + } + + $class->addStmt( + $factory->method('hydrateObject')->makePublic()->setReturnType('object')->addParams([ + (new Param('className'))->setType('string'), + (new Param('payload'))->setType('array'), + ])->addStmt( + new Node\Stmt\Return_( + new Node\Expr\Match_( + new Node\Expr\Variable('className'), + array_map(static fn (\ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator): Node\MatchArm => new Node\MatchArm( + array_map(static fn (\ApiClients\Tools\OpenApiClientGenerator\Representation\Schema $schema): Node\Scalar\String_ => new Node\Scalar\String_( + ltrim($namespace, '\\') . 'Schema\\' . $schema->className + ), $usefullHydrators[$hydrator->className]), + new Node\Expr\MethodCall( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'getObjectMapper' . ucfirst($hydrator->methodName), + ), + 'hydrateObject', + [ + new Node\Arg( + new Node\Expr\Variable('className') + ), + new Node\Arg( + new Node\Expr\Variable('payload') + ), + ] + ) + ), $hydrators) + ) + ) + ) + ); + + $class->addStmt( + $factory->method('hydrateObjects')->makePublic()->setReturnType('\\' . IterableList::class)->addParams([ + (new Param('className'))->setType('string'), + (new Param('payloads'))->setType('iterable'), + ])->addStmt( + new Node\Stmt\Return_( + new Node\Expr\New_( + new Node\Name('\\' . IterableList::class), + [ + new Node\Arg( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'doHydrateObjects', + [ + new Node\Arg( + new Node\Expr\Variable('className') + ), + new Node\Arg( + new Node\Expr\Variable('payloads') + ), + ] + ) + ) + ] + ) + ) + ) + ); + + $class->addStmt( + $factory->method('doHydrateObjects')->makePrivate()->setReturnType('\\' . \Generator::class)->addParams([ + (new Param('className'))->setType('string'), + (new Param('payloads'))->setType('iterable'), + ])->addStmt( + new Node\Stmt\Foreach_( + new Node\Expr\Variable('payloads'), + new Node\Expr\Variable('payload'), + [ + 'keyVar' => new Node\Expr\Variable('index'), + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Yield_( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'hydrateObject', + [ + new Node\Arg( + new Node\Expr\Variable('className') + ), + new Node\Arg( + new Node\Expr\Variable('payload') + ), + ] + ), + new Node\Expr\Variable('index'), + ) + ) + ], + ], + ) + ) + ); + + $class->addStmt( + $factory->method('serializeObject')->makePublic()->setReturnType('mixed')->addParams([ + (new Param('object'))->setType('object'), + ])->addStmt( + new Node\Stmt\Return_( + new Node\Expr\Match_( + new Node\Expr\ClassConstFetch( + new Node\Expr\Variable('object'), + 'class' + ), + array_map(static fn (\ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator): Node\MatchArm => new Node\MatchArm( + array_map(static fn (\ApiClients\Tools\OpenApiClientGenerator\Representation\Schema $schema): Node\Scalar\String_ => new Node\Scalar\String_( + ltrim($namespace, '\\') . 'Schema\\' . $schema->className + ), $usefullHydrators[$hydrator->className]), + new Node\Expr\MethodCall( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'getObjectMapper' . ucfirst($hydrator->methodName), + ), + 'serializeObject', + [ + new Node\Arg( + new Node\Expr\Variable('object') + ), + ] + ) + ), $hydrators) + ) + ) + ) + ); + + $class->addStmt( + $factory->method('serializeObjects')->makePublic()->setReturnType('\\' . IterableList::class)->addParams([ + (new Param('payloads'))->setType('iterable'), + ])->addStmt( + new Node\Stmt\Return_( + new Node\Expr\New_( + new Node\Name('\\' . IterableList::class), + [ + new Node\Arg( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'doSerializeObjects', + [ + new Node\Arg( + new Node\Expr\Variable('payloads') + ) + ] + ) + ) + ] + ) + ) + ) + ); + + $class->addStmt( + $factory->method('doSerializeObjects')->makePrivate()->setReturnType('\\' . \Generator::class)->addParams([ + (new Param('objects'))->setType('iterable'), + ])->addStmt( + new Node\Stmt\Foreach_( + new Node\Expr\Variable('objects'), + new Node\Expr\Variable('object'), + [ + 'keyVar' => new Node\Expr\Variable('index'), + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Yield_( + new Node\Expr\MethodCall( + new Node\Expr\Variable('this'), + 'serializeObject', + [ + new Node\Arg( + new Node\Expr\Variable('object') + ) + ] + ), + new Node\Expr\Variable('index'), + ) + ) + ], + ], + ) + ) + ); + + foreach ($hydrators as $hydrator) { + $class->addStmt( + $factory->method('getObjectMapper' . ucfirst($hydrator->methodName))->makePublic()->setReturnType($namespace . 'Hydrator\\' . str_replace('/', '\\', $hydrator->className))->addStmts([ + new Node\Stmt\If_( + new Node\Expr\BinaryOp\Identical( + new Node\Expr\Instanceof_( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $hydrator->methodName + ), + new Node\Expr\ConstFetch(new Node\Name($namespace . 'Hydrator\\' . str_replace('/', '\\', $hydrator->className))), + ), + new Node\Expr\ConstFetch(new Node\Name('false')), + ), + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $hydrator->methodName + ), + new Node\Expr\New_( + new Node\Name($namespace . 'Hydrator\\' . str_replace('/', '\\', $hydrator->className)) + ), + ), + ), + ], + ] + ), + new Node\Stmt\Return_( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $hydrator->methodName + ), + ), + ]) + ); + + } + + yield new File($namespace . 'Hydrators', $stmt->addStmt($class)->getNode()); + } +} diff --git a/src/Generator/Operation.php b/src/Generator/Operation.php index cd5c922..c644bd6 100644 --- a/src/Generator/Operation.php +++ b/src/Generator/Operation.php @@ -3,9 +3,10 @@ namespace ApiClients\Tools\OpenApiClientGenerator\Generator; use ApiClients\Tools\OpenApiClientGenerator\File; -use ApiClients\Tools\OpenApiClientGenerator\SchemaRegistry; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; +use ApiClients\Tools\OpenApiClientGenerator\Representation\OperationResponse; +use ApiClients\Tools\OpenApiClientGenerator\Utils; use cebe\openapi\spec\Operation as OpenAPiOperation; -use Jawira\CaseConverter\Convert; use PhpParser\Builder\Param; use PhpParser\BuilderFactory; use PhpParser\Comment\Doc; @@ -14,11 +15,9 @@ use PhpParser\Node\Stmt\Class_; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use React\Promise\PromiseInterface; use RingCentral\Psr7\Request; use Rx\Observable; use Rx\Scheduler\ImmediateScheduler; -use WyriHaximus\Hydrator\Hydrator; final class Operation { @@ -30,12 +29,13 @@ final class Operation * @param OpenAPiOperation $operation * @return iterable */ - public static function generate(string $path, string $method, string $namespace, string $rootNamespace, string $className, OpenAPiOperation $operation, SchemaRegistry $schemaRegistry): iterable + public static function generate(string $namespace, \ApiClients\Tools\OpenApiClientGenerator\Representation\Operation $operation, \ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator, SchemaRegistry $schemaRegistry): iterable { + $noHydrator = false; $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); + $stmt = $factory->namespace(ltrim(Utils::dirname($namespace . '\\Operation\\' . $operation->className), '\\')); - $class = $factory->class($className)->makeFinal()->addStmt( + $class = $factory->class(Utils::className(ltrim(Utils::basename($operation->className), '\\')))->makeFinal()->addStmt( new Node\Stmt\ClassConst( [ new Node\Const_( @@ -45,7 +45,7 @@ public static function generate(string $path, string $method, string $namespace, ) ), ], - Class_::MODIFIER_PRIVATE + Class_::MODIFIER_PUBLIC ) )->addStmt( new Node\Stmt\ClassConst( @@ -53,100 +53,78 @@ public static function generate(string $path, string $method, string $namespace, new Node\Const_( 'OPERATION_MATCH', new Node\Scalar\String_( - strtoupper($method) . ' ' . $path, // Deal with the query + strtoupper($operation->method) . ' ' . $operation->path, // Deal with the query ) ), ], Class_::MODIFIER_PUBLIC ) )->addStmt( - $factory->method('operationId')->makePublic()->setReturnType('string')->addStmt( - new Node\Stmt\Return_( - new Node\Expr\ClassConstFetch( - new Node\Name('self'), - 'OPERATION_ID' - ) - ) - ) - )->addStmt( - $factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() - )->addStmt( - $factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() - )->addStmt( - $factory->property('hydrator')->setType('\\' . $rootNamespace . 'Hydrator')->makeReadonly()->makePrivate() - ); - - $constructor = $factory->method('__construct')->makePublic()->addParam( - (new Param('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'requestSchemaValidator' - ), - new Node\Expr\Variable('requestSchemaValidator'), - ) - )->addParam( - (new Param('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'responseSchemaValidator' - ), - new Node\Expr\Variable('responseSchemaValidator'), + new Node\Stmt\ClassConst( + [ + new Node\Const_( + 'METHOD', + new Node\Scalar\String_( + strtoupper($operation->method), + ) + ), + ], + Class_::MODIFIER_PRIVATE ) - )->addParam( - (new Param('hydrator'))->setType('\\' . $rootNamespace . 'Hydrator') )->addStmt( - new Node\Expr\Assign( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'hydrator' - ), - new Node\Expr\Variable('hydrator'), + new Node\Stmt\ClassConst( + [ + new Node\Const_( + 'PATH', + new Node\Scalar\String_( + $operation->path, // Deal with the query + ) + ), + ], + Class_::MODIFIER_PRIVATE ) ); + if (count($operation->requestBody) > 0) { + $class->addStmt( + $factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() + ); + } + $constructor = $factory->method('__construct')->makePublic(); + + if (count($operation->requestBody) > 0) { + $constructor->addParam( + (new Param('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') + )->addStmt( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + ), + new Node\Expr\Variable('requestSchemaValidator'), + ) + ); + } $requestReplaces = []; $query = []; + $constructorParams = []; foreach ($operation->parameters as $parameter) { $paramterStmt = $factory->property($parameter->name); - if (strlen((string)$parameter->description) > 0) { + $param = new Param($parameter->name); + if (strlen($parameter->description) > 0) { $paramterStmt->setDocComment('/**' . (string)$parameter->description . '**/'); } - if ($parameter->schema->type !== null) { - $paramterStmt->setType(str_replace([ - 'integer', - 'any', - 'boolean', - ], [ - 'int', - '', - 'bool', - ], implode('|', is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type]))); + if ($parameter->type !== '') { + $paramterStmt->setType($parameter->type); + + $param->setType($parameter->type); } $class->addStmt($paramterStmt->makePrivate()); - $param = new Param($parameter->name); - if ($parameter->schema->type !== null) { - $param->setType( - str_replace([ - 'integer', - 'any', - 'boolean', - ], [ - 'int', - '', - 'bool', - ], implode('|', is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type])) - ); - } - if ($parameter->schema->default !== null) { - $param->setDefault($parameter->schema->default); + if ($parameter->default !== null) { + $param->setDefault($parameter->default); } - $constructor->addParam( - $param - )->addStmt( + $constructorParams[] = $param; + $constructor->addStmt( new Node\Expr\Assign( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), @@ -155,25 +133,37 @@ public static function generate(string $path, string $method, string $namespace, new Node\Expr\Variable($parameter->name), ) ); - if ($parameter->in === 'path' || $parameter->in === 'query') { + if ($parameter->location === 'path' || $parameter->location === 'query') { $requestReplaces['{' . $parameter->name . '}'] = new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), $parameter->name ); } - if ($parameter->in === 'query') { + if ($parameter->location === 'query') { $query[] = $parameter->name . '={' . $parameter->name . '}'; } } - $class->addStmt($constructor); $requestParameters = [ - new Node\Arg(new Node\Scalar\String_(strtoupper($method))), + new Node\Arg(new Node\Expr\ClassConstFetch( + new Node\Name('self'), + new Node\Name('METHOD'), + )), new Node\Arg(new Node\Expr\FuncCall( new Node\Name('\str_replace'), [ new Node\Expr\Array_(array_map(static fn (string $key): Node\Expr\ArrayItem => new Node\Expr\ArrayItem(new Node\Scalar\String_($key)), array_keys($requestReplaces))), new Node\Expr\Array_(array_values($requestReplaces)), - new Node\Scalar\String_(rtrim($path . '?' . implode('&', $query), '?')), + count($query) > 0 ? + new Node\Expr\BinaryOp\Concat( + new Node\Expr\ClassConstFetch( + new Node\Name('self'), + new Node\Name('PATH'), + ), + new Node\Scalar\String_(rtrim('?' . implode('&', $query), '?')), + ) : new Node\Expr\ClassConstFetch( + new Node\Name('self'), + new Node\Name('PATH'), + ), ] )), ]; @@ -182,33 +172,34 @@ public static function generate(string $path, string $method, string $namespace, $factory->param('data')->setType('array')->setDefault([]) ); - if ($operation->requestBody !== null) { - foreach ($operation->requestBody->content as $requestBodyContentType => $requestBodyContent) { - $requestParameters[] = new Node\Expr\Array_([ - new Node\Expr\ArrayItem(new Node\Scalar\String_($requestBodyContentType), new Node\Scalar\String_('Content-Type')) - ]); - $requestParameters[] = new Node\Expr\FuncCall(new Node\Name('json_encode'), [new Arg(new Node\Expr\Variable('data'))]); - $createRequestMethod->addStmt( - new Node\Stmt\Expression(new Node\Expr\MethodCall( - new Node\Expr\PropertyFetch( - new Node\Expr\Variable('this'), - 'requestSchemaValidator' - ), - new Node\Name('validate'), - [ - new Node\Arg(new Node\Expr\Variable('data')), - new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [ - new Node\Expr\ClassConstFetch( - new Node\Name('\\' . $rootNamespace . 'Schema\\' . $schemaRegistry->get($requestBodyContent->schema, $className . '\\Request\\' . (new Convert(str_replace('/', '\\', $requestBodyContentType)))->toPascal())), - new Node\Name('SCHEMA_JSON'), - ), - new Node\Scalar\String_('\cebe\openapi\spec\Schema'), - ])), - ] - )) - ); - break; - } + foreach ($operation->requestBody as $requestBody) { + $requestParameters[] = new Node\Expr\Array_([ + new Node\Expr\ArrayItem(new Node\Scalar\String_($requestBody->contentType), new Node\Scalar\String_('Content-Type')) + ]); + $requestParameters[] = new Node\Expr\FuncCall(new Node\Name('json_encode'), [new Arg(new Node\Expr\Variable('data'))]); + $createRequestMethod->addStmt( + new Node\Stmt\Expression(new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + ), + new Node\Name('validate'), + [ + new Node\Arg(new Node\Expr\Variable('data')), + new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\\' . \cebe\openapi\Reader::class), new Node\Name('readFromJson'), [ + new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Schema\\' . $requestBody->schema->className), + new Node\Name('SCHEMA_JSON'), + ), + new Node\Expr\ClassConstFetch( + new Node\Name('\\' . \cebe\openapi\spec\Schema::class), + new Node\Name('class'), + ), + ])), + ] + )) + ); + break; } $createRequestMethod->addStmt( @@ -222,20 +213,20 @@ public static function generate(string $path, string $method, string $namespace, ) ); - $class->addStmt( - $createRequestMethod - ); + $codes = array_unique(array_map(static fn (OperationResponse $response): int => $response->code, $operation->response)); $cases = []; $returnType = []; $returnTypeRaw = []; - foreach ($operation->responses as $code => $spec) { + foreach ($codes as $code) { $contentTypeCases = []; - foreach ($spec->content as $contentType => $contentTypeSchema) { - $fallbackName = 'Operation\\' . $className . '\\Response\\' . (new Convert(str_replace('/', '\\', $contentType) . '\\H' . $code))->toPascal(); - $srs = $schemaRegistry->get($contentTypeSchema->schema, $fallbackName); - $object = '\\' . $rootNamespace . 'Schema\\' . $srs; - $returnType[] = ($contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class . '<' : '') . $object . ($contentTypeSchema->schema->type === 'array' ? '>' : ''); - $returnTypeRaw[] = $contentTypeSchema->schema->type === 'array' ? '\\' . Observable::class : $object; + foreach ($operation->response as $contentTypeSchema) { + if ($contentTypeSchema->code !== $code) { + continue; + } + + $object = $namespace . 'Schema\\' . $contentTypeSchema->schema->className; + $returnType[] = ($contentTypeSchema->schema->isArray ? '\\' . Observable::class . '<' : '') . $object . ($contentTypeSchema->schema->isArray ? '>' : ''); + $returnTypeRaw[] = $contentTypeSchema->schema->isArray ? '\\' . Observable::class : $object; $hydrate = new Node\Expr\MethodCall( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), @@ -248,7 +239,7 @@ public static function generate(string $path, string $method, string $namespace, ], ); $ctc = new Node\Stmt\Case_( - new Node\Scalar\String_($contentType), + new Node\Scalar\String_($contentTypeSchema->contentType), [ new Node\Stmt\Expression(new Node\Expr\MethodCall( new Node\Expr\PropertyFetch( @@ -260,7 +251,7 @@ public static function generate(string $path, string $method, string $namespace, new Node\Arg(new Node\Expr\Variable('body')), new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [ new Node\Expr\ClassConstFetch( - new Node\Name('\\' . $rootNamespace . 'Schema\\' . $srs), + new Node\Name($namespace . 'Schema\\' . $contentTypeSchema->schema->className), new Node\Name('SCHEMA_JSON'), ), new Node\Scalar\String_('\cebe\openapi\spec\Schema'), @@ -268,7 +259,7 @@ public static function generate(string $path, string $method, string $namespace, ] )), new Node\Stmt\Return_( - $contentTypeSchema->schema->type === 'array' ? new Node\Expr\MethodCall( + $contentTypeSchema->schema->isArray ? new Node\Expr\MethodCall( new Node\Expr\StaticCall( new Node\Name('\\' . Observable::class), new Node\Name('fromArray'), @@ -307,28 +298,20 @@ public static function generate(string $path, string $method, string $namespace, count($contentTypeCases) > 0 ? new Node\Stmt\Switch_( new Node\Expr\Variable('contentType'), $contentTypeCases - ) : new Node\Stmt\Return_(new Node\Scalar\LNumber($code)), + ) : new Node\Stmt\Return_(new Node\Expr\Variable('response')), new Node\Stmt\Break_() ] ); if (count($contentTypeCases) === 0) { - $returnType[] = $returnTypeRaw[] = 'int'; + $returnType[] = $returnTypeRaw[] = '\\' . ResponseInterface::class; } + $case->setDocComment(new Doc('/**' . $contentTypeSchema->description . '**/')); $cases[] = $case; - $case->setDocComment(new Doc('/**' . $spec->description . '**/')); } - $class->addStmt( - $factory->method('createResponse')->setDocComment( - new Doc(implode(PHP_EOL, [ - '/**', - ' * @return ' . implode('|', array_unique($returnType)), - ' */', - ])) - )->addParam( - $factory->param('response')->setType('\\' . ResponseInterface::class) - )->setReturnType( - new Node\UnionType(array_map(static fn (string $object): Node\Name => new Node\Name($object), array_unique($returnTypeRaw))) - )->addStmt( + $createResponseMethod = $factory->method('createResponse'); + + if (count($cases) > 0) { + $createResponseMethod->addStmt( new Node\Expr\Assign(new Node\Expr\Variable('contentType'), new Node\Expr\MethodCall(new Node\Expr\Variable('response'), 'getHeaderLine', [new Arg(new Node\Scalar\String_('Content-Type'))])) )->addStmt( new Node\Expr\Assign(new Node\Expr\Variable('body'), new Node\Expr\FuncCall(new Node\Name('json_decode'), [new Node\Expr\MethodCall(new Node\Expr\MethodCall(new Node\Expr\Variable('response'), 'getBody'), 'getContents'), new Node\Expr\ConstFetch(new Node\Name('true'))])) @@ -342,13 +325,64 @@ public static function generate(string $path, string $method, string $namespace, new Node\Expr\New_( new Node\Name('\\' . \RuntimeException::class), [ - new Arg(new Node\Scalar\String_('Unable to find matching reponse code and content type')) + new Arg(new Node\Scalar\String_('Unable to find matching response code and content type')) ] ) ) - ) + ); + } else { + $createResponseMethod->addStmt(new Node\Stmt\Return_(new Node\Expr\Variable('response'))); + $returnType[] = $returnTypeRaw[] = '\\' . ResponseInterface::class; + $noHydrator = true; + } + $createResponseMethod->setReturnType( + new Node\UnionType(array_map(static fn (string $object): Node\Name => new Node\Name($object), array_unique($returnTypeRaw))) + )->setDocComment( + new Doc(implode(PHP_EOL, [ + '/**', + ' * @return ' . implode('|', array_unique($returnType)), + ' */', + ])) + )->addParam( + $factory->param('response')->setType('\\' . ResponseInterface::class) ); - yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode()); + if ($noHydrator === false) { + $class->addStmt( + $factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate() + )->addStmt( + $factory->property('hydrator')->setType($namespace . 'Hydrator\\' . $hydrator->className)->makeReadonly()->makePrivate() + ); + + $constructor->addParam( + (new Param('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator') + )->addStmt( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'responseSchemaValidator' + ), + new Node\Expr\Variable('responseSchemaValidator'), + ) + )->addParam( + (new Param('hydrator'))->setType($namespace . 'Hydrator\\' . $hydrator->className) + )->addStmt( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), + new Node\Expr\Variable('hydrator'), + ) + ); + } + + $constructor->addParams($constructorParams); + + $class->addStmt($constructor); + $class->addStmt($createRequestMethod); + $class->addStmt($createResponseMethod); + + yield new File($namespace . 'Operation\\' . $operation->className, $stmt->addStmt($class)->getNode()); } } diff --git a/src/Generator/Path.php b/src/Generator/Path.php deleted file mode 100644 index effec22..0000000 --- a/src/Generator/Path.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @throws \Jawira\CaseConverter\CaseConverterException - */ - public static function generate(string $path, string $namespace, string $baseNamespace, string $className, PathItem $pathItem): iterable - { - $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); - - $class = $factory->class($className)->makeFinal(); - - foreach ($pathItem->getOperations() as $method => $operation) { - $operationClassName = str_replace('/', '\\', '\\' . $baseNamespace . 'Operation/' . (new Convert($operation->operationId))->fromTrain()->toPascal()); - $method = - $factory-> - method($method)-> - setReturnType($operationClassName) - ; - $operationConstructorArguments = []; - foreach ($operation->parameters as $parameter) { - $param = new Param($parameter->name); - if ($parameter->schema->default !== null) { - $param->setType( - str_replace([ - 'integer', - 'any', - 'boolean', - ], [ - 'int', - '', - 'bool', - ], implode('|', is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type])) - ); - } - if ($parameter->schema->default !== null) { - $param->setDefault($parameter->schema->default); - } - $method->addParam($param); - $operationConstructorArguments[] = new Node\Expr\Variable($parameter->name); - } - $method->addStmt(new Node\Stmt\Return_(new Node\Expr\New_( - new Node\Name($operationClassName), - $operationConstructorArguments - ))); - $class->addStmt($method); - } - - yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode()); - } -} diff --git a/src/Generator/Schema.php b/src/Generator/Schema.php index 063aed8..e67590c 100644 --- a/src/Generator/Schema.php +++ b/src/Generator/Schema.php @@ -3,7 +3,8 @@ namespace ApiClients\Tools\OpenApiClientGenerator\Generator; use ApiClients\Tools\OpenApiClientGenerator\File; -use ApiClients\Tools\OpenApiClientGenerator\SchemaRegistry; +use ApiClients\Tools\OpenApiClientGenerator\Utils; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; use cebe\openapi\spec\Schema as OpenAPiSchema; use Jawira\CaseConverter\Convert; use PhpParser\Builder\Param; @@ -18,103 +19,36 @@ final class Schema /** * @param string $name * @param string $namespace - * @param string $className + * @param string $schema->className * @param OpenAPiSchema $schema * @return iterable */ - public static function generate(string $name, string $namespace, string $className, OpenAPiSchema $schema, SchemaRegistry $schemaRegistry, string $rootNamespace): iterable + public static function generate(string $namespace, \ApiClients\Tools\OpenApiClientGenerator\Representation\Schema $schema, SchemaRegistry $schemaRegistry): iterable { $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); + $stmt = $factory->namespace(trim(Utils::dirname($namespace . '\\Schema\\' . $schema->className), '\\')); $schemaJson = new Node\Stmt\ClassConst( [ new Node\Const_( 'SCHEMA_JSON', new Node\Scalar\String_( - json_encode($schema->getSerializableData()) + json_encode($schema->schema->getSerializableData()) ) ), ], Class_::MODIFIER_PUBLIC ); - if ($schema->type === 'array') { - $schema = $schema->items; - } - - $class = $factory->class($className)->makeFinal()->addStmt( + $class = $factory->class(trim(Utils::basename($schema->className), '\\'))->makeFinal()->makeReadonly()->addStmt( $schemaJson - )->addStmt( - new Node\Stmt\ClassConst( - [ - new Node\Const_( - 'SCHEMA_EXAMPLE', - new Node\Scalar\String_( - json_encode((function (array $schema): array { - $iterate = function (array $schema) use (&$iterate): array { - $examples = []; - - if (!array_key_exists('properties', $schema)) { - return $examples; - } - foreach ($schema['properties'] as $propertyName => $property) { - if ( - array_key_exists('type', $property) && - $property['type'] === 'object' && - array_key_exists('properties', $property) && - $property['properties'] !== null - ) { - $examples[$propertyName] = $iterate($property); - if (count($examples[$propertyName]) === 0) { - unset($examples[$propertyName]); - } - continue; - } - - if ( - array_key_exists('type', $property) && - $property['type'] === 'array' && - array_key_exists('items', $property) && - $property['items'] !== null && - array_key_exists('type', $property['items']) && - $property['items']['type'] === 'object' - ) { - $items = $iterate($property['items']); - - if (count($items) > 0) { - $examples[$propertyName] = [$items]; - } - continue; - } - - if (array_key_exists('examples', $property)) { - $examples[$propertyName] = $property['examples'][count($property['examples']) === 1 ? 0 : mt_rand(0, count($property['examples']) - 1)]; - } else if ( - array_key_exists('example', $property) && - $property['example'] !== null - ) { - $examples[$propertyName] = $property['example']; - } - } - - return $examples; - }; - - return $iterate($schema); - })(json_decode(json_encode($schema->getSerializableData()), true))) - ) - ), - ], - Class_::MODIFIER_PUBLIC - ) )->addStmt( new Node\Stmt\ClassConst( [ new Node\Const_( 'SCHEMA_TITLE', new Node\Scalar\String_( - $schema->title ?? $name + $schema->title ) ), ], @@ -126,7 +60,7 @@ public static function generate(string $name, string $namespace, string $classNa new Node\Const_( 'SCHEMA_DESCRIPTION', new Node\Scalar\String_( - $schema->description ?? '' + $schema->description ) ), ], @@ -134,156 +68,42 @@ public static function generate(string $name, string $namespace, string $classNa ) ); - if ($schema->oneOf !== null && count($schema->oneOf) > 0 && $schema->oneOf[0] instanceof OpenAPiSchema) { - yield from self::fillUpSchema($name, $namespace, $className, $class, $schema->oneOf[0], $factory, $schemaRegistry, $rootNamespace); - } else { - yield from self::fillUpSchema($name, $namespace, $className, $class, $schema, $factory, $schemaRegistry, $rootNamespace); - } - - yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode()); - } - - private static function fillUpSchema(string $name, string $namespace, string $className, \PhpParser\Builder\Class_ $class, OpenAPiSchema $schema, $factory, SchemaRegistry $schemaRegistry, string $rootNamespace): iterable - { - yield from []; $constructor = (new BuilderFactory())->method('__construct')->makePublic(); $constructDocBlock = []; - foreach ($schema->properties as $propertyName => $property) { - $propertyName = str_replace([ - '@', - '+', - '-', - '$', - ], [ - '_AT_', - '_PLUSES_', - '_MINUS_', - '', - ], $propertyName); - $propertyStmt = $factory->property($propertyName)->makePublic()->makeReadonly(); + foreach ($schema->properties as $property) { + $propertyStmt = $factory->property($property->name)->makePublic(); $propertyDocBlock = []; if (is_string($property->description) && strlen($property->description) > 0) { $propertyDocBlock[] = $property->description; } - $propertyType = $property->type; - $setDefaylt = true; + $nullable = ''; if ($property->nullable) { $nullable = '?'; -// $propertyStmt->setDefault(null); } - if ( - is_array($propertyType) && - count($propertyType) === 2 && - ( - in_array(null, $propertyType) || - in_array("null", $propertyType) - ) - ) { - foreach ($propertyType as $pt) { - if ($pt !== null && $pt !== "null") { - $propertyType = $pt; - break; - } + $types = []; + foreach ($property->type as $type) { + if ($type->payload instanceof \ApiClients\Tools\OpenApiClientGenerator\Representation\Schema) { + $types[] = $namespace . 'Schema\\' . $type->payload->className; + continue; } - $nullable = '?'; - } - - if (is_string($propertyType)) { - if (is_array($schema->required) && !in_array($propertyName, $schema->required, false)) { - $nullable = '?'; -// $propertyStmt->setDefault(null); - } - - if ($propertyType === 'array'/* && $property->items instanceof OpenAPiSchema*/) { -// if (array_key_exists(spl_object_hash($property->items), $schemaClassNameMap)) { - $propertyDocBlock[] = '@var array<\\' . $rootNamespace . '\\' . $schemaRegistry->get($property->items, $className . '\\' . (new Convert($propertyName))->toPascal()) . '>'; -// $constructDocBlock[] = '@param array<\\' . $rootNamespace . '\\' . $schemaRegistry->get($property->items, $className . '\\' . (new Convert($propertyName))->toPascal()) . '>'; - $constructDocBlock[] = '@param array<\\' . $rootNamespace . '\\' . $schemaRegistry->get($property->items, $className . '\\' . (new Convert($propertyName))->toPascal()) . '> $' . $propertyName; -// } elseif ($property->items->type === 'object') { -// $propertyDocBlock[] = '@var array<\\' . $namespace . '\\' . $className . '\\' . (new Convert($propertyName))->toPascal() . '>'; -// } - } - - if (is_string($propertyType)) { - $propertyType = str_replace([ - 'integer', - 'number', - 'any', - 'null', - 'boolean', - ], [ - 'int', - 'int', - '', - '', - 'bool', - ], $propertyType); - - if ($propertyType === '') { - $propertyType = 'mixed'; - } - } - } else { - $propertyType = 'mixed'; + $types[] = $type->payload; } - - $propertyStmt->setType(($propertyType === 'array' ? '' : $nullable) . $propertyType); - $constructorParam = (new Param($propertyName))->setType($propertyType); + $propertyStmt->setType(($property->type === 'array' ? '' : $nullable) . implode('|', $types)); + $constructorParam = (new Param($property->name))->setType(implode('|', $types)); $constructor->addStmt( new Node\Expr\Assign( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), - $propertyName + $property->name ), - new Node\Expr\Variable($propertyName), + new Node\Expr\Variable($property->name), ) ); - // 74908 - - if (is_array($property->anyOf) && $property->anyOf[0] instanceof OpenAPiSchema/* && array_key_exists(spl_object_hash($property->anyOf[0]), $schemaClassNameMap)*/) { - $fqcnn = '\\' . $rootNamespace . '\\' . $schemaRegistry->get($property->anyOf[0], $className . '\\' . (new Convert($propertyName))->toPascal()); - $propertyStmt->setType($nullable . $fqcnn); - $constructorParam->setType($nullable . $fqcnn); - $setDefaylt = false; - } else if (is_array($property->allOf) && $property->allOf[0] instanceof OpenAPiSchema/* && array_key_exists(spl_object_hash($property->allOf[0]), $schemaClassNameMap)*/) { - $fqcnn = '\\' . $rootNamespace . '\\' . $schemaRegistry->get($property->allOf[0], $className . '\\' . (new Convert($propertyName))->toPascal()); - $propertyStmt->setType($nullable . $fqcnn); - $constructorParam->setType($nullable . $fqcnn); - $setDefaylt = false; - } - -// if (($property->type === 'object' || (is_array($property->type) && count($property->type) === 2)) && $property instanceof OpenAPiSchema/* && array_key_exists(spl_object_hash($property), $schemaClassNameMap)*/) { - if ($propertyType === 'object') { - $fqcnn = '\\' . $rootNamespace . '\\' . $schemaRegistry->get($property, $className . '\\' . (new Convert($propertyName))->toPascal()); - $propertyStmt->setType($nullable . $fqcnn); - $constructorParam->setType($nullable . $fqcnn); - $setDefaylt = false; - } - - if (is_string($propertyType)) { - $t = str_replace([ - 'object', - 'integer', - 'any', - 'boolean', - ], [ - 'array', - 'int', - '', - 'bool', - ], $propertyType); - if ($t !== '') { - if ($t === 'array' && $setDefaylt === true) { -// $propertyStmt->setDefault([]); - } - } - } - if (count($propertyDocBlock) > 0) { $propertyStmt->setDocComment('/**' . PHP_EOL . ' * ' . implode(PHP_EOL . ' * ', str_replace(['/**', '*/'], '', $propertyDocBlock)) . PHP_EOL .' */'); } @@ -297,5 +117,8 @@ private static function fillUpSchema(string $name, string $namespace, string $cl } $class->addStmt($constructor); + + + yield new File($namespace . 'Schema\\' . $schema->className, $stmt->addStmt($class)->getNode()); } } diff --git a/src/Generator/WebHook.php b/src/Generator/WebHook.php index b6fafc0..43e207a 100644 --- a/src/Generator/WebHook.php +++ b/src/Generator/WebHook.php @@ -4,147 +4,139 @@ use ApiClients\Contracts\OpenAPI\WebHookInterface; use ApiClients\Tools\OpenApiClientGenerator\File; -use ApiClients\Tools\OpenApiClientGenerator\SchemaRegistry; -use cebe\openapi\spec\Operation as OpenAPiOperation; -use cebe\openapi\spec\PathItem; -use Jawira\CaseConverter\Convert; -use League\OpenAPIValidation\Schema\Exception\SchemaMismatch; +use ApiClients\Tools\OpenApiClientGenerator\Utils; +use ApiClients\Tools\OpenApiClientGenerator\Registry\Schema as SchemaRegistry; use PhpParser\Builder\Param; use PhpParser\BuilderFactory; +use PhpParser\Comment\Doc; use PhpParser\Node; -use PhpParser\Node\Stmt\Class_; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ServerRequestInterface; -use RingCentral\Psr7\Request; +use PhpParser\Node\Arg; final class WebHook { - /** - * @param string $path - * @param string $namespace - * @param string $baseNamespace - * @param string $className - * @param PathItem $pathItem - * @return iterable - * @throws \Jawira\CaseConverter\CaseConverterException - */ - public static function generate(string $path, string $namespace, string $baseNamespace, string $className, PathItem $pathItem, SchemaRegistry $schemaRegistry, string $rootNamespace): iterable + public static function generate(string $namespace, string $event, SchemaRegistry $schemaRegistry, \ApiClients\Tools\OpenApiClientGenerator\Representation\WebHook ...$webHooks): iterable { + $className = Utils::className($event); + $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); + $stmt = $factory->namespace(ltrim($namespace . 'WebHook', '\\')); + + $class = $factory->class($className)->makeFinal()->implement('\\' . WebHookInterface::class)->setDocComment(new Doc(implode(PHP_EOL, [ + '/**', + ' * @internal', + ' */', + ]))); + $class->addStmt($factory->property('requestSchemaValidator')->setType('\\' . \League\OpenAPIValidation\Schema\SchemaValidator::class)->makeReadonly()->makePrivate()); + $class->addStmt($factory->property('hydrator')->setType($namespace . 'Hydrator\\WebHook\\' . $className)->makeReadonly()->makePrivate()); - $class = $factory->class($className)->makeFinal()->implement('\\' . WebHookInterface::class); - $method = $factory->method('resolve')->makePublic()->setReturnType('string')->addParam( + $constructor = $factory->method('__construct')->makePublic()->addParam( + (new Param('requestSchemaValidator'))->setType('\\' . \League\OpenAPIValidation\Schema\SchemaValidator::class) + )->addParam( + (new Param('hydrator'))->setType($namespace . 'Hydrator\\WebHook\\' . $className) + )->addStmt( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + ), + new Node\Expr\Variable('requestSchemaValidator'), + ) + ); + $class->addStmt($constructor); + + $resolveReturnTypes = []; + $method = $factory->method('resolve')->makePublic()->setReturnType('object')->addParam( + (new Param('headers'))->setType('array') + )->addParam( (new Param('data'))->setType('array') ); - if ($pathItem->post->requestBody->content !== null) { - $content = current($pathItem->post->requestBody->content); - $tmts = []; - if ($content->schema->oneOf !== null && count($content->schema->oneOf) > 0) { - $tmts[] = new Node\Expr\Assign(new Node\Expr\Variable('schemaValidator'), new Node\Expr\New_( - new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'), + $gotoLabels = 'actions_aaaaa'; + $tmts = []; + $tmts[] = new Node\Expr\Assign( + new Node\Expr\Variable('error'), + new Node\Expr\New_( + new Node\Name('\\' . \RuntimeException::class), + [ + new Arg(new Node\Scalar\String_('No action matching given headers and data')), + ] + ) + ); + + foreach ($webHooks as $webHook) { + $headers = []; + foreach ($webHook->headers as $header) { + $headers[] = new Node\Stmt\Expression(new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + ), + new Node\Name('validate'), [ - new Node\Arg(new Node\Expr\ClassConstFetch( - new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'), - new Node\Name('VALIDATE_AS_REQUEST'), - )) + new Node\Arg(new Node\Expr\ArrayDimFetch( + new Node\Expr\Variable('headers'), + new Node\Scalar\String_(strtolower($header->name)), + )), + new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [ + new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Schema\\' . $header->schema->className), + new Node\Name('SCHEMA_JSON'), + ), + new Node\Scalar\String_('\cebe\openapi\spec\Schema'), + ])), ] )); - $gotoLabels = 'a'; - - if ($content->schema->discriminator !== null && $content->schema->discriminator->propertyName !== null && strlen($content->schema->discriminator->propertyName) > 0) { - foreach ($content->schema->oneOf as $oneOfSchema) { - foreach ($oneOfSchema->properties as $name => $property) { - if ($content->schema->discriminator->propertyName === $name) { - $tmts[] = new Node\Stmt\Label($gotoLabels); - $gotoLabels++; - //'{"title":"release created event","required":["action","release","repository","sender"],"type":"object","properties":{"action":{"enum":["created"],"type":"string"},"release":{"required":["url","assets_url","upload_url","html_url","id","node_id","tag_name","target_commitish","name","draft","author","prerelease","created_at","published_at","assets","tarball_url","zipball_url","body"],"type":"object","properties":{"url":{"type":"string","format":"uri"},"assets_url":{"type":"string","format":"uri"},"upload_url":{"type":"string","format":"uri-template"},"html_url":{"type":"string","format":"uri"},"id":{"type":"integer"},"node_id":{"type":"string"},"tag_name":{"type":"string","description":"The name of the tag."},"target_commitish":{"type":"string","description":"Specifies the commitish value that determines where the Git tag is created from."},"name":{"type":"null"},"draft":{"type":"boolean","description":"true to create a draft (unpublished) release, false to create a published one."},"author":{"title":"User","required":["login","id","node_id","avatar_url","gravatar_id","url","html_url","followers_url","following_url","gists_url","starred_url","subscriptions_url","organizations_url","repos_url","events_url","received_events_url","type","site_admin"],"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"node_id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"avatar_url":{"type":"string","format":"uri"},"gravatar_id":{"type":"string"},"url":{"type":"string","format":"uri"},"html_url":{"type":"string","format":"uri"},"followers_url":{"type":"string","format":"uri"},"following_url":{"type":"string","format":"uri-template"},"gists_url":{"type":"string","format":"uri-template"},"starred_url":{"type":"string","format":"uri-template"},"subscriptions_url":{"type":"string","format":"uri"},"organizations_url":{"type":"string","format":"uri"},"repos_url":{"type":"string","format":"uri"},"events_url":{"type":"string","format":"uri-template"},"received_events_url":{"type":"string","format":"uri"},"type":{"enum":["Bot","User","Organization"],"type":"string"},"site_admin":{"type":"boolean"}},"additionalProperties":false},"prerelease":{"type":"boolean","description":"Whether the release is identified as a prerelease or a full release."},"created_at":{"type":["string","null"],"format":"date-time"},"published_at":{"type":["string","null"],"format":"date-time"},"assets":{"type":"array","items":{"title":"Release Asset","required":["url","browser_download_url","id","node_id","name","label","state","content_type","size","download_count","created_at","updated_at"],"type":"object","properties":{"url":{"type":"string","format":"uri"},"browser_download_url":{"type":"string","format":"uri"},"id":{"type":"integer"},"node_id":{"type":"string"},"name":{"type":"string","description":"The file name of the asset."},"label":{"type":"string"},"state":{"enum":["uploaded"],"type":"string","description":"State of the release asset."},"content_type":{"type":"string"},"size":{"type":"integer"},"download_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"uploader":{"title":"User","required":["login","id","node_id","avatar_url","gravatar_id","url","html_url","followers_url","following_url","gists_url","starred_url","subscriptions_url","organizations_url","repos_url","events_url","received_events_url","type","site_admin"],"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"node_id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"avatar_url":{"type":"string","format":"uri"},"gravatar_id":{"type":"string"},"url":{"type":"string","format":"uri"},"html_url":{"type":"string","format":"uri"},"followers_url":{"type":"string","format":"uri"},"following_url":{"type":"string","format":"uri-template"},"gists_url":{"type":"string","format":"uri-template"},"starred_url":{"type":"string","format":"uri-template"},"subscriptions_url":{"type":"string","format":"uri"},"organizations_url":{"type":"string","format":"uri"},"repos_url":{"type":"string","format":"uri"},"events_url":{"type":"string","format":"uri-template"},"received_events_url":{"type":"string","format":"uri"},"type":{"enum":["Bot","User","Organization"],"type":"string"},"site_admin":{"type":"boolean"}},"additionalProperties":false}},"description":"Data related to a release.","additionalProperties":false}},"tarball_url":{"type":["string","null"],"format":"uri"},"zipball_url":{"type":["string","null"],"format":"uri"},"body":{"type":["string","null"]}},"description":"The [release](https:\\/\\/docs.github.com\\/en\\/rest\\/reference\\/repos\\/#get-a-release) object.","additionalProperties":false},"repository":{"title":"Repository","required":["id","node_id","name","full_name","private","owner","html_url","description","fork","url","forks_url","keys_url","collaborators_url","teams_url","hooks_url","issue_events_url","events_url","assignees_url","branches_url","tags_url","blobs_url","git_tags_url","git_refs_url","trees_url","statuses_url","languages_url","stargazers_url","contributors_url","subscribers_url","subscription_url","commits_url","git_commits_url","comments_url","issue_comment_url","contents_url","compare_url","merges_url","archive_url","downloads_url","issues_url","pulls_url","milestones_url","notifications_url","labels_url","releases_url","deployments_url","created_at","updated_at","pushed_at","git_url","ssh_url","clone_url","svn_url","homepage","size","stargazers_count","watchers_count","language","has_issues","has_projects","has_downloads","has_wiki","has_pages","forks_count","mirror_url","archived","open_issues_count","license","forks","open_issues","watchers","default_branch"],"type":"object","properties":{"id":{"type":"integer","description":"Unique identifier of the repository"},"node_id":{"type":"string"},"name":{"type":"string","description":"The name of the repository."},"full_name":{"type":"string"},"private":{"type":"boolean","description":"Whether the repository is private or public."},"owner":{"title":"User","required":["login","id","node_id","avatar_url","gravatar_id","url","html_url","followers_url","following_url","gists_url","starred_url","subscriptions_url","organizations_url","repos_url","events_url","received_events_url","type","site_admin"],"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"node_id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"avatar_url":{"type":"string","format":"uri"},"gravatar_id":{"type":"string"},"url":{"type":"string","format":"uri"},"html_url":{"type":"string","format":"uri"},"followers_url":{"type":"string","format":"uri"},"following_url":{"type":"string","format":"uri-template"},"gists_url":{"type":"string","format":"uri-template"},"starred_url":{"type":"string","format":"uri-template"},"subscriptions_url":{"type":"string","format":"uri"},"organizations_url":{"type":"string","format":"uri"},"repos_url":{"type":"string","format":"uri"},"events_url":{"type":"string","format":"uri-template"},"received_events_url":{"type":"string","format":"uri"},"type":{"enum":["Bot","User","Organization"],"type":"string"},"site_admin":{"type":"boolean"}},"additionalProperties":false},"html_url":{"type":"string","format":"uri"},"description":{"type":["string","null"]},"fork":{"type":"boolean"},"url":{"type":"string","format":"uri"},"forks_url":{"type":"string","format":"uri"},"keys_url":{"type":"string","format":"uri-template"},"collaborators_url":{"type":"string","format":"uri-template"},"teams_url":{"type":"string","format":"uri"},"hooks_url":{"type":"string","format":"uri"},"issue_events_url":{"type":"string","format":"uri-template"},"events_url":{"type":"string","format":"uri"},"assignees_url":{"type":"string","format":"uri-template"},"branches_url":{"type":"string","format":"uri-template"},"tags_url":{"type":"string","format":"uri"},"blobs_url":{"type":"string","format":"uri-template"},"git_tags_url":{"type":"string","format":"uri-template"},"git_refs_url":{"type":"string","format":"uri-template"},"trees_url":{"type":"string","format":"uri-template"},"statuses_url":{"type":"string","format":"uri-template"},"languages_url":{"type":"string","format":"uri"},"stargazers_url":{"type":"string","format":"uri"},"contributors_url":{"type":"string","format":"uri"},"subscribers_url":{"type":"string","format":"uri"},"subscription_url":{"type":"string","format":"uri"},"commits_url":{"type":"string","format":"uri-template"},"git_commits_url":{"type":"string","format":"uri-template"},"comments_url":{"type":"string","format":"uri-template"},"issue_comment_url":{"type":"string","format":"uri-template"},"contents_url":{"type":"string","format":"uri-template"},"compare_url":{"type":"string","format":"uri-template"},"merges_url":{"type":"string","format":"uri"},"archive_url":{"type":"string","format":"uri-template"},"downloads_url":{"type":"string","format":"uri"},"issues_url":{"type":"string","format":"uri-template"},"pulls_url":{"type":"string","format":"uri-template"},"milestones_url":{"type":"string","format":"uri-template"},"notifications_url":{"type":"string","format":"uri-template"},"labels_url":{"type":"string","format":"uri-template"},"releases_url":{"type":"string","format":"uri-template"},"deployments_url":{"type":"string","format":"uri"},"created_at":{"oneOf":[{"type":"integer"},{"type":"string","format":"date-time"}]},"updated_at":{"type":"string","format":"date-time"},"pushed_at":{"oneOf":[{"type":"integer"},{"type":"string","format":"date-time"},{"type":"null"}]},"git_url":{"type":"string","format":"uri"},"ssh_url":{"type":"string"},"clone_url":{"type":"string","format":"uri"},"svn_url":{"type":"string","format":"uri"},"homepage":{"type":["string","null"]},"size":{"type":"integer"},"stargazers_count":{"type":"integer"},"watchers_count":{"type":"integer"},"language":{"type":["string","null"]},"has_issues":{"type":"boolean","description":"Whether issues are enabled.","default":true},"has_projects":{"type":"boolean","description":"Whether projects are enabled.","default":true},"has_downloads":{"type":"boolean","description":"Whether downloads are enabled.","default":true},"has_wiki":{"type":"boolean","description":"Whether the wiki is enabled.","default":true},"has_pages":{"type":"boolean"},"forks_count":{"type":"integer"},"mirror_url":{"type":["string","null"],"format":"uri"},"archived":{"type":"boolean","description":"Whether the repository is archived.","default":false},"disabled":{"type":"boolean","description":"Returns whether or not this repository is disabled."},"open_issues_count":{"type":"integer"},"license":{"oneOf":[{"title":"License","required":["key","name","spdx_id","url","node_id"],"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"spdx_id":{"type":"string"},"url":{"type":["string","null"],"format":"uri"},"node_id":{"type":"string"}},"additionalProperties":false},{"type":"null"}]},"forks":{"type":"integer"},"open_issues":{"type":"integer"},"watchers":{"type":"integer"},"stargazers":{"type":"integer"},"default_branch":{"type":"string","description":"The default branch of the repository."},"allow_squash_merge":{"type":"boolean","description":"Whether to allow squash merges for pull requests.","default":true},"allow_merge_commit":{"type":"boolean","description":"Whether to allow merge commits for pull requests.","default":true},"allow_rebase_merge":{"type":"boolean","description":"Whether to allow rebase merges for pull requests.","default":true},"delete_branch_on_merge":{"type":"boolean","description":"Whether to delete head branches when pull requests are merged","default":false},"master_branch":{"type":"string"},"permissions":{"required":["pull","push","admin"],"type":"object","properties":{"pull":{"type":"boolean"},"push":{"type":"boolean"},"admin":{"type":"boolean"},"maintain":{"type":"boolean"},"triage":{"type":"boolean"}},"additionalProperties":false},"public":{"type":"boolean"},"organization":{"type":"string"}},"description":"A git repository","additionalProperties":false},"sender":{"title":"User","required":["login","id","node_id","avatar_url","gravatar_id","url","html_url","followers_url","following_url","gists_url","starred_url","subscriptions_url","organizations_url","repos_url","events_url","received_events_url","type","site_admin"],"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"node_id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"]},"avatar_url":{"type":"string","format":"uri"},"gravatar_id":{"type":"string"},"url":{"type":"string","format":"uri"},"html_url":{"type":"string","format":"uri"},"followers_url":{"type":"string","format":"uri"},"following_url":{"type":"string","format":"uri-template"},"gists_url":{"type":"string","format":"uri-template"},"starred_url":{"type":"string","format":"uri-template"},"subscriptions_url":{"type":"string","format":"uri"},"organizations_url":{"type":"string","format":"uri"},"repos_url":{"type":"string","format":"uri"},"events_url":{"type":"string","format":"uri-template"},"received_events_url":{"type":"string","format":"uri"},"type":{"enum":["Bot","User","Organization"],"type":"string"},"site_admin":{"type":"boolean"}},"additionalProperties":false},"installation":{"title":"InstallationLite","required":["id","node_id"],"type":"object","properties":{"id":{"type":"integer","description":"The ID of the installation."},"node_id":{"type":"string"}},"description":"Installation","additionalProperties":false},"organization":{"title":"Organization","required":["login","id","node_id","url","repos_url","events_url","hooks_url","issues_url","members_url","public_members_url","avatar_url","description"],"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"node_id":{"type":"string"},"url":{"type":"string","format":"uri"},"html_url":{"type":"string","format":"uri"},"repos_url":{"type":"string","format":"uri"},"events_url":{"type":"string","format":"uri"},"hooks_url":{"type":"string","format":"uri"},"issues_url":{"type":"string","format":"uri"},"members_url":{"type":"string","format":"uri-template"},"public_members_url":{"type":"string","format":"uri-template"},"avatar_url":{"type":"string","format":"uri"},"description":{"type":["string","null"]}},"additionalProperties":false}},"additionalProperties":false}' - $fabicatedSchema = new \cebe\openapi\spec\Schema([ - 'title' => $oneOfSchema->title, - 'required' => [$content->schema->discriminator->propertyName], - 'properties' => [ - $name => $property->getSerializableData(), - ], - 'additionalProperties' => true, - ]); - $tmts[] = new Node\Stmt\TryCatch([ - new Node\Stmt\Expression(new Node\Expr\MethodCall( - new Node\Expr\Variable('schemaValidator'), - new Node\Name('validate'), - [ - new Node\Arg(new Node\Expr\Variable('data')), - new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [ - new Node\Expr\ClassConstFetch( - new Node\Name('\\' . $rootNamespace . 'Schema\\' . $schemaRegistry->get($fabicatedSchema, $className . '\\' . $gotoLabels)), - new Node\Name('SCHEMA_JSON'), - ), - new Node\Scalar\String_('\cebe\openapi\spec\Schema'), - ])), - ] - )), - new Node\Stmt\Return_(new Node\Scalar\String_($rootNamespace . 'Schema\\' . $schemaRegistry->get($oneOfSchema, $className . '\\' . $gotoLabels))), - ], [ - new Node\Stmt\Catch_( - [new Node\Name('\\' . \Throwable::class)], - new Node\Expr\Variable($gotoLabels), - [ - new Node\Stmt\Goto_($gotoLabels), - ] + } + foreach ($webHook->schema as $contentTYpe => $schema) { + $resolveReturnTypes[] = $namespace . 'Schema\\' . $schema->className; + $tmts[] = new Node\Stmt\If_( + new Node\Expr\BinaryOp\Equal( + new Node\Expr\ArrayDimFetch(new Node\Expr\Variable(new Node\Name('headers')), new Node\Scalar\String_('content-type')), + new Node\Scalar\String_($contentTYpe), + ), + [ + 'stmts' => [ + new Node\Stmt\TryCatch([ + ...$headers, + new Node\Stmt\Return_(new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' ), - ]); - } - } - } - - $tmts[] = new Node\Stmt\Label($gotoLabels); - $tmts[] = new Node\Stmt\Throw_(new Node\Expr\Variable($gotoLabels)); - } else { - foreach ($content->schema->oneOf as $oneOfSchema) { -// if (!array_key_exists(spl_object_hash($oneOfSchema), $schemaClassNameMap)) { -// continue; // TODO: Remove to make sure we have all schemas mapped -// } - $tmts[] = new Node\Stmt\Label($gotoLabels); - $gotoLabels++; - $tmts[] = new Node\Stmt\TryCatch([ - new Node\Stmt\Expression(new Node\Expr\MethodCall( - new Node\Expr\Variable('schemaValidator'), - new Node\Name('validate'), - [ - new Node\Arg(new Node\Expr\Variable('data')), - new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [ - new Node\Expr\ClassConstFetch( - new Node\Name('\\' . $rootNamespace . 'Schema\\' . $schemaRegistry->get($oneOfSchema, $className . '\\' . $gotoLabels)), - new Node\Name('SCHEMA_JSON'), - ), - new Node\Scalar\String_('\cebe\openapi\spec\Schema'), - ])), - ] - )), - new Node\Stmt\Return_(new Node\Scalar\String_($rootNamespace . 'Schema\\' . $schemaRegistry->get($oneOfSchema, $className . '\\' . $gotoLabels))), - ], [ - new Node\Stmt\Catch_( - [new Node\Name('\\' . \Throwable::class)], - new Node\Expr\Variable($gotoLabels), - [ - new Node\Stmt\Goto_($gotoLabels), - ] - ), - ]); - } - $tmts[] = new Node\Stmt\Label($gotoLabels); - $tmts[] = new Node\Stmt\Throw_(new Node\Expr\Variable($gotoLabels)); - } + new Node\Name('hydrateObject'), + [ + new Node\Arg(new Node\Expr\ClassConstFetch( + new Node\Name($namespace . 'Schema\\' . $schema->className), + new Node\Name('class'), + )), + new Node\Arg(new Node\Expr\Variable('data')), + ] + )), + ], [ + new Node\Stmt\Catch_( + [new Node\Name('\\' . \Throwable::class)], + new Node\Expr\Variable('error'), + [ + new Node\Stmt\Goto_($gotoLabels), + ] + ), + ]), + ], + ] + ); } + $tmts[] = new Node\Stmt\Label($gotoLabels); + $gotoLabels++; + } - if (count($tmts) === 0) { - $tmts[] = new Node\Stmt\Return_(new Node\Scalar\String_($rootNamespace . 'Schema\\' . $schemaRegistry->get($content->schema, $className . '\\Default'))); - } + $tmts[] = new Node\Stmt\Throw_(new Node\Expr\Variable('error')); - $method->addStmts($tmts); + if (count($resolveReturnTypes) > 0) { + $method->setReturnType(implode('|', array_unique($resolveReturnTypes))); } + $method->addStmts($tmts); $class->addStmt($method); - yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode()); + yield new File($namespace . 'WebHook\\' . $className, $stmt->addStmt($class)->getNode()); } } diff --git a/src/Generator/WebHooks.php b/src/Generator/WebHooks.php index 9b98c15..e295cdd 100644 --- a/src/Generator/WebHooks.php +++ b/src/Generator/WebHooks.php @@ -2,107 +2,290 @@ namespace ApiClients\Tools\OpenApiClientGenerator\Generator; -use ApiClients\Contracts\HTTP\Headers\AuthenticationInterface; use ApiClients\Contracts\OpenAPI\WebHookInterface; +use ApiClients\Contracts\OpenAPI\WebHooksInterface; use ApiClients\Tools\OpenApiClientGenerator\File; -use cebe\openapi\spec\Operation as OpenAPiOperation; -use cebe\openapi\spec\PathItem; +use ApiClients\Tools\OpenApiClientGenerator\Utils; use Jawira\CaseConverter\Convert; -use League\OpenAPIValidation\Schema\Exception\SchemaMismatch; use PhpParser\Builder\Param; use PhpParser\BuilderFactory; +use PhpParser\Comment\Doc; use PhpParser\Node; -use PhpParser\Node\Stmt\Class_; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ServerRequestInterface; -use React\Http\Browser; -use RingCentral\Psr7\Request; +use PhpParser\Node\Arg; final class WebHooks { /** - * @param string $path - * @param string $namespace - * @param string $baseNamespace - * @param string $className - * @param PathItem $pathItem - * @return iterable - * @throws \Jawira\CaseConverter\CaseConverterException + * @param array $webHooksHydrators + * @param array $webHooks + * @return iterable */ - public static function generate(string $namespace, string $baseNamespace, array $eventClassNameMapping): iterable + public static function generate(string $namespace, array $webHooksHydrators, array $webHooks): iterable { $factory = new BuilderFactory(); - $stmt = $factory->namespace($namespace); + $stmt = $factory->namespace(trim($namespace, '\\')); - $class = $factory->class('WebHooks')->makeFinal()->addStmt( - new Node\Stmt\ClassConst( - [ - new Node\Const_( - new Node\Name('EVENT_CLASS_MAPPING'), - new Node\Expr\Array_((static function (array $eventClassNameMapping): array { - $array = []; + $class = $factory->class('WebHooks')->makeFinal()->implement('\\' . WebHooksInterface::class); + $class->addStmt($factory->property('requestSchemaValidator')->setType('\\' . \League\OpenAPIValidation\Schema\SchemaValidator::class)->makeReadonly()->makePrivate()); + $class->addStmt($factory->property('hydrator')->setType('Hydrators')->makeReadonly()->makePrivate()); - foreach ($eventClassNameMapping as $key => $value) { - $array[] = new Node\Expr\ArrayItem(new Node\Scalar\String_($value), new Node\Scalar\String_($key)); - } - return $array; - })($eventClassNameMapping)) - ), - ], - Class_::MODIFIER_PUBLIC - ) - )->addStmt( - $factory->method('resolve')->makePublic()->makeStatic()->setReturnType('\\' . WebHookInterface::class)->addParam( - (new Param('event'))->setType('string') - )->addStmt( - new Node\Stmt\If_( - new Node\Expr\BooleanNot( - new Node\Expr\FuncCall( - new Node\Name('array_key_exists'), - [ - new Node\Arg( - new Node\Expr\Variable('event') - ), - new Node\Expr\ClassConstFetch( - new Node\Name('self'), - new Node\Name('EVENT_CLASS_MAPPING'), - ), - ] - ) + $constructor = $factory->method('__construct')->makePublic()->addParams([ + (new Param('requestSchemaValidator'))->setType('\\' . \League\OpenAPIValidation\Schema\SchemaValidator::class), + (new Param('hydrator'))->setType('Hydrators'), + ])->addStmts([ + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + ), + new Node\Expr\Variable('requestSchemaValidator'), + ), + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), + new Node\Expr\Variable('hydrator'), + ), + ]); + $class->addStmt($constructor); + + $class->addStmt( + $factory->method('hydrateWebHook')->makePublic()->setDocComment( + new Doc(implode(PHP_EOL, [ + '/**', + ' * @template H', + ' * @param class-string $className', + ' * @return H', + ' */', + ])) + )->setReturnType('object')->addParam( + (new Param('className'))->setType('string') + )->addParam( + (new Param('data'))->setType('array') + )->addStmt(new Node\Stmt\Return_( + new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' ), + new Node\Name('hydrateObject'), [ - 'stmts' => [ - new Node\Stmt\Throw_( - new Node\Expr\New_( - new Node\Name('\InvalidArgumentException') - ) - ) - ], + new Node\Arg(new Node\Expr\Variable('className')), + new Node\Arg(new Node\Expr\Variable('data')), ] ) - )->addStmt( - new Node\Expr\Assign( - new Node\Expr\Variable( - new Node\Name('class') - ), - new Node\Expr\ArrayDimFetch( + )) + ); + + $class->addStmt( + $factory->method('serializeWebHook')->makePublic()->setDocComment( + new Doc(implode(PHP_EOL, [ + '/**', + ' * @return array{className: class-string, data: mixed}', + ' */', + ])) + )->setReturnType('array')->addParam( + (new Param('object'))->setType('object') + )->addStmt(new Node\Stmt\Return_( + new Node\Expr\Array_([ + new Node\Expr\ArrayItem( new Node\Expr\ClassConstFetch( - new Node\Name('self'), - new Node\Name('EVENT_CLASS_MAPPING'), + new Node\Expr\Variable('object'), + 'class', ), - new Node\Expr\Variable('event') - ) - ) - )->addStmt(new Node\Stmt\Return_( - new Node\Expr\New_( - new Node\Expr\Variable( - new Node\Name('class') + new Node\Scalar\String_('className'), + ), + new Node\Expr\ArrayItem( + new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), + new Node\Name('serializeObject'), + [ + new Node\Arg(new Node\Expr\Variable('object')), + ] + ), + new Node\Scalar\String_('data') ) - ) + ]) )) ); - yield new File($namespace . '\\' . 'WebHooks', $stmt->addStmt($class)->getNode()); + $method = $factory->method('resolve')->makePublic()->setReturnType('object')->setDocComment(new Doc(implode(PHP_EOL, [ + '/**', + ' * @return ' . implode('|', array_unique( + (static function (\ApiClients\Tools\OpenApiClientGenerator\Representation\WebHook ...$webHooks) use ($namespace): array { + $schemas = []; + foreach ($webHooks as $webHook) { + foreach ($webHook->schema as $schema) { + $schemas[] = $namespace . 'Schema\\' . $schema->className; + } + } + + return $schemas; + })(...(static function (array $webHooks) { + $hooks = []; + foreach ($webHooks as $hook) { + $hooks = [...$hooks, ...$hook]; + } + + return $hooks; + })($webHooks)) + )), + ' */' + ])))->addParam( + (new Param('headers'))->setType('array') + )->addParam( + (new Param('data'))->setType('array') + ); + $gotoLabels = 'webhooks_aaaaa'; + $tmts = []; + $tmts[] = new Node\Expr\Assign( + new Node\Expr\Variable('headers'), + new Node\Expr\FuncCall( + new Node\Expr\Closure( + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('flatHeaders'), + new Node\Expr\Array_(), + ), + ), + new Node\Stmt\Foreach_( + new Node\Expr\Variable('headers'), + new Node\Expr\Variable('value'), + [ + 'keyVar' => new Node\Expr\Variable('key'), + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\ArrayDimFetch( + new Node\Expr\Variable('flatHeaders'), + new Node\Expr\FuncCall( + new Node\Name('strtolower'), + [ + new Arg( + new Node\Expr\Variable('key'), + ), + ], + ), + ), + new Node\Expr\Variable('value'), + ), + ), + ], + ], + ), + new Node\Stmt\Return_( + new Node\Expr\Variable('flatHeaders'), + ), + ], + 'params' => [ + new Node\Param( + new Node\Expr\Variable('headers'), + ) + ], + 'returnType' => new Node\Name('array'), + 'static' => true, + ], + ), + [ + new Arg( + new Node\Expr\Variable('headers'), + ), + ] + ), + ); + $tmts[] = new Node\Expr\Assign( + new Node\Expr\Variable('error'), + new Node\Expr\New_( + new Node\Name('\\' . \RuntimeException::class), + [ + new Arg(new Node\Scalar\String_('No event matching given headers and data')), + ] + ) + ); + + foreach ($webHooks as $event => $hooks) { + $eventClassname = $namespace . 'WebHook\\' .Utils::className($event); + $eventSanitized = lcfirst((new Convert($event))->toPascal()); + + $class->addStmt($factory->property($eventSanitized)->setType('?' . $eventClassname)->setDefault(null)->makePrivate()); + + $tmts[] = new Node\Stmt\TryCatch([ + new Node\Stmt\If_( + new Node\Expr\BinaryOp\Identical( + new Node\Expr\Instanceof_( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $eventSanitized + ), + new Node\Expr\ConstFetch(new Node\Name($eventClassname)), + ), + new Node\Expr\ConstFetch(new Node\Name('false')), + ), + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $eventSanitized + ), + new Node\Expr\New_( + new Node\Name($eventClassname), + [ + new Node\Arg(new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'requestSchemaValidator' + )), + new Node\Arg(new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + 'hydrator' + ), + 'getObjectMapper' . ucfirst($webHooksHydrators[$event]->methodName) + )), + ] + ), + ), + ), + ], + ] + ), + new Node\Stmt\Return_(new Node\Expr\MethodCall( + new Node\Expr\PropertyFetch( + new Node\Expr\Variable('this'), + $eventSanitized + ), + new Node\Name('resolve'), + [ + new Node\Arg(new Node\Expr\Variable('headers')), + new Node\Arg(new Node\Expr\Variable('data')), + ] + )), + ], [ + new Node\Stmt\Catch_( + [new Node\Name('\\' . \Throwable::class)], + new Node\Expr\Variable('error'), + [ + new Node\Stmt\Goto_($gotoLabels), + ] + ), + ]); + $tmts[] = new Node\Stmt\Label($gotoLabels); + $gotoLabels++; + } + + $tmts[] = new Node\Stmt\Throw_(new Node\Expr\Variable('error')); + + $method->addStmts($tmts); + $class->addStmt($method); + + yield new File($namespace . 'WebHooks', $stmt->addStmt($class)->getNode()); } } diff --git a/src/SchemaRegistry.php b/src/Registry/Schema.php similarity index 60% rename from src/SchemaRegistry.php rename to src/Registry/Schema.php index b725eb3..549f235 100644 --- a/src/SchemaRegistry.php +++ b/src/Registry/Schema.php @@ -1,10 +1,11 @@ @@ -20,16 +21,22 @@ final class SchemaRegistry */ private array $unknownSchemas = []; - public function addClassName(string $className, Schema $schema): void + /** + * @var array + */ + private array $unknownSchemasJson = []; + + public function addClassName(string $className, openAPISchema $schema): void { if ($schema->type === 'array') { $schema = $schema->items; } + $className = Utils::className($className); $this->splHash[spl_object_hash($schema)] = $className; $this->json[json_encode($schema->getSerializableData())] = $className; } - public function get(\cebe\openapi\spec\Schema $schema, string $fallbackName): string + public function get(openAPISchema $schema, string $fallbackName): string { if ($schema->type === 'array') { $schema = $schema->items; @@ -43,8 +50,17 @@ public function get(\cebe\openapi\spec\Schema $schema, string $fallbackName): st if (array_key_exists($json, $this->json)) { return $this->json[$json]; } + if (array_key_exists($json, $this->unknownSchemasJson)) { + return $this->unknownSchemasJson[$json]; + } - $className = Generator::className($fallbackName); + $className = Utils::fixKeyword($fallbackName); + $suffix = 'a'; + while (array_key_exists($className, $this->unknownSchemas)) { + $className = Utils::fixKeyword($fallbackName . strtoupper($suffix++)); + } + $this->splHash[spl_object_hash($schema)] = $className; + $this->unknownSchemasJson[$json] = $className; $this->unknownSchemas[$className] = [ 'name' => $fallbackName, 'className' => $className, diff --git a/src/Representation/Client.php b/src/Representation/Client.php new file mode 100644 index 0000000..153f046 --- /dev/null +++ b/src/Representation/Client.php @@ -0,0 +1,15 @@ + $paths + */ + public readonly array $paths, + ){ + } +} diff --git a/src/Representation/Header.php b/src/Representation/Header.php new file mode 100644 index 0000000..5aa90b8 --- /dev/null +++ b/src/Representation/Header.php @@ -0,0 +1,14 @@ + $schemas */ + public readonly array $schemas, + ){ + } +} diff --git a/src/Representation/Operation.php b/src/Representation/Operation.php new file mode 100644 index 0000000..a0fff9f --- /dev/null +++ b/src/Representation/Operation.php @@ -0,0 +1,28 @@ + $returnType */ + public readonly array $returnType, + /** @var array $parameters */ + public readonly array $parameters, + /** @var array $requestBody */ + public readonly array $requestBody, + /** @var array $response */ + public readonly array $response, + ){ + } +} diff --git a/src/Representation/OperationRequestBody.php b/src/Representation/OperationRequestBody.php new file mode 100644 index 0000000..9cf75e1 --- /dev/null +++ b/src/Representation/OperationRequestBody.php @@ -0,0 +1,14 @@ + */ + public readonly array $operations, + ){ + } +} diff --git a/src/Representation/Property.php b/src/Representation/Property.php new file mode 100644 index 0000000..2323ea7 --- /dev/null +++ b/src/Representation/Property.php @@ -0,0 +1,15 @@ + */ + public readonly array $type, + public readonly bool $nullable, + ){ + } +} diff --git a/src/Representation/PropertyType.php b/src/Representation/PropertyType.php new file mode 100644 index 0000000..0f8c49d --- /dev/null +++ b/src/Representation/PropertyType.php @@ -0,0 +1,12 @@ + */ + public readonly array $properties, + public readonly baseSchema $schema, + public readonly bool $isArray, + ){ + } +} diff --git a/src/Representation/WebHook.php b/src/Representation/WebHook.php new file mode 100644 index 0000000..aa5d4e4 --- /dev/null +++ b/src/Representation/WebHook.php @@ -0,0 +1,19 @@ + */ + public readonly array $headers, + /** @var array */ + public readonly array $schema, + ){ + } +} diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..875e20c --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,64 @@ +toPascal())); + } + + public static function cleanUpNamespace(string $namespace): string + { + $namespace = str_replace('/', '\\', $namespace); + $namespace = str_replace('\\\\', '\\', $namespace); + + return '\\' . $namespace; + } + + public static function fqcn(string $fqcn): string + { + return str_replace('/', '\\', $fqcn); + } + + public static function dirname(string $fqcn): string + { + $fqcn = str_replace('\\', '/', $fqcn); + + return self::cleanUpNamespace(dirname($fqcn)); + } + + public static function basename(string $fqcn): string + { + $fqcn = str_replace('\\', '/', $fqcn); + + return self::cleanUpNamespace(basename($fqcn)); + } + + public static function fixKeyword(string $name): string + { + $name = self::fqcn($name); + $nameBoom = explode('\\', $name); + return $name . (in_array( + strtolower($nameBoom[count($nameBoom) - 1]), array('__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'self', 'parent', 'object'), + false + ) ? '_' : ''); + } +}