Skip to content

Commit

Permalink
Fix issues in code generation
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-helmich committed Dec 11, 2023
1 parent 8ecb2bd commit 079c027
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 25 deletions.
12 changes: 11 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 42 additions & 3 deletions src/Generator/ClientFactoryGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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);
}
Expand Down
34 changes: 26 additions & 8 deletions src/Generator/ClientGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/Generator/ComponentGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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));
Expand Down
7 changes: 7 additions & 0 deletions src/Generator/SchemaReferenceLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
32 changes: 32 additions & 0 deletions src/Utils/Strings/Lowercaser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Mittwald\ApiToolsPHP\Utils\Strings;

class Lowercaser
{
private static $commonAbbreviations = [
"SSH",
"SSL",
"API",
"URL",
"URI",
"UUID",
"DNS",
"HTTPS",
"HTTP",
"FTP",
"SFTP",

];

public static function abbreviationAwareLowercase(string $input): string
{
foreach (self::$commonAbbreviations as $abbreviation) {
if (str_starts_with(strtoupper($input), $abbreviation)) {
return strtolower($abbreviation) . substr($input, strlen($abbreviation));
}
}

return lcfirst($input);
}
}

0 comments on commit 079c027

Please sign in to comment.