Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/Generator/ClientGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class ClientGenerator
{
private SchemaToClass $classBuilder;
private WriterInterface $writer;
private SchemaReferenceLookup $referenceLookup;
private SchemaReferenceLookup $requestReferenceLookup;
private SchemaReferenceLookup $responseReferenceLookup;

public function __construct(
private readonly Context $context,
Expand All @@ -45,7 +46,10 @@ public function __construct(
$output = new ConsoleOutput();
$this->writer = new FileWriter($output);
$this->classBuilder = $s2c->build($this->writer, $output);
$this->referenceLookup = new SchemaReferenceLookup($this->context);
// Unknown enum values from the API must not break deserialization of responses; in
// requests they are still an error on the caller's side and fail before being sent.
$this->requestReferenceLookup = new SchemaReferenceLookup($this->context);
$this->responseReferenceLookup = new SchemaReferenceLookup($this->context, tolerantEnums: true);
}

/**
Expand Down Expand Up @@ -254,7 +258,7 @@ private function buildOperationRequestClass(string $namespace, string $methodNam
$req = $req->withAdditionalMethod($buildUrlMethod);
$req = $req->withAdditionalMethod($buildRequestOptionsMethod);
$req = $req->withAdditionalMethod($withHeaderMethod);
$req = $req->withReferenceLookup($this->referenceLookup);
$req = $req->withReferenceLookup($this->requestReferenceLookup);

$this->classBuilder->schemaToClass($req);
return $paramClassNameFQ;
Expand Down Expand Up @@ -350,7 +354,7 @@ private function buildOperationMethod(string $namespace, string $tag, string $pa
];

$req = new GeneratorRequest($envelopedResponseSchema, new ValidatedSpecificationFilesItem($responseClassNamespace, $responseClassName, $outputDir), $this->generatorOpts)
->withReferenceLookup($this->referenceLookup)
->withReferenceLookup($this->responseReferenceLookup)
->withAdditionalMethod($factoryMethod)
->withAdditionalMethod($getResponseMethod)
->withAdditionalProperty($httpResponseProperty)
Expand Down
7 changes: 6 additions & 1 deletion src/Generator/ComponentGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public function generate(string $baseNamespace, array $component, string $compon
return;
}

// Add a fallback case for values added to the API after generation; see TolerantReferencedTypeEnum.
if (isset($component["enum"]) && ($component["type"] ?? null) === "string") {
$component["enum"][] = TolerantReferencedTypeEnum::FallbackValue;
}

$className = $baseNamespace . "\\" . static::componentNameToClassName($componentName);
$namespace = substr($className, 0, strrpos($className, "\\"));
$classNameWithoutNamespace = substr($className, strrpos($className, "\\") + 1);
Expand All @@ -81,7 +86,7 @@ public function generate(string $baseNamespace, array $component, string $compon
->withNewValidatorClassExpr("new \Mittwald\ApiClient\Validator\Validator()");

$request = new GeneratorRequest($component, $spec, $opts);
$request = $request->withReferenceLookup(new SchemaReferenceLookup($this->context));
$request = $request->withReferenceLookup(new SchemaReferenceLookup($this->context, tolerantEnums: true));
$request = $request->withHook(new class($component, $componentName) implements ClassCreatedHook {
function __construct(private readonly array $component, private readonly string $componentName) {}

Expand Down
18 changes: 15 additions & 3 deletions src/Generator/SchemaReferenceLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

class SchemaReferenceLookup implements ReferenceLookup
{
public function __construct(private readonly Context $context)
public function __construct(
private readonly Context $context,
private readonly bool $tolerantEnums = false,
)
{
}

Expand Down Expand Up @@ -79,12 +82,21 @@ public function lookupSchema(string $reference): array
return $this->context->schema["components"][$componentType][$name];
}

private function buildEnumReference(string $fqcn, array $schema): ReferencedTypeEnum
{
if ($this->tolerantEnums && ($schema["type"] ?? null) === "string") {
return new TolerantReferencedTypeEnum($fqcn);
}

return new ReferencedTypeEnum($fqcn);
}

private function buildTypeReference(string $fqcn, array $schema): ReferencedType
{
return match (true) {
isset($schema["enum"]) => new ReferencedTypeEnum($fqcn),
isset($schema["enum"]) => $this->buildEnumReference($fqcn, $schema),
isset($schema["items"]["\$ref"]) => new ReferencedTypeList($this->lookupReference($schema["items"]["\$ref"])),
isset($schema["items"]["enum"]) => new ReferencedTypeList(new ReferencedTypeEnum($fqcn . "Item")),
isset($schema["items"]["enum"]) => new ReferencedTypeList($this->buildEnumReference($fqcn . "Item", $schema["items"])),
isset($schema["items"]) => new ReferencedTypeList($this->buildTypeReference($fqcn . "Item", $schema["items"])),
isset($schema["type"]) && $schema["type"] === "string" => new ReferencedString(),
isset($schema["oneOf"]) => $this->buildUnionType($fqcn, $schema["oneOf"]),
Expand Down
25 changes: 25 additions & 0 deletions src/Generator/TolerantReferencedTypeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Mittwald\ApiToolsPHP\Generator;

use Helmich\Schema2Class\Generator\GeneratorRequest;
use Helmich\Schema2Class\Generator\ReferencedTypeEnum;
use Helmich\Schema2Class\Generator\SchemaToEnum;

/**
* A referenced enum that tolerates values the generated enum does not know, by mapping them to a
* fallback case instead of raising a ValueError. This keeps released clients working when new
* values are added to an enum in the API schema. The fallback case is added to the generated enums
* by appending {@see self::FallbackValue} to their schema (see ComponentGenerator).
*/
readonly class TolerantReferencedTypeEnum extends ReferencedTypeEnum
{
public const FallbackValue = "__unknown__";

public function inputMappingExpr(GeneratorRequest $req, string $expr, ?string $validateExpr): string
{
$fallbackCase = SchemaToEnum::enumCaseName(self::FallbackValue);

return "(\\{$this->name()}::tryFrom({$expr}) ?? \\{$this->name()}::{$fallbackCase})";
}
}
Loading