From 079c02704c912be474c49e6f8a4f81f2b8e82b7a Mon Sep 17 00:00:00 2001 From: Martin Helmich Date: Mon, 11 Dec 2023 18:22:11 +0100 Subject: [PATCH] Fix issues in code generation --- composer.json | 12 ++++++- composer.lock | 18 +++++----- src/Generator/ClientFactoryGenerator.php | 45 ++++++++++++++++++++++-- src/Generator/ClientGenerator.php | 34 +++++++++++++----- src/Generator/ComponentGenerator.php | 9 ++--- src/Generator/SchemaReferenceLookup.php | 7 ++++ src/Utils/Strings/Lowercaser.php | 32 +++++++++++++++++ 7 files changed, 132 insertions(+), 25 deletions(-) create mode 100644 src/Utils/Strings/Lowercaser.php diff --git a/composer.json b/composer.json index c630390..6401533 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,17 @@ ], "require": { "symfony/console": "^6.3", - "helmich/schema2class": "^3.1.3" + "helmich/schema2class": "^3.2.0" + }, + "scripts": { + "clone-target": "if [ -d .work ] ; then cd .work && git fetch && git checkout -- . && git clean -fxd . && git reset --hard origin/master ; else git clone https://github.com/mittwald/api-client-php.git ./.work ; fi", + "setup-target": "cd .work && composer install --no-interaction", + "generate": [ + "@clone-target", + "@setup-target", + "php ./cmd/generate.php generate https://api.mittwald.de/openapi ./.work -v", + "cd .work && composer check" + ] }, "minimum-stability": "dev", "prefer-stable": true diff --git a/composer.lock b/composer.lock index 96f9c24..380d671 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a8e758ee1fc44fd9a5ece1cfc3de2296", + "content-hash": "e51ddb5a925d966d3848edfa87ab4342", "packages": [ { "name": "composer/semver", @@ -89,16 +89,16 @@ }, { "name": "helmich/schema2class", - "version": "v3.1.3", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/martin-helmich/php-schema2class.git", - "reference": "c242eae9ee4a7ff89aa4acd3e861578fa54a88a2" + "reference": "500fb6a732804edcd9b6eda9fc12646a456e740b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/martin-helmich/php-schema2class/zipball/c242eae9ee4a7ff89aa4acd3e861578fa54a88a2", - "reference": "c242eae9ee4a7ff89aa4acd3e861578fa54a88a2", + "url": "https://api.github.com/repos/martin-helmich/php-schema2class/zipball/500fb6a732804edcd9b6eda9fc12646a456e740b", + "reference": "500fb6a732804edcd9b6eda9fc12646a456e740b", "shasum": "" }, "require": { @@ -113,7 +113,7 @@ "require-dev": { "phpspec/prophecy": "^1.17", "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.5", "vimeo/psalm": "^5.12" }, "bin": [ @@ -138,7 +138,7 @@ "description": "Build PHP classes from JSON schema definitions", "support": { "issues": "https://github.com/martin-helmich/php-schema2class/issues", - "source": "https://github.com/martin-helmich/php-schema2class/tree/v3.1.3" + "source": "https://github.com/martin-helmich/php-schema2class/tree/v3.2.0" }, "funding": [ { @@ -150,7 +150,7 @@ "type": "github" } ], - "time": "2023-11-09T18:03:00+00:00" + "time": "2023-12-11T17:02:57+00:00" }, { "name": "justinrainbow/json-schema", @@ -1074,5 +1074,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Generator/ClientFactoryGenerator.php b/src/Generator/ClientFactoryGenerator.php index 4b84cc4..dd1bc65 100644 --- a/src/Generator/ClientFactoryGenerator.php +++ b/src/Generator/ClientFactoryGenerator.php @@ -8,7 +8,9 @@ use Laminas\Code\Generator\DocBlock\Tag\GenericTag; use Laminas\Code\Generator\DocBlockGenerator; use Laminas\Code\Generator\FileGenerator; +use Laminas\Code\Generator\InterfaceGenerator; use Laminas\Code\Generator\MethodGenerator; +use Mittwald\ApiToolsPHP\Utils\Strings\Lowercaser; use Symfony\Component\Console\Output\ConsoleOutput; class ClientFactoryGenerator @@ -23,11 +25,48 @@ public function __construct(private readonly Context $context) public function generate(string $namespace, array $clients): void { - $clsName = "Client"; + $this->generateInterface($namespace, $clients); + $this->generateImplementation($namespace, $clients); + } + + private function generateInterface(string $namespace, array $clients): void + { + $ifaceName = "Client"; + $iface = new InterfaceGenerator( + name: $ifaceName, + namespaceName: $namespace, + ); + + $iface->setDocBlock(new DocBlockGenerator( + shortDescription: "Auto-generated factory for mittwald mStudio v{$this->context->version} clients.", + longDescription: CommentUtils::AutoGenerationNotice, + )); + + foreach ($clients as [$clientName, $clientNamespace]) { + $method = new MethodGenerator(name: Lowercaser::abbreviationAwareLowercase($clientName)); + $method->setReturnType("{$clientNamespace}\\{$clientName}Client"); + + $iface->addMethodFromGenerator($method); + } + + $file = new FileGenerator(); + $file->setClass($iface); + $file->setNamespace($namespace); + + $outputDir = GeneratorUtil::outputDirForClass($this->context, $namespace . "\\" . $ifaceName); + $outputFile = $outputDir . "/" . $ifaceName . ".php"; + + $this->writer->writeFile($outputFile, $file->generate()); + } + + private function generateImplementation(string $namespace, array $clients): void + { + $clsName = "ClientImpl"; $cls = new ClassGenerator( name: $clsName, namespaceName: $namespace, extends: "Mittwald\\ApiClient\\Client\\BaseClient", + interfaces: ["{$namespace}\\Client"], ); $cls->setDocBlock(new DocBlockGenerator( @@ -36,9 +75,9 @@ public function generate(string $namespace, array $clients): void )); foreach ($clients as [$clientName, $clientNamespace]) { - $method = new MethodGenerator(name: lcfirst($clientName)); + $method = new MethodGenerator(name: Lowercaser::abbreviationAwareLowercase($clientName)); $method->setReturnType("{$clientNamespace}\\{$clientName}Client"); - $method->setBody("return new \\{$clientNamespace}\\{$clientName}Client(\$this->client);"); + $method->setBody("return new \\{$clientNamespace}\\{$clientName}ClientImpl(\$this->client);"); $cls->addMethodFromGenerator($method); } diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 8ddcca6..a1daa03 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -21,6 +21,7 @@ use Laminas\Code\Generator\DocBlock\Tag\ThrowsTag; use Laminas\Code\Generator\DocBlockGenerator; use Laminas\Code\Generator\FileGenerator; +use Laminas\Code\Generator\InterfaceGenerator; use Laminas\Code\Generator\MethodGenerator; use Laminas\Code\Generator\ParameterGenerator; use Laminas\Code\Generator\PropertyGenerator; @@ -50,7 +51,8 @@ public function __construct( */ public function generate(string $baseNamespace, array $tag): void { - $clsName = ucfirst(preg_replace("/[^a-zA-Z0-9]/", "", $tag["name"])) . "Client"; + $ifaceName = ucfirst(preg_replace("/[^a-zA-Z0-9]/", "", $tag["name"])) . "Client"; + $clsName = $ifaceName . "Impl"; $operations = $this->collectOperations($tag["name"]); $operationMethods = $this->buildOperationMethods($baseNamespace, $tag["name"], $operations); @@ -76,25 +78,41 @@ public function generate(string $baseNamespace, array $tag): void new GenericTag("see", CommentUtils::AutoGeneratorURL), ], ); - $cls = new ClassGenerator($clsName, $baseNamespace, properties: $props, methods: [$constructor, ...$operationMethods]); + $cls = new ClassGenerator($clsName, $baseNamespace, properties: $props, methods: [$constructor, ...$operationMethods], interfaces: ["{$baseNamespace}\\{$ifaceName}"]); $cls->setDocBlock($clsComment); - $file = new FileGenerator(); - $file->setClass($cls); - $file->setNamespace($baseNamespace); - $file->setUses([ + $ifaceMethods = array_map(fn(MethodGenerator $m) => clone $m, $operationMethods); + + $iface = new InterfaceGenerator($ifaceName, $baseNamespace, methods: $ifaceMethods); + $iface->setDocBlock($clsComment); + + $clsFile = new FileGenerator(); + $clsFile->setClass($cls); + $clsFile->setNamespace($baseNamespace); + $clsFile->setUses([ 'GuzzleHttp\\Psr7\\Request', ]); + $ifaceFile = new FileGenerator(); + $ifaceFile->setClass($iface); + $ifaceFile->setNamespace($baseNamespace); + $outputDir = GeneratorUtil::outputDirForClass($this->context, $baseNamespace . "\\" . $clsName); - $content = $file->generate(); + $clsContent = self::sanitizeOutput($clsFile->generate(), $baseNamespace); + $ifaceContent = self::sanitizeOutput($ifaceFile->generate(), $baseNamespace); + $this->writer->writeFile("{$outputDir}/{$ifaceName}.php", $ifaceContent); + $this->writer->writeFile("{$outputDir}/{$clsName}.php", $clsContent); + } + + private function sanitizeOutput(string $content, string $baseNamespace): string + { // Do some corrections because the Zend code generation library is stupid. $content = preg_replace('/ : \\\\self/', ' : self', $content); $content = preg_replace('/\\\\' . preg_quote($baseNamespace) . '\\\\/', '', $content); - $this->writer->writeFile("{$outputDir}/{$clsName}.php", $content); + return $content; } private function buildOperationMethods(string $namespace, string $tag, array $operations): array diff --git a/src/Generator/ComponentGenerator.php b/src/Generator/ComponentGenerator.php index 3ee9f40..7928b9c 100644 --- a/src/Generator/ComponentGenerator.php +++ b/src/Generator/ComponentGenerator.php @@ -45,8 +45,8 @@ public function __construct(Context $context, SchemaToClassFactory $s2c) public function generate(string $baseNamespace, array $component, string $componentName): void { - // Special treatment for inlined enums - if (isset($component["items"]["enum"])) { + // Special treatment for inlined item types + if (isset($component["items"])) { $this->generate($baseNamespace, $component["items"], $componentName . "Item"); return; } @@ -60,7 +60,7 @@ public function generate(string $baseNamespace, array $component, string $compon return; } - if (!isset($component["properties"]) && !(isset($component["enum"]))) { + if (!isset($component["properties"]) && !isset($component["enum"]) && !isset($component["allOf"])) { trigger_error("Component {$componentName} is not an object, skipping.", E_USER_WARNING); return; } @@ -73,7 +73,8 @@ public function generate(string $baseNamespace, array $component, string $compon $spec = new ValidatedSpecificationFilesItem($namespace, $classNameWithoutNamespace, $outputDir); $opts = (new SpecificationOptions()) ->withTargetPHPVersion("8.2") - ->withTreatValuesWithDefaultAsOptional(true); + ->withTreatValuesWithDefaultAsOptional(true) + ->withInlineAllofReferences(true); $request = new GeneratorRequest($component, $spec, $opts); $request = $request->withReferenceLookup(new SchemaReferenceLookup($this->context)); diff --git a/src/Generator/SchemaReferenceLookup.php b/src/Generator/SchemaReferenceLookup.php index 6b0f858..713d3af 100644 --- a/src/Generator/SchemaReferenceLookup.php +++ b/src/Generator/SchemaReferenceLookup.php @@ -73,12 +73,19 @@ public function lookupReference(string $reference): ReferencedType return $this->buildTypeReference($fqcn, $schema); } + public function lookupSchema(string $reference): array + { + [, , $componentType, $name] = explode("/", $reference); + return $this->context->schema["components"][$componentType][$name]; + } + private function buildTypeReference(string $fqcn, array $schema): ReferencedType { return match (true) { isset($schema["enum"]) => new ReferencedTypeEnum($fqcn), isset($schema["items"]["\$ref"]) => new ReferencedTypeList($this->lookupReference($schema["items"]["\$ref"])), isset($schema["items"]["enum"]) => new ReferencedTypeList(new ReferencedTypeEnum($fqcn . "Item")), + isset($schema["items"]) => $this->buildTypeReference($fqcn . "Item", $schema["items"]), isset($schema["type"]) && $schema["type"] === "string" => new ReferencedString(), isset($schema["oneOf"]) => $this->buildUnionType($fqcn, $schema["oneOf"]), default => new ReferencedTypeClass($fqcn), diff --git a/src/Utils/Strings/Lowercaser.php b/src/Utils/Strings/Lowercaser.php new file mode 100644 index 0000000..7f169d1 --- /dev/null +++ b/src/Utils/Strings/Lowercaser.php @@ -0,0 +1,32 @@ +