diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 517715f..f2c82d9 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); } /** @@ -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; @@ -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) diff --git a/src/Generator/ComponentGenerator.php b/src/Generator/ComponentGenerator.php index 67b34b0..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,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) {} 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})"; + } +}