From 24af6ed661739a6a3ddf55c1edb325a3d7d3b6d0 Mon Sep 17 00:00:00 2001 From: Aykut Yalcin Date: Fri, 28 Aug 2026 17:12:11 +0200 Subject: [PATCH 1/4] fix client gen, enable component unknown enum fallback --- src/Generator/ClientGenerator.php | 11 +++++++++-- src/Generator/ComponentGenerator.php | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 517715f..64896f0 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -130,9 +130,15 @@ private function buildOperationMethods(string $namespace, string $tag, array $op continue; } - $methods[] = $this->buildOperationMethod($namespace, $tag, $path, $method, $operationData); + $methodName = $this->mapOperationId($tag, $operationData["operationId"]); + if (isset($methods[$methodName])) { + continue; + } + + $methods[$methodName] = $this->buildOperationMethod($namespace, $tag, $path, $method, $operationData); } - return $methods; + + return array_values($methods); } private function buildOperationRequestClass(string $namespace, string $methodName, string $httpMethod, string $path, array $operationData): string @@ -351,6 +357,7 @@ private function buildOperationMethod(string $namespace, string $tag, string $pa $req = new GeneratorRequest($envelopedResponseSchema, new ValidatedSpecificationFilesItem($responseClassNamespace, $responseClassName, $outputDir), $this->generatorOpts) ->withReferenceLookup($this->referenceLookup) + ->withUnknownEnumFallback() ->withAdditionalMethod($factoryMethod) ->withAdditionalMethod($getResponseMethod) ->withAdditionalProperty($httpResponseProperty) diff --git a/src/Generator/ComponentGenerator.php b/src/Generator/ComponentGenerator.php index 67b34b0..d457011 100644 --- a/src/Generator/ComponentGenerator.php +++ b/src/Generator/ComponentGenerator.php @@ -82,6 +82,7 @@ public function generate(string $baseNamespace, array $component, string $compon $request = new GeneratorRequest($component, $spec, $opts); $request = $request->withReferenceLookup(new SchemaReferenceLookup($this->context)); + $request = $request->withUnknownEnumFallback(); $request = $request->withHook(new class($component, $componentName) implements ClassCreatedHook { function __construct(private readonly array $component, private readonly string $componentName) {} From f56d05e54287a8e85a758076a9215458c390ad12 Mon Sep 17 00:00:00 2001 From: Aykut Yalcin Date: Fri, 28 Aug 2026 17:45:45 +0200 Subject: [PATCH 2/4] TolerantReferencedTypeEnum --- src/Generator/ClientGenerator.php | 13 ++++++---- src/Generator/ComponentGenerator.php | 8 +++++-- src/Generator/SchemaReferenceLookup.php | 18 +++++++++++--- src/Generator/TolerantReferencedTypeEnum.php | 25 ++++++++++++++++++++ 4 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 src/Generator/TolerantReferencedTypeEnum.php diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 64896f0..3301a0b 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -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, @@ -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); } /** @@ -260,7 +264,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; @@ -356,8 +360,7 @@ private function buildOperationMethod(string $namespace, string $tag, string $pa ]; $req = new GeneratorRequest($envelopedResponseSchema, new ValidatedSpecificationFilesItem($responseClassNamespace, $responseClassName, $outputDir), $this->generatorOpts) - ->withReferenceLookup($this->referenceLookup) - ->withUnknownEnumFallback() + ->withReferenceLookup($this->responseReferenceLookup) ->withAdditionalMethod($factoryMethod) ->withAdditionalMethod($getResponseMethod) ->withAdditionalProperty($httpResponseProperty) diff --git a/src/Generator/ComponentGenerator.php b/src/Generator/ComponentGenerator.php index d457011..d418523 100644 --- a/src/Generator/ComponentGenerator.php +++ b/src/Generator/ComponentGenerator.php @@ -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); @@ -81,8 +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->withUnknownEnumFallback(); + $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) {} diff --git a/src/Generator/SchemaReferenceLookup.php b/src/Generator/SchemaReferenceLookup.php index 697bdc3..848f241 100644 --- a/src/Generator/SchemaReferenceLookup.php +++ b/src/Generator/SchemaReferenceLookup.php @@ -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, + ) { } @@ -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"]), diff --git a/src/Generator/TolerantReferencedTypeEnum.php b/src/Generator/TolerantReferencedTypeEnum.php new file mode 100644 index 0000000..c7520d5 --- /dev/null +++ b/src/Generator/TolerantReferencedTypeEnum.php @@ -0,0 +1,25 @@ +name()}::tryFrom({$expr}) ?? \\{$this->name()}::{$fallbackCase})"; + } +} From 1f327f8eebc6578398a701b295cfd818dbae31ce Mon Sep 17 00:00:00 2001 From: Aykut Yalcin Date: Mon, 31 Aug 2026 09:49:22 +0200 Subject: [PATCH 3/4] revert double operation name handling --- src/Generator/ClientGenerator.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 3301a0b..44a9f91 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -139,10 +139,9 @@ private function buildOperationMethods(string $namespace, string $tag, array $op continue; } - $methods[$methodName] = $this->buildOperationMethod($namespace, $tag, $path, $method, $operationData); + $methods[] = $this->buildOperationMethod($namespace, $tag, $path, $method, $operationData); } - - return array_values($methods); + return $methods; } private function buildOperationRequestClass(string $namespace, string $methodName, string $httpMethod, string $path, array $operationData): string From f9e31ab3ad6fb1f7e6ba9f79305847bb251b3f51 Mon Sep 17 00:00:00 2001 From: Aykut Yalcin Date: Mon, 31 Aug 2026 09:49:55 +0200 Subject: [PATCH 4/4] revert double operation name handling --- src/Generator/ClientGenerator.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 44a9f91..f2c82d9 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -134,11 +134,6 @@ private function buildOperationMethods(string $namespace, string $tag, array $op continue; } - $methodName = $this->mapOperationId($tag, $operationData["operationId"]); - if (isset($methods[$methodName])) { - continue; - } - $methods[] = $this->buildOperationMethod($namespace, $tag, $path, $method, $operationData); } return $methods;