diff --git a/CHANGELOG.md b/CHANGELOG.md index 86331be..2771dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [11.4.0] - 2026-09-28 +### Fixed +Generation failures: +- A nullable optional path, query or header parameter no longer aborts generation with "Nullable optional parameter is not supported". Setting it to `null` leaves it out, the same as leaving it unset +- A request body media type without a `schema` (e.g. `application/x-www-form-urlencoded: {}`) no longer crashes the generator. The request sends that `Content-Type` with no body. With several media types, the body is now taken from the one that has a schema, whatever their order +- A schema named after a PHP reserved word (`Match`, `List`, `Class`, ...) now gets a `Schema` suffix (`MatchSchema`, with a warning) instead of producing a class that doesn't parse on PHP 8. The enum value `class` becomes `V_CLASS` +- A literal (scalar) body among several 2xx responses no longer aborts generation + +Generated code that threw or misbehaved at runtime: +- Integer, number and boolean header parameters (including integer-backed enums) are now sent as strings; `guzzlehttp/psr7` 2.11+ raised `E_USER_DEPRECATED` for them. Booleans are sent as `1`/`0`, like query and cookie parameters +- Request bodies whose schema is a primitive or an array of non-objects are now serialized, through the new copied `Request\LiteralRequestBody`. They used to fail on every call +- A `nullable: true` property defined as `allOf: [$ref]` (the OpenAPI 3.0 nullable-reference idiom) now keeps its nullability instead of throwing a `TypeError` on `null` +- Optional nullable enum properties map `null` to `null` instead of calling `Enum::from(null)` (8.1+) +- oneOf/anyOf alternatives without a discriminator that differ only by an enum value are matched correctly: the enum `ValueError` is caught like `UnexpectedResponseBodyException` (8.1+) +- A discriminator value that the mapping (or implicit schema-name mapping) doesn't cover now throws `UnexpectedResponseBodyException` instead of a PHP `Error` from a dynamic mapper call +- Arrays of arrays of objects are mapped to arrays of their collections, as the docblocks already declared, instead of staying raw arrays +- A 2xx status code not listed in the specification now throws `RuntimeException` on 8.0+ targets too, as the pre-8.0 `switch` already did, instead of `UnhandledMatchError` +- An empty or `null` body for a non-nullable literal (scalar) response throws `UnexpectedResponseBodyException` instead of an "Undefined array key" warning followed by a `TypeError`; a nullable one returns `null` +- Inline enum response bodies get their enum class generated (8.1+). The return type used to name a class that did not exist +- Optional untyped (`mixed`) properties are initialised with `null` (8.0+) instead of failing with "must not be accessed before initialization"; `nullable: true` without a type no longer produces the invalid `?mixed` +- Enum values made only of symbols (`*`, `=`, `>=`, ...) get names such as `ASTERISK` and `EQUALS` instead of being dropped, which could leave an enum with no cases +- Different schemas that derive the same class name, such as an enum property and an enum parameter, or an inline schema and a component, get distinct classes with a numeric suffix (`CallStatus2Enum`, `HotDeal2`) and a warning, instead of silently merging into one. Identical definitions still share a class + +### Changed +These are the changes regenerated clients can notice: +- The copied `AbstractJsonContentTypeSerializer` unwraps `LiteralRequestBody` values, so it changes in every generated client that uses JSON +- A `false` boolean header is sent as `0` instead of an empty value +- Properties using the nullable `allOf` idiom become nullable in their constructor, getter and property type +- Arrays of arrays of objects now return collections (`FooCollection[]`) instead of raw arrays +- In a specification with a discriminator mapping, a value that is not a mapping key no longer maps by accident when it equals the lcfirst class name of a mapped alternative; it throws `UnexpectedResponseBodyException` +- Array-of-enum response items no longer get an `{X}EnumMapper` class and service entry (8.1+); it had an empty `toSchema()` and was never used +- Schemas named after reserved words and colliding schemas get new class names as described above. Existing, working class names are kept + ## [11.3.0] - 2026-09-28 ### Fixed These affect `CLIENT_PHP_VERSION` 8.1 and later, where enums are generated as native PHP enums. diff --git a/example/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php b/example/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php index f6c6110..0277978 100644 --- a/example/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php +++ b/example/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php @@ -24,7 +24,12 @@ abstract class AbstractJsonContentTypeSerializer implements ContentTypeSerialize */ public function encode(SerializableInterface $body): string { - $encodedData = json_encode($body->toArray(), self::JSON_OPTIONS); + $data = $body->toArray(); + // A literal (non-object) body is wrapped the same way decode() wraps a literal response. + if (array_keys($data) === [ContentTypeSerializerInterface::LITERAL_VALUE_KEY]) { + $data = $data[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + $encodedData = json_encode($data, self::JSON_OPTIONS); $lastErrorCode = json_last_error(); if ($lastErrorCode === JSON_ERROR_NONE && $encodedData !== false) { return $encodedData; diff --git a/example/PetStoreClient/src/SwaggerPetstoreOpenAPI3Client.php b/example/PetStoreClient/src/SwaggerPetstoreOpenAPI3Client.php index b394423..1a5c1a3 100644 --- a/example/PetStoreClient/src/SwaggerPetstoreOpenAPI3Client.php +++ b/example/PetStoreClient/src/SwaggerPetstoreOpenAPI3Client.php @@ -10,6 +10,7 @@ namespace OpenApi\PetStoreClient; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use OpenApi\PetStoreClient\Request\AddPetRequest; use OpenApi\PetStoreClient\Request\CreateUserRequest; use OpenApi\PetStoreClient\Request\CreateUsersWithListInputRequest; @@ -149,6 +150,9 @@ public function createUsersWithListInput(CreateUsersWithListInputRequest $reques public function loginUser(LoginUserRequest $request): string { $response = $this->handleResponse($this->sendRequest($request)); + if (!isset($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY])) { + throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; } diff --git a/example3_1/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php b/example3_1/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php index f6c6110..0277978 100644 --- a/example3_1/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php +++ b/example3_1/PetStoreClient/src/Serializer/ContentType/AbstractJsonContentTypeSerializer.php @@ -24,7 +24,12 @@ abstract class AbstractJsonContentTypeSerializer implements ContentTypeSerialize */ public function encode(SerializableInterface $body): string { - $encodedData = json_encode($body->toArray(), self::JSON_OPTIONS); + $data = $body->toArray(); + // A literal (non-object) body is wrapped the same way decode() wraps a literal response. + if (array_keys($data) === [ContentTypeSerializerInterface::LITERAL_VALUE_KEY]) { + $data = $data[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + $encodedData = json_encode($data, self::JSON_OPTIONS); $lastErrorCode = json_last_error(); if ($lastErrorCode === JSON_ERROR_NONE && $encodedData !== false) { return $encodedData; diff --git a/example3_1/PetStoreClient/src/SwaggerPetstoreOpenAPI31Client.php b/example3_1/PetStoreClient/src/SwaggerPetstoreOpenAPI31Client.php index 7c020a7..8f222d3 100644 --- a/example3_1/PetStoreClient/src/SwaggerPetstoreOpenAPI31Client.php +++ b/example3_1/PetStoreClient/src/SwaggerPetstoreOpenAPI31Client.php @@ -10,6 +10,7 @@ namespace OpenApi\PetStoreClient; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use OpenApi\PetStoreClient\Request\AddPetRequest; use OpenApi\PetStoreClient\Request\CreateUserRequest; use OpenApi\PetStoreClient\Request\CreateUsersWithListInputRequest; @@ -149,6 +150,9 @@ public function createUsersWithListInput(CreateUsersWithListInputRequest $reques public function loginUser(LoginUserRequest $request): string { $response = $this->handleResponse($this->sendRequest($request)); + if (!isset($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY])) { + throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; } diff --git a/src/Ast/Builder/CodeBuilder.php b/src/Ast/Builder/CodeBuilder.php index b413b2e..fa302b0 100644 --- a/src/Ast/Builder/CodeBuilder.php +++ b/src/Ast/Builder/CodeBuilder.php @@ -44,6 +44,7 @@ use PhpParser\Node\Expr\NullsafePropertyFetch; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Ternary; +use PhpParser\Node\Expr\Throw_ as ThrowExpression; use PhpParser\Node\Expr\Variable; use PhpParser\Node\MatchArm; use PhpParser\Node\Name; @@ -170,9 +171,12 @@ public function localProperty( ->makePrivate(); if (!empty($type) && $this->phpVersion->isPropertyTypeHintSupported()) { - if ($nullable && $type !== FieldType::PHP_TYPE_MIXED) { + if ($nullable) { $property->setDefault(null); - $type = '?' . $type; + // mixed already includes null and cannot be marked nullable + if ($type !== FieldType::PHP_TYPE_MIXED) { + $type = '?' . $type; + } } $property->setType($type); @@ -260,6 +264,11 @@ public function castToArray(Expr $expr): Cast\Array_ return new Cast\Array_($expr); } + public function castToString(Expr $expr): Cast\String_ + { + return new Cast\String_($expr); + } + public function expr(Expr $expr): Expression { return new Expression($expr); @@ -280,6 +289,11 @@ public function matchArm(array $conditions, Expr $body): MatchArm return new MatchArm($conditions, $body); } + public function defaultMatchArm(Expr $body): MatchArm + { + return new MatchArm(null, $body); + } + public function switch(Expr $condition, Case_ ...$cases): Switch_ { return new Switch_($condition, $cases); @@ -370,6 +384,16 @@ public function throw(string $exceptionClassName, Expr $message = null): Stmt return new Throw_($this->new($exceptionClassName, $args)); } + public function throwExpression(string $exceptionClassName, ?Expr $message = null): ThrowExpression + { + $args = []; + if ($message !== null) { + $args = [$message]; + } + + return new ThrowExpression($this->new($exceptionClassName, $args)); + } + public function foreach(Expr $array, Variable $asValue, array $stmts, Variable $asKey = null): Foreach_ { $subNodes = ['stmts' => $stmts]; diff --git a/src/Ast/Builder/ParameterBuilder.php b/src/Ast/Builder/ParameterBuilder.php index ed2bfeb..899823d 100644 --- a/src/Ast/Builder/ParameterBuilder.php +++ b/src/Ast/Builder/ParameterBuilder.php @@ -6,6 +6,7 @@ use DoclerLabs\ApiClientGenerator\Ast\ParameterNode; use DoclerLabs\ApiClientGenerator\Ast\PhpVersion; +use DoclerLabs\ApiClientGenerator\Entity\FieldType; use PhpParser\Builder\Param; use PhpParser\Node\Expr\Variable; @@ -26,7 +27,8 @@ public function setType($type, bool $isNullable = false): self if ($isNullable) { if ($this->phpVersion->isNullableTypeHintSupported() && is_string($type)) { - return parent::setType(sprintf('?%s', $type)); + // mixed already includes null and cannot be marked nullable + return parent::setType($type === FieldType::PHP_TYPE_MIXED ? $type : sprintf('?%s', $type)); } return $this; diff --git a/src/Ast/PhpVersion.php b/src/Ast/PhpVersion.php index 22d075b..3fb5e32 100644 --- a/src/Ast/PhpVersion.php +++ b/src/Ast/PhpVersion.php @@ -84,6 +84,11 @@ public function isNullSafeSupported(): bool return $this->isVersionGreaterThanOrEqualTo80(); } + public function isThrowExpressionSupported(): bool + { + return $this->isVersionGreaterThanOrEqualTo80(); + } + public function isMixedTypehintSupported(): bool { return $this->isVersionGreaterThanOrEqualTo80(); diff --git a/src/Command/GenerateCommand.php b/src/Command/GenerateCommand.php index afb52ab..6765523 100644 --- a/src/Command/GenerateCommand.php +++ b/src/Command/GenerateCommand.php @@ -13,6 +13,7 @@ use DoclerLabs\ApiClientGenerator\Input\Specification; use DoclerLabs\ApiClientGenerator\MetaTemplateFacade; use DoclerLabs\ApiClientGenerator\Output\Copy\Request\AuthenticationCredentials; +use DoclerLabs\ApiClientGenerator\Output\Copy\Request\LiteralRequestBody; use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\FormUrlencodedContentTypeSerializer; use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\JsonContentTypeSerializer; use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\VdnApiJsonContentTypeSerializer; @@ -294,6 +295,10 @@ private function getUnusedValueObjects(Specification $specification): array $unusedClasses[] = AuthenticationCredentials::class; } + if (!$specification->hasLiteralRequestBody()) { + $unusedClasses[] = LiteralRequestBody::class; + } + return $unusedClasses; } diff --git a/src/Entity/Field.php b/src/Entity/Field.php index d2aeb56..9ccc1f6 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -8,6 +8,7 @@ use DoclerLabs\ApiClientGenerator\Entity\Constraint\ConstraintCollection; use DoclerLabs\ApiClientGenerator\Naming\CaseCaster; use DoclerLabs\ApiClientGenerator\Naming\SchemaCollectionNaming; +use DoclerLabs\ApiClientGenerator\Naming\SchemaNaming; use RuntimeException; class Field @@ -30,6 +31,8 @@ class Field private mixed $discriminator = null; + private bool $explicitReferenceName = false; + public function __construct( private PhpVersion $phpVersion, private string $name, @@ -122,6 +125,28 @@ public function getReferenceName(): string return $this->referenceName; } + public function setReferenceName(string $referenceName): self + { + $this->referenceName = $referenceName; + + return $this; + } + + /** + * Whether the reference name is the name of the referenced component, rather than a fallback name. + */ + public function hasExplicitReferenceName(): bool + { + return $this->explicitReferenceName; + } + + public function setExplicitReferenceName(bool $explicitReferenceName): self + { + $this->explicitReferenceName = $explicitReferenceName; + + return $this; + } + public function isRequired(): bool { return $this->required; @@ -188,6 +213,30 @@ public function isArrayOfEnums(): bool && $this->getArrayItem()->isEnum(); } + /** + * An array of arrays (at any depth) of objects, e.g. array>: its innermost arrays are collections. + */ + public function isArrayOfArraysOfObjects(): bool + { + if (!$this->isArray()) { + return false; + } + + $item = $this->getArrayItem(); + + return !$item->isNullable() && ($item->isArrayOfObjects() || $item->isArrayOfArraysOfObjects()); + } + + /** + * The innermost array of objects (the collection) of an array of arrays of objects. + */ + public function getInnermostArrayOfObjects(): Field + { + $item = $this->getArrayItem(); + + return $item->isArrayOfObjects() ? $item : $item->getInnermostArrayOfObjects(); + } + public function getDefault(): mixed { return $this->default; @@ -220,7 +269,7 @@ public function getPhpVariableName(): string public function getPhpClassName(): string { if ($this->type->isObject()) { - return $this->referenceName; + return SchemaNaming::getSchemaClassName($this->referenceName); } if ( diff --git a/src/Entity/RequestFieldRegistry.php b/src/Entity/RequestFieldRegistry.php index e51720b..06dca02 100644 --- a/src/Entity/RequestFieldRegistry.php +++ b/src/Entity/RequestFieldRegistry.php @@ -87,6 +87,19 @@ public function getBody(): ?Field return current($this->items[self::ORIGIN_BODY]); } + /** + * A literal body has a schema that is not an object: a string, number, boolean, or an array of those. + * Untyped (mixed) bodies are passed through as they are. + */ + public function hasLiteralBody(): bool + { + $body = $this->getBody(); + + return $body !== null + && !$body->isComposite() + && $body->getType()->toSpecificationType() !== null; + } + /** * @return RecursiveIteratorIterator|Field[] */ diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 3782889..7bce42c 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -4,6 +4,7 @@ namespace DoclerLabs\ApiClientGenerator\Generator; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use DoclerLabs\ApiClientGenerator\Ast\Builder\CodeBuilder; use DoclerLabs\ApiClientGenerator\Ast\Builder\MethodBuilder; use DoclerLabs\ApiClientGenerator\Ast\Builder\ParameterBuilder; @@ -23,9 +24,11 @@ use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\ContentTypeSerializerInterface; use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; use Psr\Container\ContainerInterface; @@ -35,6 +38,10 @@ class ClientGenerator extends GeneratorAbstract { + private const UNMAPPED_STATUS_CODE_MESSAGE = 'Response status code not properly mapped in schema.'; + + private const MISSING_LITERAL_VALUE_MESSAGE = 'Response body does not contain the expected non-null value.'; + public function __construct( string $baseNamespace, CodeBuilder $builder, @@ -263,13 +270,92 @@ private function processResponse(Variable $unserializedResponseVar, Field $respo return $this->builder->methodCall($getMethod, 'toSchema', [$unserializedResponseVar]); } + $literalValue = $this->getLiteralResponseValue($unserializedResponseVar); + $isEnum = $responseBody->isEnum() && $this->phpVersion->isEnumSupported(); + if ($isEnum) { + $this->addImport( + sprintf( + '%s%s\\%s', + $this->baseNamespace, + EnumGenerator::NAMESPACE_SUBPATH, + $responseBody->getPhpClassName() + ) + ); + } + + // An empty response body carries no literal value: a nullable response maps it to null, any other throws + // (inline where throw expressions are supported, otherwise via generateLiteralResponseGuard()). + if ($responseBody->isNullable()) { + if ($isEnum) { + return $this->builder->ternary( + $this->builder->funcCall('isset', [$literalValue]), + $this->builder->staticCall($responseBody->getPhpClassName(), 'from', [$literalValue]), + $this->builder->val(null) + ); + } + + return $this->builder->coalesce($literalValue, $this->builder->val(null)); + } + + $value = $literalValue; + if ($this->phpVersion->isThrowExpressionSupported()) { + $this->addImport(UnexpectedResponseBodyException::class); + $value = $this->builder->coalesce( + $literalValue, + $this->builder->throwExpression( + 'UnexpectedResponseBodyException', + $this->builder->val(self::MISSING_LITERAL_VALUE_MESSAGE) + ) + ); + } + + if ($isEnum) { + return $this->builder->staticCall($responseBody->getPhpClassName(), 'from', [$value]); + } + + return $value; + } + + /** + * Guards a non-nullable literal response against an empty body on targets without throw expressions. + * + * @return Stmt[] + */ + private function generateLiteralResponseGuard(Variable $unserializedResponseVar, Field $responseBody): array + { + if ( + $responseBody->isComposite() + || $responseBody->isNullable() + || $this->phpVersion->isThrowExpressionSupported() + ) { + return []; + } + + $this->addImport(UnexpectedResponseBodyException::class); + + return [ + $this->builder->if( + $this->builder->not( + $this->builder->funcCall('isset', [$this->getLiteralResponseValue($unserializedResponseVar)]) + ), + [ + $this->builder->throw( + 'UnexpectedResponseBodyException', + $this->builder->val(self::MISSING_LITERAL_VALUE_MESSAGE) + ), + ] + ), + ]; + } + + private function getLiteralResponseValue(Variable $unserializedResponseVar): ArrayDimFetch + { $this->addImport(CopiedNamespace::getImport($this->baseNamespace, ContentTypeSerializerInterface::class)); - $literalValue = $this->builder->getArrayItem( + + return $this->builder->getArrayItem( $unserializedResponseVar, $this->builder->classConstFetch('ContentTypeSerializerInterface', 'LITERAL_VALUE_KEY') ); - - return $literalValue; } private function emptyBodyAction( @@ -298,6 +384,7 @@ private function singleBodyAction( $handleResponseStmt = $this->builder->localMethodCall('handleResponse', $this->builder->args([$sendRequestStmt])); $stmts = [ $this->builder->assign($responseVar, $handleResponseStmt), + ...$this->generateLiteralResponseGuard($responseVar, $responseBody), $this->builder->return($this->processResponse($responseVar, $responseBody)), ]; @@ -344,18 +431,22 @@ private function multiBodyAction( $nullableCases[$response->statusCode] = $this->builder->return($this->builder->val(null)); } } else { - $returnTypeHints[$responseBody->getPhpTypeHint()] = true; - $isNullable = $isNullable || $responseBody->isNullable(); - - $phpClassName = $responseBody->getPhpClassName(); - - $caseConditions[$phpClassName][] = new LNumber($response->statusCode); - if (!isset($caseBodies[$phpClassName])) { + // Responses are grouped by type hint: the class name for composite, date and enum bodies, + // the scalar type for any other literal body. + $typeHint = $responseBody->getPhpTypeHint(); + $returnTypeHints[$typeHint] = true; + $isNullable = $isNullable || $responseBody->isNullable(); + + $caseConditions[$typeHint][] = new LNumber($response->statusCode); + if (!isset($caseBodies[$typeHint])) { $response = $this->processResponse($unserializedResponseVar, $responseBody); if ($this->phpVersion->isMatchSupported()) { - $matchBodies[$phpClassName] = $response; + $matchBodies[$typeHint] = $response; } else { - $caseBodies[$phpClassName] = $this->builder->return($response); + $caseBodies[$typeHint] = [ + ...$this->generateLiteralResponseGuard($unserializedResponseVar, $responseBody), + $this->builder->return($response), + ]; } } } @@ -366,9 +457,17 @@ private function multiBodyAction( foreach ($nullableMatchArms as $statusCode => $nullableMatchArm) { $matchArms[] = $this->builder->matchArm([new LNumber($statusCode)], $nullableMatchArm); } - foreach ($matchBodies as $phpClassName => $matchBody) { - $matchArms[] = $this->builder->matchArm($caseConditions[$phpClassName], $matchBody); + foreach ($matchBodies as $typeHint => $matchBody) { + $matchArms[] = $this->builder->matchArm($caseConditions[$typeHint], $matchBody); } + + $this->addImport(RuntimeException::class); + $matchArms[] = $this->builder->defaultMatchArm( + $this->builder->throwExpression( + 'RuntimeException', + $this->builder->val(self::UNMAPPED_STATUS_CODE_MESSAGE) + ) + ); $stmts[] = $this->builder->return( $this->builder->match( $this->builder->methodCall($responseVar, 'getStatusCode'), @@ -380,11 +479,11 @@ private function multiBodyAction( foreach ($nullableCases as $statusCode => $nullableCase) { $cases[] = $this->builder->case(new LNumber($statusCode), $nullableCase); } - foreach ($caseBodies as $phpClassName => $caseBody) { - for ($i = 0, $l = count($caseConditions[$phpClassName]) - 1; $i < $l; ++$i) { - $cases[] = $this->builder->case($caseConditions[$phpClassName][$i]); + foreach ($caseBodies as $typeHint => $caseBody) { + for ($i = 0, $l = count($caseConditions[$typeHint]) - 1; $i < $l; ++$i) { + $cases[] = $this->builder->case($caseConditions[$typeHint][$i]); } - $cases[] = $this->builder->case($caseConditions[$phpClassName][$l], $caseBody); + $cases[] = $this->builder->case($caseConditions[$typeHint][$l], ...$caseBody); } $stmts[] = $this->builder->switch( $this->builder->methodCall($responseVar, 'getStatusCode'), @@ -394,7 +493,7 @@ private function multiBodyAction( $this->addImport(RuntimeException::class); $stmts[] = $this->builder->throw( 'RuntimeException', - $this->builder->val('Response status code not properly mapped in schema.') + $this->builder->val(self::UNMAPPED_STATUS_CODE_MESSAGE) ); } diff --git a/src/Generator/EnumGenerator.php b/src/Generator/EnumGenerator.php index dcb1a51..90dc65d 100644 --- a/src/Generator/EnumGenerator.php +++ b/src/Generator/EnumGenerator.php @@ -6,6 +6,7 @@ use DoclerLabs\ApiClientGenerator\Entity\Field; use DoclerLabs\ApiClientGenerator\Input\Specification; +use DoclerLabs\ApiClientGenerator\Naming\SchemaNaming; use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection; class EnumGenerator extends MutatorAccessorClassGeneratorAbstract @@ -16,15 +17,58 @@ class EnumGenerator extends MutatorAccessorClassGeneratorAbstract public static function getCaseName(string $value): string { - $sanitized = (string)preg_replace('/[^A-Z0-9_]/', '', strtoupper(str_replace([' ', '-', '/', '.'], '_', $value))); + $sanitized = self::sanitizeCaseName($value); - if (preg_match('/^[0-9]/', $sanitized) === 1) { + if ($sanitized === '') { + // no letters, digits or separators, e.g. `*` or `>=` + return SchemaNaming::getSymbolicEnumValueName($value); + } + + // a case name cannot start with a digit, and `class` is reserved for the class name constant + if (preg_match('/^[0-9]/', $sanitized) === 1 || $sanitized === 'CLASS') { return 'V_' . $sanitized; } return $sanitized; } + /** + * Case names of all values of an enum, with the keys of the values. A symbolic name (a value without letters + * or digits) that coincides with the name of another value gets a numeric suffix, so that one keeps its name. + * + * @param array $values + * + * @return array + */ + public static function getCaseNames(array $values): array + { + $caseNames = []; + foreach ($values as $key => $value) { + $caseNames[$key] = self::getCaseName((string)$value); + } + + foreach ($values as $key => $value) { + if (self::sanitizeCaseName((string)$value) === '') { + $caseNames[$key] = SchemaNaming::getUniqueName( + $caseNames[$key], + array_diff_key($caseNames, [$key => true]) + ); + } + } + + return $caseNames; + } + + /** + * @param array $values + */ + public static function getCaseNameOfValue(array $values, int|string $value): string + { + $key = array_search($value, $values, true); + + return $key === false ? self::getCaseName((string)$value) : self::getCaseNames($values)[$key]; + } + public function generate(Specification $specification, PhpFileCollection $fileRegistry): void { if (!$this->phpVersion->isEnumSupported()) { @@ -54,6 +98,11 @@ public function generate(Specification $specification, PhpFileCollection $fileRe $this->generateEnum($field, $fileRegistry); } } + foreach ($operation->successfulResponses as $response) { + if ($response->body !== null && $response->body->isEnum()) { + $this->generateEnum($response->body, $fileRegistry); + } + } } } @@ -75,18 +124,19 @@ private function generateEnumConsts(Field $root): array } $statements = []; - foreach ($root->getEnumValues() as $value) { - $caseName = self::getCaseName((string)$value); - if (empty($caseName)) { - continue; - } - + $caseNames = self::getCaseNames($root->getEnumValues()); + foreach ($root->getEnumValues() as $key => $value) { $statements[] = $this ->builder - ->enumCase($caseName) + ->enumCase($caseNames[$key]) ->setValue($value); } return $statements; } + + private static function sanitizeCaseName(string $value): string + { + return (string)preg_replace('/[^A-Z0-9_]/', '', strtoupper(str_replace([' ', '-', '/', '.'], '_', $value))); + } } diff --git a/src/Generator/MutatorAccessorClassGeneratorAbstract.php b/src/Generator/MutatorAccessorClassGeneratorAbstract.php index 7547460..301cc69 100644 --- a/src/Generator/MutatorAccessorClassGeneratorAbstract.php +++ b/src/Generator/MutatorAccessorClassGeneratorAbstract.php @@ -19,6 +19,8 @@ abstract class MutatorAccessorClassGeneratorAbstract extends GeneratorAbstract { + private const CONSTANT_NAME_PATTERN = '/^[a-zA-Z_][a-zA-Z0-9_]*$/'; + protected CodeBuilder $builder; abstract public function generate(Specification $specification, PhpFileCollection $fileRegistry): void; @@ -112,22 +114,37 @@ protected function generateEnumStatements(Field $field): array return []; } - $statements = []; $enumValues = $field->isArrayOfEnums() ? $field->getArrayItem()->getEnumValues() : $field->getEnumValues(); - if (!empty($enumValues)) { - foreach ($enumValues as $enumValue) { - if (is_string($enumValue)) { - $constName = SchemaNaming::getEnumConstName($field, $enumValue); - if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $constName)) { - $statements[] = $this->builder->constant( - $constName, - $this->builder->val($enumValue) - ); - } + $enumValues = array_filter($enumValues ?? [], 'is_string'); + + $constNames = []; + foreach ($enumValues as $key => $enumValue) { + $constName = SchemaNaming::getEnumConstName($field, $enumValue); + if (preg_match(self::CONSTANT_NAME_PATTERN, $constName)) { + $constNames[$key] = $constName; + } + } + + // a value that yields no valid name from its letters and digits (e.g. `*`) gets a symbolic one + foreach ($enumValues as $key => $enumValue) { + if (!isset($constNames[$key])) { + $constName = SchemaNaming::getSymbolicEnumConstName($field, $enumValue); + if (preg_match(self::CONSTANT_NAME_PATTERN, $constName)) { + $constNames[$key] = SchemaNaming::getUniqueName($constName, $constNames); } } } + $statements = []; + foreach ($enumValues as $key => $enumValue) { + if (isset($constNames[$key])) { + $statements[] = $this->builder->constant( + $constNames[$key], + $this->builder->val($enumValue) + ); + } + } + return $statements; } diff --git a/src/Generator/RequestGenerator.php b/src/Generator/RequestGenerator.php index c51f203..7e76ed2 100644 --- a/src/Generator/RequestGenerator.php +++ b/src/Generator/RequestGenerator.php @@ -13,12 +13,12 @@ use DoclerLabs\ApiClientGenerator\Entity\Operation; use DoclerLabs\ApiClientGenerator\Entity\Request; use DoclerLabs\ApiClientGenerator\Generator\Security\SecurityStrategyInterface; -use DoclerLabs\ApiClientGenerator\Input\InvalidSpecificationException; use DoclerLabs\ApiClientGenerator\Input\Specification; use DoclerLabs\ApiClientGenerator\Naming\CopiedNamespace; use DoclerLabs\ApiClientGenerator\Naming\RequestNaming; use DoclerLabs\ApiClientGenerator\Output\Copy\Schema\SerializableInterface; use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Stmt\ClassMethod; @@ -171,7 +171,7 @@ protected function generateConstructor( )) { $param->setDefault($this->builder->classConstFetch( $field->getPhpClassName(), - EnumGenerator::getCaseName((string)$default) + EnumGenerator::getCaseNameOfValue($field->getEnumValues() ?? [], $default) )); } else { $param->setDefault($field->getDefault()); @@ -253,9 +253,8 @@ private function generateSetters(Request $request): array if ($field->isRequired()) { continue; } - if ($field->isNullable()) { - throw new InvalidSpecificationException('Nullable optional parameter is not supported'); - } + // A nullable optional parameter accepts an explicit null. It is treated like an unset parameter: + // null values are left out of the query, header and cookie parameters, as they are for required ones. $statements[] = $this->generateSet($field); } @@ -362,7 +361,7 @@ private function generateGetParametersMethods( $methods[] = $this->generateGetRawParametersMethod('getRawQueryParameters', $fields->getQueryFields(), $securityQueryFields); $methods[] = $this->generateGetParametersMethod('getCookies', $fields->getCookieFields(), $securityCookies); $methods[] = $this->generateGetHeadersMethod($request, $fields->getHeaderFields(), $operation, $specification); - $methods[] = $this->generateGetBody($fields->getBody()); + $methods[] = $this->generateGetBody($fields->getBody(), $fields->hasLiteralBody()); return $methods; } @@ -459,8 +458,25 @@ private function generateFieldsArray(array $fields): array return $fieldsArr; } - private function generateGetBody(?Field $body): ClassMethod + private function generateGetBody(?Field $body, bool $isLiteral): ClassMethod { + if ($body !== null && $isLiteral) { + // The body serializers can only encode SerializableInterface, so a literal body gets wrapped. + $returnType = 'LiteralRequestBody'; + $return = $this->builder->new( + $returnType, + [$this->builder->localPropertyFetch($body->getPhpVariableName())] + ); + + return $this + ->builder + ->method('getBody') + ->makePublic() + ->addStmt($this->builder->return($return)) + ->composeDocBlock([], $returnType) + ->getNode(); + } + if ($body !== null) { $returnType = $body->getPhpTypeHint(); @@ -503,6 +519,14 @@ private function generateGetHeadersMethod( $fieldsArr = $this->generateFieldsArray($fields); $returnType = 'array'; + foreach ($fields as $field) { + /** @var Field $field */ + $headerValue = $this->generateHeaderStringValue($field); + if ($headerValue !== null) { + $fieldsArr[$field->getName()] = $headerValue; + } + } + if (!empty($fieldsArr)) { $returnVal = $this->builder->funcCall( 'array_merge', @@ -521,6 +545,43 @@ private function generateGetHeadersMethod( ->getNode(); } + /** + * PSR-7 header values are strings (guzzlehttp/psr7 >= 2.11 deprecates anything else, nyholm/psr7 rejects booleans), + * so integer, number and boolean header parameters (including integer backed enums) are cast to string. + * Booleans are sent as '1' and '0', the same way the query and cookie parameters serialize them. + * A null value (unset optional or nullable parameter) stays null, so it is still filtered out. + * Returns null when the header value needs no conversion. + */ + private function generateHeaderStringValue(Field $field): ?Expr + { + $type = $field->getType(); + if (!$type->isInteger() && !$type->isFloat() && !$type->isBoolean()) { + return null; + } + + $property = $this->builder->localPropertyFetch($field->getPhpVariableName()); + $value = $property; + if ($field->isEnum() && $this->phpVersion->isEnumSupported()) { + $value = $this->builder->propertyFetch($property, 'value'); + } + + if ($type->isBoolean()) { + $stringValue = $this->builder->ternary($value, $this->builder->val('1'), $this->builder->val('0')); + } else { + $stringValue = $this->builder->castToString($value); + } + + if ($field->isNullable() || $field->isOptional()) { + $stringValue = $this->builder->ternary( + $this->builder->equals($property, $this->builder->val(null)), + $this->builder->val(null), + $stringValue + ); + } + + return $stringValue; + } + private function getSecurityHeadersStmts(Operation $operation, Specification $specification): array { $stmts = []; diff --git a/src/Generator/SchemaCollectionGenerator.php b/src/Generator/SchemaCollectionGenerator.php index ceee4e5..bde3219 100644 --- a/src/Generator/SchemaCollectionGenerator.php +++ b/src/Generator/SchemaCollectionGenerator.php @@ -174,7 +174,7 @@ protected function generateFirst(Field $arrayItem): ClassMethod ->addStmt($if) ->addStmt($return) ->setReturnType($arrayItem->getPhpTypeHint(), true) - ->composeDocBlock([], $arrayItem->getReferenceName() . '|null') + ->composeDocBlock([], $arrayItem->getPhpClassName() . '|null') ->getNode(); } } diff --git a/src/Generator/SchemaGenerator.php b/src/Generator/SchemaGenerator.php index 9dde972..587a1c5 100644 --- a/src/Generator/SchemaGenerator.php +++ b/src/Generator/SchemaGenerator.php @@ -12,6 +12,8 @@ use DoclerLabs\ApiClientGenerator\Input\Specification; use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection; use JsonSerializable; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; @@ -344,6 +346,16 @@ private function collectSerializationFields(Field $root, Variable $arrayVariable $this->builder->val(null) ) : $arrayMapCall; + } elseif ($propertyField->isArrayOfArraysOfObjects()) { + $collectionsToArray = $this->generateArrayOfArraysOfObjectsToArray($propertyField, $value); + + $value = $propertyField->isNullable() + ? $this->builder->ternary( + $this->builder->notEquals($value, $this->builder->val(null)), + $collectionsToArray, + $this->builder->val(null) + ) + : $collectionsToArray; } $fieldName = $this->builder->val($propertyField->getName()); @@ -366,4 +378,33 @@ private function collectSerializationFields(Field $root, Variable $arrayVariable return $statements; } + + /** + * Serializes every collection of an array of arrays of objects. + */ + private function generateArrayOfArraysOfObjectsToArray(Field $field, Expr $value): FuncCall + { + $item = $field->getArrayItem(); + $itemVar = $this->builder->var('item'); + if ($item->isArrayOfObjects()) { + $itemToArray = $this->builder->methodCall($itemVar, 'toArray'); + $itemType = $item->getPhpClassName(); + } else { + $itemToArray = $this->generateArrayOfArraysOfObjectsToArray($item, $itemVar); + $itemType = FieldType::PHP_TYPE_ARRAY; + } + + return $this->builder->funcCall( + 'array_map', + [ + $this->builder->closure( + [$this->builder->return($itemToArray)], + [$this->builder->param('item')->setType($itemType)->getNode()], + [], + FieldType::PHP_TYPE_ARRAY + ), + $value, + ] + ); + } } diff --git a/src/Generator/SchemaMapperGenerator.php b/src/Generator/SchemaMapperGenerator.php index 916b557..064d8c9 100644 --- a/src/Generator/SchemaMapperGenerator.php +++ b/src/Generator/SchemaMapperGenerator.php @@ -9,14 +9,17 @@ use DoclerLabs\ApiClientGenerator\Ast\Builder\ParameterBuilder; use DoclerLabs\ApiClientGenerator\Ast\ParameterNode; use DoclerLabs\ApiClientGenerator\Entity\Field; +use DoclerLabs\ApiClientGenerator\Entity\FieldType; use DoclerLabs\ApiClientGenerator\Input\Specification; use DoclerLabs\ApiClientGenerator\Naming\SchemaMapperNaming; use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Case_; use PhpParser\Node\Stmt\ClassMethod; +use ValueError; class SchemaMapperGenerator extends MutatorAccessorClassGeneratorAbstract { @@ -30,6 +33,11 @@ public function generate(Specification $specification, PhpFileCollection $fileRe { foreach ($specification->getCompositeResponseFields() as $field) { /** @var Field $field */ + if ($field->isEnum()) { + // array-of-enum items are mapped by the parent with from(), they need no mapper + continue; + } + $this->generateMapper($fileRegistry, $field); } } @@ -59,6 +67,9 @@ protected function generateProperties(Field $root): array if ($root->isObject()) { $alreadyInjected = []; foreach ($root->getObjectProperties() as $child) { + if ($child->isArrayOfArraysOfObjects()) { + $child = $child->getInnermostArrayOfObjects(); + } if ($child->isComposite()) { $childClassName = SchemaMapperNaming::getClassName($child); if (!isset($alreadyInjected[$childClassName])) { @@ -96,6 +107,9 @@ protected function generateConstructor(Field $root): ?ClassMethod if ($root->isObject()) { $alreadyInjected = []; foreach ($root->getObjectProperties() as $child) { + if ($child->isArrayOfArraysOfObjects()) { + $child = $child->getInnermostArrayOfObjects(); + } if ($child->isComposite()) { $childClassName = SchemaMapperNaming::getClassName($child); if (!isset($alreadyInjected[$childClassName])) { @@ -387,6 +401,15 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload $this->builder->val(null) ) : $arrayMapCall; + } elseif ($field->isArrayOfArraysOfObjects()) { + $collectionsMapping = $this->generateArrayOfArraysOfObjectsMapping($field, $requiredResponseItems[$i]); + $requiredVars[] = $field->isNullable() + ? $this->builder->ternary( + $this->builder->notEquals($requiredResponseItems[$i], $this->builder->val(null)), + $collectionsMapping, + $this->builder->val(null) + ) + : $collectionsMapping; } else { $requiredVars[] = $requiredResponseItems[$i]; } @@ -433,7 +456,14 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload } } elseif ($field->isEnum() && $this->phpVersion->isEnumSupported()) { $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $field->getPhpClassName())); - $optionalVar = $this->builder->staticCall($field->getPhpClassName(), 'from', [$optionalResponseItems[$i]]); + $newEnum = $this->builder->staticCall($field->getPhpClassName(), 'from', [$optionalResponseItems[$i]]); + $optionalVar = $field->isNullable() + ? $this->builder->ternary( + $this->builder->notEquals($optionalResponseItems[$i], $this->builder->val(null)), + $newEnum, + $this->builder->val(null) + ) + : $newEnum; } elseif ($field->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) { $enumField = $field->getArrayItem(); $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $enumField->getPhpClassName())); @@ -457,6 +487,15 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload $this->builder->val(null) ) : $arrayMapCall; + } elseif ($field->isArrayOfArraysOfObjects()) { + $collectionsMapping = $this->generateArrayOfArraysOfObjectsMapping($field, $optionalResponseItems[$i]); + $optionalVar = $field->isNullable() + ? $this->builder->ternary( + $this->builder->notEquals($optionalResponseItems[$i], $this->builder->val(null)), + $collectionsMapping, + $this->builder->val(null) + ) + : $collectionsMapping; } else { $optionalVar = $optionalResponseItems[$i]; } @@ -478,8 +517,14 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload )); $this->addImport(UnexpectedResponseBodyException::class); + $caughtExceptions = [$this->builder->className('UnexpectedResponseBodyException')]; + if ($this->phpVersion->isEnumSupported() && $this->isMappedWithEnums($field)) { + // ::from() throws a ValueError for a value of another alternative + $this->addImport(ValueError::class); + $caughtExceptions[] = $this->builder->className('ValueError'); + } $catchStatement = $this->builder->catch( - [$this->builder->className('UnexpectedResponseBodyException')], + $caughtExceptions, $this->builder->var('exception'), [] ); @@ -554,6 +599,41 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload return $statements; } + /** + * Maps every innermost array of an array of arrays of objects to its collection. + */ + private function generateArrayOfArraysOfObjectsMapping(Field $field, Expr $payloadItem): FuncCall + { + $item = $field->getArrayItem(); + $itemVar = $this->builder->var('item'); + if ($item->isArrayOfObjects()) { + $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $item->getPhpClassName())); + $itemMapping = $this->builder->methodCall( + $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($item)), + 'toSchema', + [$itemVar] + ); + $itemType = $item->getPhpClassName(); + } else { + $itemMapping = $this->generateArrayOfArraysOfObjectsMapping($item, $itemVar); + $itemType = FieldType::PHP_TYPE_ARRAY; + } + + return $this->builder->funcCall( + 'array_map', + [ + $this->builder->closure( + [$this->builder->return($itemMapping)], + [$this->builder->param('item')->setType(FieldType::PHP_TYPE_ARRAY)->getNode()], + [], + $itemType, + false + ), + $payloadItem, + ] + ); + } + private function generateDiscriminatorStatement(Field $root, Variable $payloadVariable): Stmt { $discriminator = $root->getDiscriminator(); @@ -571,105 +651,114 @@ private function generateDiscriminatorStatement(Field $root, Variable $payloadVa $this->builder->val($propertyName) ); - $fallbackStatements = $this->generateDiscriminatorFallbackStatements($payloadDiscriminator, $payloadVariable); - /** @phpstan-ignore-next-line */ $mapping = $discriminator->mapping ?? []; - $cases = $this->generateDiscriminatorMappingCases($root, $mapping, $payloadVariable); - - if ($cases === []) { - return $this->builder->if($ifCondition, $fallbackStatements); - } - - $defaultStatements = [...$fallbackStatements, $this->builder->break()]; - $cases[] = $this->builder->default(...$defaultStatements); - - return $this->builder->if($ifCondition, [$this->builder->switch($payloadDiscriminator, ...$cases)]); - } - - /** - * @return Stmt[] - */ - private function generateDiscriminatorFallbackStatements(Expr $payloadDiscriminator, Variable $payloadVariable): array - { - $assignMethodName = $this->builder->expr( - $this->builder->assign( - $this->builder->var('methodName'), - $this->builder->concat( - $this->builder->val('set'), - $this->builder->funcCall('ucfirst', [$payloadDiscriminator]) - ) - ) - ); - - $assignMapperName = $this->builder->expr( - $this->builder->assign( - $this->builder->var('mapperName'), - $this->builder->concat( - $payloadDiscriminator, - $this->builder->val('Mapper') + $cases = $this->generateDiscriminatorCases($root, $mapping, $payloadVariable); + + $this->addImport(UnexpectedResponseBodyException::class); + $cases[] = $this->builder->default( + $this->builder->throw( + 'UnexpectedResponseBodyException', + $this->builder->val( + sprintf( + 'Unknown `%s` discriminator value for `%s` in the response body', + $propertyName, + $root->getPhpClassName() + ) ) ) ); + $this->mapMethodThrownExceptions['UnexpectedResponseBodyException'] = true; - $schemaMethodCall = $this->builder->expr( - $this->builder->methodCall( - $this->builder->var('schema'), - '$methodName', - [ - $this->builder->methodCall( - $this->builder->localPropertyFetch('$mapperName'), - 'toSchema', - [$payloadVariable] - ), - ] - ) - ); - - return [$assignMethodName, $assignMapperName, $schemaMethodCall]; + return $this->builder->if($ifCondition, [$this->builder->switch($payloadDiscriminator, ...$cases)]); } /** + * One case per discriminator value: the explicit mapping first, then the implicit mapping (the schema name) + * of every alternative the explicit mapping does not cover. The latter also accepts the lcfirst class name, + * which the previous value based dispatch (`$this->{$value . 'Mapper'}`) resolved. + * * @param string[] $mapping * * @return Case_[] */ - private function generateDiscriminatorMappingCases(Field $root, array $mapping, Variable $payloadVariable): array + private function generateDiscriminatorCases(Field $root, array $mapping, Variable $payloadVariable): array { - $childrenByClassName = []; + $childrenBySchemaName = []; foreach ($root->getObjectProperties() as $child) { if ($child->isComposite()) { - $childrenByClassName[$child->getPhpClassName()] = $child; + $childrenBySchemaName[$child->getName()] = $child; } } - $cases = []; + $cases = []; + $usedValues = []; + $explicitlyMapped = []; foreach ($mapping as $discriminatorValue => $reference) { $schemaName = $this->resolveSchemaNameFromReference($reference); - if (!isset($childrenByClassName[$schemaName])) { + if (!isset($childrenBySchemaName[$schemaName])) { continue; } - $child = $childrenByClassName[$schemaName]; - $cases[] = $this->builder->case( - $this->builder->val((string)$discriminatorValue), - $this->builder->expr( - $this->builder->methodCall( - $this->builder->var('schema'), - $this->getSetMethodName($child), - [ - $this->builder->methodCall( - $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($child)), - 'toSchema', - [$payloadVariable] - ), - ] - ) - ), - $this->builder->break() + $cases[] = $this->generateDiscriminatorCase( + [(string)$discriminatorValue], + $childrenBySchemaName[$schemaName], + $payloadVariable ); + $usedValues[] = (string)$discriminatorValue; + $explicitlyMapped[$schemaName] = true; + } + + foreach ($childrenBySchemaName as $schemaName => $child) { + if (isset($explicitlyMapped[$schemaName])) { + continue; + } + + $implicitValues = array_values( + array_diff(array_unique([(string)$schemaName, lcfirst($child->getPhpClassName())]), $usedValues) + ); + if ($implicitValues === []) { + continue; + } + + $cases[] = $this->generateDiscriminatorCase($implicitValues, $child, $payloadVariable); + $usedValues = [...$usedValues, ...$implicitValues]; + } + + return array_merge([], ...$cases); + } + + /** + * @param string[] $discriminatorValues + * + * @return Case_[] + */ + private function generateDiscriminatorCase(array $discriminatorValues, Field $child, Variable $payloadVariable): array + { + $cases = []; + $lastValue = array_pop($discriminatorValues); + foreach ($discriminatorValues as $discriminatorValue) { + $cases[] = $this->builder->case($this->builder->val($discriminatorValue)); } + $cases[] = $this->builder->case( + $this->builder->val($lastValue), + $this->builder->expr( + $this->builder->methodCall( + $this->builder->var('schema'), + $this->getSetMethodName($child), + [ + $this->builder->methodCall( + $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($child)), + 'toSchema', + [$payloadVariable] + ), + ] + ) + ), + $this->builder->break() + ); + return $cases; } @@ -679,4 +768,31 @@ private function resolveSchemaNameFromReference(string $reference): string return (string)end($segments); } + + /** + * Whether mapping the field (or anything nested in it) can throw the ValueError of an enum. + */ + private function isMappedWithEnums(Field $field): bool + { + if ($field->isEnum()) { + return true; + } + + if ($field->isArray()) { + return $this->isMappedWithEnums($field->getArrayItem()); + } + + if (($field->hasOneOf() || $field->hasAnyOf()) && !$field->getDiscriminator()) { + // its own mapper catches the ValueError of its alternatives + return false; + } + + foreach ($field->getObjectProperties() as $property) { + if ($this->isMappedWithEnums($property)) { + return true; + } + } + + return false; + } } diff --git a/src/Generator/ServiceProviderGenerator.php b/src/Generator/ServiceProviderGenerator.php index 88ba55a..6b92a22 100644 --- a/src/Generator/ServiceProviderGenerator.php +++ b/src/Generator/ServiceProviderGenerator.php @@ -62,7 +62,11 @@ public function generate(Specification $specification, PhpFileCollection $fileRe ) ); - $compositeFields = $specification->getCompositeResponseFields()->getUniqueByPhpClassName(); + $compositeFields = array_filter( + $specification->getCompositeResponseFields()->getUniqueByPhpClassName(), + // array-of-enum items have no mapper to register + static fn (Field $field): bool => !$field->isEnum() + ); $classBuilder = $this ->builder @@ -277,6 +281,9 @@ private function buildMapperDependencies(Field $field, Variable $containerVariab if ($field->isObject()) { $alreadyInjected = []; foreach ($field->getObjectProperties() as $subfield) { + if ($subfield->isArrayOfArraysOfObjects()) { + $subfield = $subfield->getInnermostArrayOfObjects(); + } if ($subfield->isComposite() && !isset($alreadyInjected[$subfield->getPhpClassName()])) { $getMethodArg = $this->builder->classConstFetch( SchemaMapperNaming::getClassName($subfield), diff --git a/src/Input/ClassNameCollisionResolver.php b/src/Input/ClassNameCollisionResolver.php new file mode 100644 index 0000000..c12a0f7 --- /dev/null +++ b/src/Input/ClassNameCollisionResolver.php @@ -0,0 +1,269 @@ +getOperations() as $operation) { + foreach ($operation->request->fields as $field) { + $this->collectNamedFields($field, $operation->name, $fieldsByClassName, $operationsByField); + } + foreach ($operation->successfulResponses as $response) { + if ($response->body !== null) { + $this->collectNamedFields($response->body, $operation->name, $fieldsByClassName, $operationsByField); + } + } + } + + $takenClassNames = $this->getTakenClassNames($fieldsByClassName); + $emittedFields = $this->getEmittedFields($specification); + foreach ($fieldsByClassName as $className => $fields) { + if (count($fields) < 2) { + continue; + } + + $definitions = []; + foreach ($fields as $field) { + $definitions[$this->getSignature($field)][] = $field; + } + + if (count($definitions) < 2) { + continue; + } + + $keptSignature = $this->getKeptSignature($definitions, $emittedFields[$className] ?? null); + foreach ($definitions as $signature => $definitionFields) { + if ($signature === $keptSignature) { + continue; + } + + $newClassName = $this->getUniqueClassName((string)$className, $takenClassNames); + $takenClassNames[] = $newClassName; + $operationNames = []; + foreach ($definitionFields as $field) { + $field->setReferenceName($newClassName); + $operationNames[] = $operationsByField[spl_object_id($field)]; + } + + $warningMessage = sprintf( + 'Class name %s is shared by different schemas, %s is used for the one in %s. Consider giving the schemas distinct names.', + $className, + $newClassName, + implode(', ', array_unique($operationNames)) + ); + trigger_error($warningMessage, E_USER_WARNING); + } + } + } + + /** + * @param array $fieldsByClassName + * @param array $operationsByField + */ + private function collectNamedFields( + Field $field, + string $operationName, + array &$fieldsByClassName, + array &$operationsByField + ): void { + if ($field->isObject() || ($field->isEnum() && $this->phpVersion->isEnumSupported())) { + $fieldsByClassName[$field->getPhpClassName()][] = $field; + $operationsByField[spl_object_id($field)] = $operationName; + } + + if ($field->isArray()) { + $this->collectNamedFields($field->getArrayItem(), $operationName, $fieldsByClassName, $operationsByField); + } + + foreach ($field->getObjectProperties() as $property) { + $this->collectNamedFields($property, $operationName, $fieldsByClassName, $operationsByField); + } + } + + /** + * The fields whose definition the generators emit for a class name when names collide, see the generators. + * + * @return array + */ + private function getEmittedFields(Specification $specification): array + { + $emittedFields = []; + $compositeFields = $specification->getCompositeFields()->getUniqueByPhpClassName(); + if ($this->phpVersion->isEnumSupported()) { + // EnumGenerator + foreach ($compositeFields as $field) { + if ($field->isObject() && !$field->isFreeFormObject()) { + foreach ($field->getObjectProperties() as $propertyField) { + if ($propertyField->isEnum()) { + $emittedFields[$propertyField->getPhpClassName()] = $propertyField; + } + } + } + if ($field->isEnum()) { + $emittedFields[$field->getPhpClassName()] = $field; + } + } + foreach ($specification->getOperations() as $operation) { + foreach ($operation->request->fields as $field) { + if ($field->isEnum()) { + $emittedFields[$field->getPhpClassName()] = $field; + } + } + } + } + + // SchemaGenerator and FreeFormSchemaGenerator, registered after the enums + foreach ($compositeFields as $className => $field) { + if ($field->isObject()) { + $emittedFields[$className] = $field; + } + } + + return $emittedFields; + } + + /** + * @param array $definitions + */ + private function getKeptSignature(array $definitions, ?Field $emittedField): string + { + $explicitSignatures = []; + foreach ($definitions as $signature => $fields) { + foreach ($fields as $field) { + if ($field->hasExplicitReferenceName()) { + $explicitSignatures[] = (string)$signature; + + break; + } + } + } + + $candidates = empty($explicitSignatures) ? array_map('strval', array_keys($definitions)) : $explicitSignatures; + $emittedSignature = $emittedField === null ? null : $this->getSignature($emittedField); + if ($emittedSignature !== null && in_array($emittedSignature, $candidates, true)) { + return $emittedSignature; + } + + return (string)end($candidates); + } + + /** + * @param string[] $takenClassNames + */ + private function getUniqueClassName(string $className, array $takenClassNames): string + { + $suffix = str_ends_with($className, self::ENUM_SUFFIX) ? self::ENUM_SUFFIX : ''; + $base = substr($className, 0, strlen($className) - strlen($suffix)); + + $number = 2; + while (in_array($base . $number . $suffix, $takenClassNames, true)) { + ++$number; + } + + return $base . $number . $suffix; + } + + /** + * @param array $fieldsByClassName + * + * @return string[] + */ + private function getTakenClassNames(array $fieldsByClassName): array + { + $takenClassNames = []; + foreach ($fieldsByClassName as $className => $fields) { + $takenClassNames[] = (string)$className; + foreach ($fields as $field) { + $takenClassNames[] = SchemaCollectionNaming::getClassName($field->getReferenceName()); + } + } + + return array_values(array_unique($takenClassNames)); + } + + /** + * Identifies the definition of the class that a field is generated to: fields with the same signature are + * generated to the same code (apart from the names of the classes they refer to). + */ + private function getSignature(Field $field): string + { + return serialize($this->describeDefinition($field)); + } + + private function describeDefinition(Field $field): array + { + if ($field->isEnum()) { + $values = $field->getEnumValues() ?? []; + sort($values); + + return ['enum', $field->getType()->toSpecificationType(), $values]; + } + + if ($field->isObject()) { + $discriminator = $field->getDiscriminator(); + + return [ + 'object', + $field->isFreeFormObject(), + $field->hasOneOf(), + $field->hasAnyOf(), + $discriminator instanceof SpecObjectInterface ? $discriminator->getSerializableData() : $discriminator, + array_map(fn (Field $property): array => $this->describeProperty($property), $field->getObjectProperties()), + ]; + } + + if ($field->isArray()) { + // the name of an inline item depends on the operation, e.g. `{Operation}{Field}Item` + return ['array', $this->describeProperty($field->getArrayItem(), false)]; + } + + return [$field->getType()->toSpecificationType(), $field->getFormat()]; + } + + private function describeProperty(Field $field, bool $withName = true): array + { + $constraints = []; + foreach ($field->getConstraints() as $constraint) { + /** @var ConstraintInterface $constraint */ + if ($constraint->exists()) { + $constraints[] = [get_class($constraint), $constraint->getExceptionMessage()]; + } + } + + return [ + $withName ? $field->getName() : null, + $field->isRequired(), + $field->isNullable(), + $field->getDefault(), + $constraints, + $this->describeDefinition($field), + ]; + } +} diff --git a/src/Input/Factory/FieldFactory.php b/src/Input/Factory/FieldFactory.php index 370d7dd..36fc79e 100644 --- a/src/Input/Factory/FieldFactory.php +++ b/src/Input/Factory/FieldFactory.php @@ -27,6 +27,9 @@ class FieldFactory { + /** @var string[] reserved words already reported as schema names */ + private array $reservedSchemaNames = []; + public function __construct(private PhpNameValidator $nameValidator, private PhpVersion $phpVersion) { } @@ -122,7 +125,7 @@ public function create( $arrayItem = $this->create( $operationName, lcfirst($itemReferenceName), - $sibling, + $itemsReference, true, $itemReferenceName ); @@ -155,6 +158,20 @@ public function create( } } + if ( + FieldType::isSpecificationTypeObject($type) + && SchemaNaming::isReservedClassName($referenceName) + && !in_array($referenceName, $this->reservedSchemaNames, true) + ) { + $this->reservedSchemaNames[] = $referenceName; + $warningMessage = sprintf( + 'Schema name %s is a reserved word in PHP, %s is used as class name instead.', + $referenceName, + SchemaNaming::getSchemaClassName($referenceName) + ); + trigger_error($warningMessage, E_USER_WARNING); + } + $fieldType = new FieldType($type, $this->phpVersion); $field = new Field( $this->phpVersion, @@ -177,6 +194,8 @@ public function create( !empty($anyOf) ); + $field->setExplicitReferenceName($schemaOrReference instanceof Reference); + if ($arrayItem !== null) { $field->setArrayItem($arrayItem); } elseif (!empty($objectProperties)) { @@ -283,6 +302,9 @@ private function mergeAllOfProperties(string $operationName, SpecObjectInterface private function mergeAllOfAttributes(SpecObjectInterface $schema, bool &$nullable): SpecObjectInterface { + // `nullable: true` next to `allOf` (the OAS 3.0 nullable reference idiom) must survive the merge: + // the referenced schemas carry `nullable: false` by default, which would otherwise override it. + $explicitlyNullable = $nullable; foreach ($schema->allOf as $allOfSchema) { if ($allOfSchema instanceof Reference) { $allOfSchema = $allOfSchema->resolve(); @@ -291,7 +313,7 @@ private function mergeAllOfAttributes(SpecObjectInterface $schema, bool &$nullab if (in_array('null', (array)$allOfSchema->type, true)) { // 3.1 schema nullable does not exist anymore, rather it's done via type $nullable = true; - } elseif (isset($allOfSchema->nullable)) { + } elseif (!$explicitlyNullable && isset($allOfSchema->nullable)) { $nullable = $allOfSchema->nullable; } diff --git a/src/Input/Factory/RequestFactory.php b/src/Input/Factory/RequestFactory.php index 365bbd5..11576fd 100644 --- a/src/Input/Factory/RequestFactory.php +++ b/src/Input/Factory/RequestFactory.php @@ -6,7 +6,6 @@ use cebe\openapi\spec\Reference; use cebe\openapi\spec\RequestBody; -use cebe\openapi\SpecObjectInterface; use DoclerLabs\ApiClientGenerator\Entity\Request; use DoclerLabs\ApiClientGenerator\Entity\RequestFieldRegistry; use DoclerLabs\ApiClientGenerator\Input\InvalidSpecificationException; @@ -27,6 +26,7 @@ public function create( RequestBody $body = null ): Request { $contentTypes = []; + $schema = null; $collection = new RequestFieldRegistry(); foreach ($parameters as $parameter) { $referenceName = ''; @@ -47,17 +47,22 @@ public function create( } if ($body !== null) { - $schema = null; foreach ($body->content as $contentType => $content) { + $contentTypes[] = $contentType; + + // A media type without schema does not describe its content, so it has no body field of its own. + // The media type is still accepted, it can be sent with the body of the other media types or empty. + if ($content->schema === null) { + continue; + } if ($schema !== null && !Parity::isEqualTo($content->schema, $schema)) { throw new InvalidSpecificationException('Multiple schemas per request is not currently supported.'); } - $schema = $content->schema; - $contentTypes[] = $contentType; + $schema = $content->schema; } + } - /** @var SpecObjectInterface $schema */ - + if ($schema !== null) { $schemaName = SchemaNaming::getClassName($schema, ucfirst($operationName) . 'RequestBody'); $collection->add( diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 8adcd99..4a6fa25 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -16,7 +16,8 @@ class Parser { public function __construct( private OperationCollectionFactory $operationCollectionFactory, - private PhpVersion $phpVersion + private PhpVersion $phpVersion, + private ClassNameCollisionResolver $classNameCollisionResolver ) { } @@ -34,12 +35,16 @@ public function parse(array $data, string $contextUri): Specification $compositeRequestFields = $this->extractCompositeRequestFields($operations); $compositeResponseFields = $this->extractCompositeResponseFields($operations); - return new Specification( + $specification = new Specification( $openApi, $operations, $compositeRequestFields, $compositeResponseFields, ); + + $this->classNameCollisionResolver->resolve($specification); + + return $specification; } private function extractCompositeRequestFields(OperationCollection $operations): FieldCollection @@ -86,6 +91,10 @@ private function extractField(Field $field, FieldCollection $allFields): void if ($field->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) { $allFields->add($field->getArrayItem()); } + + if ($field->isArrayOfArraysOfObjects()) { + $this->extractField($field->getArrayItem(), $allFields); + } } private function extractPropertyFields(Field $rootObject, FieldCollection $allFields): void diff --git a/src/Input/Specification.php b/src/Input/Specification.php index 505b143..cb0ea9f 100644 --- a/src/Input/Specification.php +++ b/src/Input/Specification.php @@ -121,6 +121,18 @@ public function getAllContentTypes(): array return array_keys(array_filter($allContentTypes)); } + public function hasLiteralRequestBody(): bool + { + /** @var Operation $operation */ + foreach ($this->getOperations() as $operation) { + if ($operation->request->fields->hasLiteralBody()) { + return true; + } + } + + return false; + } + public function requiresIntlExtension(): bool { foreach ($this->getOperations() as $operation) { diff --git a/src/Naming/SchemaCollectionNaming.php b/src/Naming/SchemaCollectionNaming.php index 0279d97..d51426e 100644 --- a/src/Naming/SchemaCollectionNaming.php +++ b/src/Naming/SchemaCollectionNaming.php @@ -17,6 +17,6 @@ public static function getClassName(string $schemaName): string public static function getArrayDocType(Field $arrayItem): string { - return sprintf('%s[]', $arrayItem->getReferenceName()); + return sprintf('%s[]', $arrayItem->getPhpClassName()); } } diff --git a/src/Naming/SchemaNaming.php b/src/Naming/SchemaNaming.php index 8e2288b..3cf0c04 100644 --- a/src/Naming/SchemaNaming.php +++ b/src/Naming/SchemaNaming.php @@ -13,6 +13,61 @@ class SchemaNaming { private const OPENAPI_COMPONENT_TYPES = ['schemas', 'parameters']; + private const RESERVED_CLASS_NAME_SUFFIX = 'Schema'; + + /** + * Words that cannot be used as a class name (keywords and reserved type names) in at least one PHP version. + * The soft reserved words (enum, resource, numeric) are still valid class names, so they are not listed. + */ + private const RESERVED_CLASS_NAMES = [ + 'abstract', 'and', 'array', 'as', 'bool', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', + 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', + 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'false', 'final', 'finally', + 'float', 'fn', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', + 'instanceof', 'insteadof', 'int', 'interface', 'isset', 'iterable', 'list', 'match', 'mixed', 'namespace', + 'never', 'new', 'null', 'object', 'or', 'parent', 'print', 'private', 'protected', 'public', 'readonly', + 'require', 'require_once', 'return', 'self', 'static', 'string', 'switch', 'throw', 'trait', 'true', 'try', + 'unset', 'use', 'var', 'void', 'while', 'xor', 'yield', + ]; + + private const EMPTY_ENUM_VALUE_NAME = 'EMPTY'; + + private const SYMBOL_NAMES = [ + ' ' => 'SPACE', + '!' => 'EXCLAMATION', + '"' => 'QUOTE', + '#' => 'HASH', + '$' => 'DOLLAR', + '%' => 'PERCENT', + '&' => 'AMPERSAND', + '\'' => 'APOSTROPHE', + '(' => 'LEFT_PARENTHESIS', + ')' => 'RIGHT_PARENTHESIS', + '*' => 'ASTERISK', + '+' => 'PLUS', + ',' => 'COMMA', + '-' => 'MINUS', + '.' => 'DOT', + '/' => 'SLASH', + ':' => 'COLON', + ';' => 'SEMICOLON', + '<' => 'LESS_THAN', + '=' => 'EQUALS', + '>' => 'GREATER_THAN', + '?' => 'QUESTION_MARK', + '@' => 'AT', + '[' => 'LEFT_BRACKET', + '\\' => 'BACKSLASH', + ']' => 'RIGHT_BRACKET', + '^' => 'CARET', + '_' => 'UNDERSCORE', + '`' => 'BACKTICK', + '{' => 'LEFT_BRACE', + '|' => 'PIPE', + '}' => 'RIGHT_BRACE', + '~' => 'TILDE', + ]; + public static function getClassName(SpecObjectInterface $reference, string $fallbackName = ''): string { if (!($reference instanceof Reference)) { @@ -36,6 +91,20 @@ public static function getClassName(SpecObjectInterface $reference, string $fall return CaseCaster::toPascal($referencePath[0]); } + /** + * Class name of an object schema. A schema name that is a reserved word in PHP (e.g. `Match`) gets a `Schema` + * suffix; the names derived from the schema name (e.g. `MatchCollection`) are valid and stay unchanged. + */ + public static function getSchemaClassName(string $schemaName): string + { + return self::isReservedClassName($schemaName) ? $schemaName . self::RESERVED_CLASS_NAME_SUFFIX : $schemaName; + } + + public static function isReservedClassName(string $name): bool + { + return in_array(strtolower($name), self::RESERVED_CLASS_NAMES, true); + } + public static function getEnumConstName(Field $field, string $enum): string { return sprintf( @@ -44,4 +113,55 @@ public static function getEnumConstName(Field $field, string $enum): string CaseCaster::toMacro($enum) ); } + + /** + * Constant name for an enum value that yields no valid name from its letters and digits (e.g. `*`). + */ + public static function getSymbolicEnumConstName(Field $field, string $enum): string + { + return sprintf( + '%s_%s', + CaseCaster::toMacro($field->getName()), + self::getSymbolicEnumValueName($enum) + ); + } + + /** + * Name for an enum value without letters or digits, built from the names of its characters, + * e.g. `*` -> ASTERISK, `>=` -> GREATER_THAN_EQUALS, `€` -> U20AC and the empty string -> EMPTY. + */ + public static function getSymbolicEnumValueName(string $value): string + { + if ($value === '') { + return self::EMPTY_ENUM_VALUE_NAME; + } + + $names = []; + foreach (mb_str_split($value, 1, 'UTF-8') as $character) { + if (isset(self::SYMBOL_NAMES[$character])) { + $names[] = self::SYMBOL_NAMES[$character]; + } elseif (mb_check_encoding($character, 'UTF-8')) { + $names[] = sprintf('U%04X', mb_ord($character, 'UTF-8')); + } else { + $names[] = 'X' . strtoupper(bin2hex($character)); + } + } + + return implode('_', $names); + } + + /** + * Returns the name, or when it is already taken, the name with the first free numeric suffix. + * + * @param string[] $takenNames + */ + public static function getUniqueName(string $name, array $takenNames, string $separator = '_'): string + { + $uniqueName = $name; + for ($suffix = 2; in_array($uniqueName, $takenNames, true); ++$suffix) { + $uniqueName = $name . $separator . $suffix; + } + + return $uniqueName; + } } diff --git a/src/Output/Copy/Request/LiteralRequestBody.php b/src/Output/Copy/Request/LiteralRequestBody.php new file mode 100644 index 0000000..8645c19 --- /dev/null +++ b/src/Output/Copy/Request/LiteralRequestBody.php @@ -0,0 +1,43 @@ +value = $value; + } + + public function toArray(): array + { + return [ContentTypeSerializerInterface::LITERAL_VALUE_KEY => $this->normalize($this->value)]; + } + + private function normalize($value) + { + if ($value instanceof DateTimeInterface) { + return $value->format(DATE_RFC3339); + } + + if (is_array($value)) { + foreach ($value as $key => $item) { + $value[$key] = $this->normalize($item); + } + } + + return $value; + } +} diff --git a/src/Output/Copy/Serializer/ContentType/AbstractJsonContentTypeSerializer.php b/src/Output/Copy/Serializer/ContentType/AbstractJsonContentTypeSerializer.php index e2a8112..8aed19e 100644 --- a/src/Output/Copy/Serializer/ContentType/AbstractJsonContentTypeSerializer.php +++ b/src/Output/Copy/Serializer/ContentType/AbstractJsonContentTypeSerializer.php @@ -18,7 +18,14 @@ abstract class AbstractJsonContentTypeSerializer implements ContentTypeSerialize */ public function encode(SerializableInterface $body): string { - $encodedData = json_encode($body->toArray(), self::JSON_OPTIONS); + $data = $body->toArray(); + + // A literal (non-object) body is wrapped the same way decode() wraps a literal response. + if (array_keys($data) === [ContentTypeSerializerInterface::LITERAL_VALUE_KEY]) { + $data = $data[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + + $encodedData = json_encode($data, self::JSON_OPTIONS); $lastErrorCode = json_last_error(); if ($lastErrorCode === JSON_ERROR_NONE && $encodedData !== false) { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index b69dd9a..1eb9d19 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -26,6 +26,7 @@ use DoclerLabs\ApiClientGenerator\Generator\Security\BearerAuthenticationSecurityStrategy; use DoclerLabs\ApiClientGenerator\Generator\Security\XWSSESecurityStrategy; use DoclerLabs\ApiClientGenerator\Generator\ServiceProviderGenerator; +use DoclerLabs\ApiClientGenerator\Input\ClassNameCollisionResolver; use DoclerLabs\ApiClientGenerator\Input\Configuration; use DoclerLabs\ApiClientGenerator\Input\Factory\FieldFactory; use DoclerLabs\ApiClientGenerator\Input\Factory\OperationCollectionFactory; @@ -144,6 +145,11 @@ public function register(Container $pimple): void $pimple[OpenApiParser::class] = static fn (Container $container) => new OpenApiParser( $container[OperationCollectionFactory::class], + $container[PhpVersion::class], + $container[ClassNameCollisionResolver::class] + ); + + $pimple[ClassNameCollisionResolver::class] = static fn (Container $container) => new ClassNameCollisionResolver( $container[PhpVersion::class] ); diff --git a/test/suite/functional/Generator/Client/LiteralResponsesClient74.php b/test/suite/functional/Generator/Client/LiteralResponsesClient74.php new file mode 100644 index 0000000..3a2b934 --- /dev/null +++ b/test/suite/functional/Generator/Client/LiteralResponsesClient74.php @@ -0,0 +1,81 @@ +client = $client; + $this->container = $container; + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->client->sendRequest($this->container->get(RequestMapperInterface::class)->map($request)); + } + + public function getClock(GetClockRequest $request): string + { + $response = $this->handleResponse($this->sendRequest($request)); + if (!isset($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY])) { + throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + + public function getLastSeen(GetLastSeenRequest $request): ?string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? null; + } + + public function getHealth(GetHealthRequest $request): string + { + $response = $this->handleResponse($this->sendRequest($request)); + if (!isset($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY])) { + throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + + public function getMode(GetModeRequest $request): ?string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? null; + } + + protected function handleResponse(ResponseInterface $response) + { + return $this->container->get(ResponseHandler::class)->handle($response); + } +} diff --git a/test/suite/functional/Generator/Client/LiteralResponsesClient80.php b/test/suite/functional/Generator/Client/LiteralResponsesClient80.php new file mode 100644 index 0000000..2c22414 --- /dev/null +++ b/test/suite/functional/Generator/Client/LiteralResponsesClient80.php @@ -0,0 +1,69 @@ +client->sendRequest($this->container->get(RequestMapperInterface::class)->map($request)); + } + + public function getClock(GetClockRequest $request): string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + public function getLastSeen(GetLastSeenRequest $request): ?string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? null; + } + + public function getHealth(GetHealthRequest $request): string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + public function getMode(GetModeRequest $request): ?string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? null; + } + + protected function handleResponse(ResponseInterface $response) + { + return $this->container->get(ResponseHandler::class)->handle($response); + } +} diff --git a/test/suite/functional/Generator/Client/LiteralResponsesClient81.php b/test/suite/functional/Generator/Client/LiteralResponsesClient81.php new file mode 100644 index 0000000..fd2116a --- /dev/null +++ b/test/suite/functional/Generator/Client/LiteralResponsesClient81.php @@ -0,0 +1,71 @@ +client->sendRequest($this->container->get(RequestMapperInterface::class)->map($request)); + } + + public function getClock(GetClockRequest $request): string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + public function getLastSeen(GetLastSeenRequest $request): ?string + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? null; + } + + public function getHealth(GetHealthRequest $request): GetHealthResponseBodyEnum + { + $response = $this->handleResponse($this->sendRequest($request)); + + return GetHealthResponseBodyEnum::from($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.')); + } + + public function getMode(GetModeRequest $request): ?GetModeResponseBodyEnum + { + $response = $this->handleResponse($this->sendRequest($request)); + + return isset($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]) ? GetModeResponseBodyEnum::from($response[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]) : null; + } + + protected function handleResponse(ResponseInterface $response) + { + return $this->container->get(ResponseHandler::class)->handle($response); + } +} diff --git a/test/suite/functional/Generator/Client/MultipleResponsesClient74.php b/test/suite/functional/Generator/Client/MultipleResponsesClient74.php index 37ab010..d1ace7a 100644 --- a/test/suite/functional/Generator/Client/MultipleResponsesClient74.php +++ b/test/suite/functional/Generator/Client/MultipleResponsesClient74.php @@ -10,11 +10,14 @@ namespace Test; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Psr\Container\ContainerInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseInterface; use RuntimeException; use Test\Request\AddPetTaskRequest; +use Test\Request\GetPetsStatusRequest; +use Test\Request\ImportPetsRequest; use Test\Request\Mapper\RequestMapperInterface; use Test\Request\RequestInterface; use Test\Request\SavePetRequest; @@ -22,6 +25,7 @@ use Test\Schema\Mapper\PetMapper; use Test\Schema\Mapper\PetTaskMapper; use Test\Schema\Pet; +use Test\Serializer\ContentType\ContentTypeSerializerInterface; class MultipleResponsesClient { @@ -70,6 +74,36 @@ public function addPetTask(AddPetTaskRequest $request) throw new RuntimeException('Response status code not properly mapped in schema.'); } + public function importPets(ImportPetsRequest $request) + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + switch ($response->getStatusCode()) { + case 201: + return null; + case 202: + return null; + } + throw new RuntimeException('Response status code not properly mapped in schema.'); + } + + public function getPetsStatus(GetPetsStatusRequest $request): ?string + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + switch ($response->getStatusCode()) { + case 204: + return null; + case 200: + if (!isset($unserializedResponse[ContentTypeSerializerInterface::LITERAL_VALUE_KEY])) { + throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'); + } + + return $unserializedResponse[ContentTypeSerializerInterface::LITERAL_VALUE_KEY]; + } + throw new RuntimeException('Response status code not properly mapped in schema.'); + } + protected function handleResponse(ResponseInterface $response) { return $this->container->get(ResponseHandler::class)->handle($response); diff --git a/test/suite/functional/Generator/Client/MultipleResponsesClient80.php b/test/suite/functional/Generator/Client/MultipleResponsesClient80.php index bd4c45a..206e763 100644 --- a/test/suite/functional/Generator/Client/MultipleResponsesClient80.php +++ b/test/suite/functional/Generator/Client/MultipleResponsesClient80.php @@ -10,10 +10,14 @@ namespace Test; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Psr\Container\ContainerInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseInterface; +use RuntimeException; use Test\Request\AddPetTaskRequest; +use Test\Request\GetPetsStatusRequest; +use Test\Request\ImportPetsRequest; use Test\Request\Mapper\RequestMapperInterface; use Test\Request\RequestInterface; use Test\Request\SavePetRequest; @@ -21,6 +25,7 @@ use Test\Schema\Mapper\PetMapper; use Test\Schema\Mapper\PetTaskMapper; use Test\Schema\Pet; +use Test\Serializer\ContentType\ContentTypeSerializerInterface; class MultipleResponsesClient { @@ -41,6 +46,7 @@ public function savePet(SavePetRequest $request): ?Pet return match ($response->getStatusCode()) { 204 => null, 200, 201 => $this->container->get(PetMapper::class)->toSchema($unserializedResponse), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), }; } @@ -52,7 +58,32 @@ public function addPetTask(AddPetTaskRequest $request) return match ($response->getStatusCode()) { 204 => null, 200, 201 => $this->container->get(PetMapper::class)->toSchema($unserializedResponse), - 202 => $this->container->get(PetTaskMapper::class)->toSchema($unserializedResponse), + 202 => $this->container->get(PetTaskMapper::class)->toSchema($unserializedResponse), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), + }; + } + + public function importPets(ImportPetsRequest $request) + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + + return match ($response->getStatusCode()) { + 201 => null, + 202 => null, + default => throw new RuntimeException('Response status code not properly mapped in schema.'), + }; + } + + public function getPetsStatus(GetPetsStatusRequest $request): ?string + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + + return match ($response->getStatusCode()) { + 204 => null, + 200 => $unserializedResponse[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.'), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), }; } diff --git a/test/suite/functional/Generator/Client/MultipleResponsesClient81.php b/test/suite/functional/Generator/Client/MultipleResponsesClient81.php index 17f2604..02f5793 100644 --- a/test/suite/functional/Generator/Client/MultipleResponsesClient81.php +++ b/test/suite/functional/Generator/Client/MultipleResponsesClient81.php @@ -10,17 +10,23 @@ namespace Test; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Psr\Container\ContainerInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseInterface; +use RuntimeException; use Test\Request\AddPetTaskRequest; +use Test\Request\GetPetsStatusRequest; +use Test\Request\ImportPetsRequest; use Test\Request\Mapper\RequestMapperInterface; use Test\Request\RequestInterface; use Test\Request\SavePetRequest; use Test\Response\ResponseHandler; +use Test\Schema\GetPetsStatusResponseBodyEnum; use Test\Schema\Mapper\PetMapper; use Test\Schema\Mapper\PetTaskMapper; use Test\Schema\Pet; +use Test\Serializer\ContentType\ContentTypeSerializerInterface; class MultipleResponsesClient { @@ -41,6 +47,7 @@ public function savePet(SavePetRequest $request): ?Pet return match ($response->getStatusCode()) { 204 => null, 200, 201 => $this->container->get(PetMapper::class)->toSchema($unserializedResponse), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), }; } @@ -52,7 +59,32 @@ public function addPetTask(AddPetTaskRequest $request) return match ($response->getStatusCode()) { 204 => null, 200, 201 => $this->container->get(PetMapper::class)->toSchema($unserializedResponse), - 202 => $this->container->get(PetTaskMapper::class)->toSchema($unserializedResponse), + 202 => $this->container->get(PetTaskMapper::class)->toSchema($unserializedResponse), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), + }; + } + + public function importPets(ImportPetsRequest $request) + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + + return match ($response->getStatusCode()) { + 201 => null, + 202 => null, + default => throw new RuntimeException('Response status code not properly mapped in schema.'), + }; + } + + public function getPetsStatus(GetPetsStatusRequest $request): ?GetPetsStatusResponseBodyEnum + { + $response = $this->sendRequest($request); + $unserializedResponse = $this->handleResponse($response); + + return match ($response->getStatusCode()) { + 204 => null, + 200 => GetPetsStatusResponseBodyEnum::from($unserializedResponse[ContentTypeSerializerInterface::LITERAL_VALUE_KEY] ?? throw new UnexpectedResponseBodyException('Response body does not contain the expected non-null value.')), + default => throw new RuntimeException('Response status code not properly mapped in schema.'), }; } diff --git a/test/suite/functional/Generator/Client/literal-responses.yaml b/test/suite/functional/Generator/Client/literal-responses.yaml new file mode 100644 index 0000000..3a61e6c --- /dev/null +++ b/test/suite/functional/Generator/Client/literal-responses.yaml @@ -0,0 +1,52 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Literal Responses +paths: + /clock: + get: + operationId: getClock + responses: + '200': + description: Server time + content: + application/json: + schema: + type: string + /last-seen: + get: + operationId: getLastSeen + responses: + '200': + description: Last seen time, if any + content: + application/json: + schema: + type: string + nullable: true + /health: + get: + operationId: getHealth + responses: + '200': + description: Health check + content: + application/json: + schema: + type: string + enum: + - OK + /mode: + get: + operationId: getMode + responses: + '200': + description: Mode, if set + content: + application/json: + schema: + type: string + nullable: true + enum: + - auto + - manual diff --git a/test/suite/functional/Generator/Client/multiple-responses.yaml b/test/suite/functional/Generator/Client/multiple-responses.yaml index 08976d8..86cddba 100644 --- a/test/suite/functional/Generator/Client/multiple-responses.yaml +++ b/test/suite/functional/Generator/Client/multiple-responses.yaml @@ -59,6 +59,29 @@ paths: $ref: '#/components/schemas/PetTask' '204': description: pet did not change + /pets-import: + post: + operationId: importPets + responses: + '201': + description: pets imported + '202': + description: pets import accepted + /pets-status: + get: + operationId: getPetsStatus + responses: + '200': + description: pet store status + content: + application/json: + schema: + type: string + enum: + - open + - closed + '204': + description: pet store status unknown components: schemas: Pet: diff --git a/test/suite/functional/Generator/ClientGeneratorTest.php b/test/suite/functional/Generator/ClientGeneratorTest.php index a46dca8..f27df56 100644 --- a/test/suite/functional/Generator/ClientGeneratorTest.php +++ b/test/suite/functional/Generator/ClientGeneratorTest.php @@ -52,6 +52,24 @@ public function exampleProvider(): array self::BASE_NAMESPACE . '\\MultipleResponsesClient', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'Literal responses with php 7.4' => [ + '/Client/literal-responses.yaml', + '/Client/LiteralResponsesClient74.php', + self::BASE_NAMESPACE . '\\LiteralResponsesClient', + ConfigurationBuilder::fake()->build(), + ], + 'Literal responses with php 8.0' => [ + '/Client/literal-responses.yaml', + '/Client/LiteralResponsesClient80.php', + self::BASE_NAMESPACE . '\\LiteralResponsesClient', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP80)->build(), + ], + 'Literal responses with php 8.1' => [ + '/Client/literal-responses.yaml', + '/Client/LiteralResponsesClient81.php', + self::BASE_NAMESPACE . '\\LiteralResponsesClient', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], 'Basic schema with php 8.1 and include tags' => [ '/Client/petstore.yaml', '/Client/SwaggerPetstoreClientWithTags.php', diff --git a/test/suite/functional/Generator/EnumGeneratorTest.php b/test/suite/functional/Generator/EnumGeneratorTest.php index 35ea966..ff10155 100644 --- a/test/suite/functional/Generator/EnumGeneratorTest.php +++ b/test/suite/functional/Generator/EnumGeneratorTest.php @@ -53,6 +53,60 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\GetResourcesByStatusesStatusesItemEnum', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'Inline enum response body' => [ + '/Client/literal-responses.yaml', + '/Schema/GetHealthResponseBodyEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\GetHealthResponseBodyEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Inline nullable enum response body' => [ + '/Client/literal-responses.yaml', + '/Schema/GetModeResponseBodyEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\GetModeResponseBodyEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request parameter enum with a value consisting of symbols only' => [ + '/Request/getResourcesBySymbols.yaml', + '/Schema/OperatorEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\OperatorEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Inline array of enums request parameter with a value consisting of symbols only' => [ + '/Request/getResourcesBySymbols.yaml', + '/Schema/GetResourcesBySymbolsEmbedItemEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\GetResourcesBySymbolsEmbedItemEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Enum named after a reserved word, with the value class' => [ + '/Schema/reservedWords.yaml', + '/Schema/ListEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ListEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Enum property colliding with an enum query parameter keeps the emitted definition' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/CallStatusEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\CallStatusEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Enum property colliding with an enum query parameter gets a numeric suffix' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/CallStatus2Enum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\CallStatus2Enum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Same enum query parameter with different values, the last one keeps the name' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/CriterionStatusEqEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\CriterionStatusEqEnum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Same enum query parameter with different values, the other one gets a numeric suffix' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/CriterionStatusEq2Enum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\CriterionStatusEq2Enum', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], ]; } diff --git a/test/suite/functional/Generator/Request/AddMemberRequest72.php b/test/suite/functional/Generator/Request/AddMemberRequest72.php new file mode 100644 index 0000000..9c20381 --- /dev/null +++ b/test/suite/functional/Generator/Request/AddMemberRequest72.php @@ -0,0 +1,72 @@ +campaignId = $campaignId; + $this->addMemberRequestBody = $addMemberRequestBody; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return strtr('v1/campaigns/{campaignId}/members', ['{campaignId}' => $this->campaignId]); + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->addMemberRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/AddMemberRequest74.php b/test/suite/functional/Generator/Request/AddMemberRequest74.php new file mode 100644 index 0000000..9ae178d --- /dev/null +++ b/test/suite/functional/Generator/Request/AddMemberRequest74.php @@ -0,0 +1,69 @@ +campaignId = $campaignId; + $this->addMemberRequestBody = $addMemberRequestBody; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return strtr('v1/campaigns/{campaignId}/members', ['{campaignId}' => $this->campaignId]); + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->addMemberRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/AddMemberRequest81.php b/test/suite/functional/Generator/Request/AddMemberRequest81.php new file mode 100644 index 0000000..151fb63 --- /dev/null +++ b/test/suite/functional/Generator/Request/AddMemberRequest81.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return strtr('v1/campaigns/{campaignId}/members', ['{campaignId}' => $this->campaignId]); + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->addMemberRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/AddMemberRequest83.php b/test/suite/functional/Generator/Request/AddMemberRequest83.php new file mode 100644 index 0000000..151fb63 --- /dev/null +++ b/test/suite/functional/Generator/Request/AddMemberRequest83.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return strtr('v1/campaigns/{campaignId}/members', ['{campaignId}' => $this->campaignId]); + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->addMemberRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/CheckEmailStatusRequest72.php b/test/suite/functional/Generator/Request/CheckEmailStatusRequest72.php new file mode 100644 index 0000000..13b064d --- /dev/null +++ b/test/suite/functional/Generator/Request/CheckEmailStatusRequest72.php @@ -0,0 +1,57 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/email-status'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/CheckEmailStatusRequest74.php b/test/suite/functional/Generator/Request/CheckEmailStatusRequest74.php new file mode 100644 index 0000000..611796c --- /dev/null +++ b/test/suite/functional/Generator/Request/CheckEmailStatusRequest74.php @@ -0,0 +1,56 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/email-status'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/CheckEmailStatusRequest81.php b/test/suite/functional/Generator/Request/CheckEmailStatusRequest81.php new file mode 100644 index 0000000..611796c --- /dev/null +++ b/test/suite/functional/Generator/Request/CheckEmailStatusRequest81.php @@ -0,0 +1,56 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/email-status'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/CheckEmailStatusRequest83.php b/test/suite/functional/Generator/Request/CheckEmailStatusRequest83.php new file mode 100644 index 0000000..611796c --- /dev/null +++ b/test/suite/functional/Generator/Request/CheckEmailStatusRequest83.php @@ -0,0 +1,56 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/email-status'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/CreateNoteRequest72.php b/test/suite/functional/Generator/Request/CreateNoteRequest72.php new file mode 100644 index 0000000..a3e244d --- /dev/null +++ b/test/suite/functional/Generator/Request/CreateNoteRequest72.php @@ -0,0 +1,71 @@ +note = $note; + $this->contentType = $contentType; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/notes'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return Note + */ + public function getBody() + { + return $this->note; + } +} diff --git a/test/suite/functional/Generator/Request/CreateNoteRequest74.php b/test/suite/functional/Generator/Request/CreateNoteRequest74.php new file mode 100644 index 0000000..783d25a --- /dev/null +++ b/test/suite/functional/Generator/Request/CreateNoteRequest74.php @@ -0,0 +1,69 @@ +note = $note; + $this->contentType = $contentType; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/notes'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return Note + */ + public function getBody() + { + return $this->note; + } +} diff --git a/test/suite/functional/Generator/Request/CreateNoteRequest81.php b/test/suite/functional/Generator/Request/CreateNoteRequest81.php new file mode 100644 index 0000000..b812d07 --- /dev/null +++ b/test/suite/functional/Generator/Request/CreateNoteRequest81.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/notes'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return Note + */ + public function getBody() + { + return $this->note; + } +} diff --git a/test/suite/functional/Generator/Request/CreateNoteRequest83.php b/test/suite/functional/Generator/Request/CreateNoteRequest83.php new file mode 100644 index 0000000..b812d07 --- /dev/null +++ b/test/suite/functional/Generator/Request/CreateNoteRequest83.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'POST'; + } + + public function getRoute(): string + { + return 'v1/notes'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return Note + */ + public function getBody() + { + return $this->note; + } +} diff --git a/test/suite/functional/Generator/Request/FindHostsRequest81.php b/test/suite/functional/Generator/Request/FindHostsRequest81.php new file mode 100644 index 0000000..692fe1b --- /dev/null +++ b/test/suite/functional/Generator/Request/FindHostsRequest81.php @@ -0,0 +1,92 @@ +contentType; + } + + public function setCallStatus(CallStatusEnum $callStatus): self + { + $this->callStatus = $callStatus; + + return $this; + } + + public function setStatus(StatusEnum $status): self + { + $this->status = $status; + + return $this; + } + + public function setCriterionStatusEq(CriterionStatusEq2Enum $criterionStatusEq): self + { + $this->criterionStatusEq = $criterionStatusEq; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/hosts'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['callStatus' => $this->callStatus?->value, 'status' => $this->status?->value, 'criterion_status_eq' => $this->criterionStatusEq?->value], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['callStatus' => $this->callStatus?->value, 'status' => $this->status?->value, 'criterion_status_eq' => $this->criterionStatusEq?->value]; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return []; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/FindItemsRequest72.php b/test/suite/functional/Generator/Request/FindItemsRequest72.php new file mode 100644 index 0000000..8108fc1 --- /dev/null +++ b/test/suite/functional/Generator/Request/FindItemsRequest72.php @@ -0,0 +1,149 @@ +contentType; + } + + /** + * @throws RequestValidationException + */ + public function setLimit(?int $limit): self + { + if ($limit !== null && $limit < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Cannot be less than 1.', 'limit', $limit)); + } + $this->limit = $limit; + + return $this; + } + + /** + * @throws RequestValidationException + */ + public function setName(?string $name): self + { + if ($name !== null && grapheme_strlen($name) < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Length should be greater than 1.', 'name', $name)); + } + $this->name = $name; + + return $this; + } + + public function setStatus(?string $status): self + { + $this->status = $status; + + return $this; + } + + public function setCreatedAfter(?DateTimeInterface $createdAfter): self + { + $this->createdAfter = $createdAfter; + + return $this; + } + + public function setXRequestSource(?string $xRequestSource): self + { + $this->xRequestSource = $xRequestSource; + + return $this; + } + + public function setSession(?string $session): self + { + $this->session = $session; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/items'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status, 'createdAfter' => $this->createdAfter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status, 'createdAfter' => $this->createdAfter]; + } + + public function getCookies(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['session' => $this->session], static function ($value) { + return null !== $value; + })); + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Request-Source' => $this->xRequestSource], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/FindItemsRequest74.php b/test/suite/functional/Generator/Request/FindItemsRequest74.php new file mode 100644 index 0000000..15ed5cc --- /dev/null +++ b/test/suite/functional/Generator/Request/FindItemsRequest74.php @@ -0,0 +1,142 @@ +contentType; + } + + /** + * @throws RequestValidationException + */ + public function setLimit(?int $limit): self + { + if ($limit !== null && $limit < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Cannot be less than 1.', 'limit', $limit)); + } + $this->limit = $limit; + + return $this; + } + + /** + * @throws RequestValidationException + */ + public function setName(?string $name): self + { + if ($name !== null && grapheme_strlen($name) < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Length should be greater than 1.', 'name', $name)); + } + $this->name = $name; + + return $this; + } + + public function setStatus(?string $status): self + { + $this->status = $status; + + return $this; + } + + public function setCreatedAfter(?DateTimeInterface $createdAfter): self + { + $this->createdAfter = $createdAfter; + + return $this; + } + + public function setXRequestSource(?string $xRequestSource): self + { + $this->xRequestSource = $xRequestSource; + + return $this; + } + + public function setSession(?string $session): self + { + $this->session = $session; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/items'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status, 'createdAfter' => $this->createdAfter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status, 'createdAfter' => $this->createdAfter]; + } + + public function getCookies(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['session' => $this->session], static function ($value) { + return null !== $value; + })); + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Request-Source' => $this->xRequestSource], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/FindItemsRequest81.php b/test/suite/functional/Generator/Request/FindItemsRequest81.php new file mode 100644 index 0000000..59097c0 --- /dev/null +++ b/test/suite/functional/Generator/Request/FindItemsRequest81.php @@ -0,0 +1,139 @@ +contentType; + } + + /** + * @throws RequestValidationException + */ + public function setLimit(?int $limit): self + { + if ($limit !== null && $limit < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Cannot be less than 1.', 'limit', $limit)); + } + $this->limit = $limit; + + return $this; + } + + /** + * @throws RequestValidationException + */ + public function setName(?string $name): self + { + if ($name !== null && grapheme_strlen($name) < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Length should be greater than 1.', 'name', $name)); + } + $this->name = $name; + + return $this; + } + + public function setStatus(?StatusEnum $status): self + { + $this->status = $status; + + return $this; + } + + public function setCreatedAfter(?DateTimeInterface $createdAfter): self + { + $this->createdAfter = $createdAfter; + + return $this; + } + + public function setXRequestSource(?string $xRequestSource): self + { + $this->xRequestSource = $xRequestSource; + + return $this; + } + + public function setSession(?string $session): self + { + $this->session = $session; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/items'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status?->value, 'createdAfter' => $this->createdAfter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status?->value, 'createdAfter' => $this->createdAfter]; + } + + public function getCookies(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['session' => $this->session], static function ($value) { + return null !== $value; + })); + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Request-Source' => $this->xRequestSource], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/FindItemsRequest83.php b/test/suite/functional/Generator/Request/FindItemsRequest83.php new file mode 100644 index 0000000..59097c0 --- /dev/null +++ b/test/suite/functional/Generator/Request/FindItemsRequest83.php @@ -0,0 +1,139 @@ +contentType; + } + + /** + * @throws RequestValidationException + */ + public function setLimit(?int $limit): self + { + if ($limit !== null && $limit < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Cannot be less than 1.', 'limit', $limit)); + } + $this->limit = $limit; + + return $this; + } + + /** + * @throws RequestValidationException + */ + public function setName(?string $name): self + { + if ($name !== null && grapheme_strlen($name) < 1) { + throw new RequestValidationException(sprintf('Invalid %s value. Given: `%s`. Length should be greater than 1.', 'name', $name)); + } + $this->name = $name; + + return $this; + } + + public function setStatus(?StatusEnum $status): self + { + $this->status = $status; + + return $this; + } + + public function setCreatedAfter(?DateTimeInterface $createdAfter): self + { + $this->createdAfter = $createdAfter; + + return $this; + } + + public function setXRequestSource(?string $xRequestSource): self + { + $this->xRequestSource = $xRequestSource; + + return $this; + } + + public function setSession(?string $session): self + { + $this->session = $session; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/items'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status?->value, 'createdAfter' => $this->createdAfter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['limit' => $this->limit, 'name' => $this->name, 'status' => $this->status?->value, 'createdAfter' => $this->createdAfter]; + } + + public function getCookies(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['session' => $this->session], static function ($value) { + return null !== $value; + })); + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Request-Source' => $this->xRequestSource], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetMessagesRequest72.php b/test/suite/functional/Generator/Request/GetMessagesRequest72.php new file mode 100644 index 0000000..d9ff5f5 --- /dev/null +++ b/test/suite/functional/Generator/Request/GetMessagesRequest72.php @@ -0,0 +1,133 @@ +xAccountId = $xAccountId; + $this->xDebug = $xDebug; + $this->xTraceId = $xTraceId; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function setXUserId(int $xUserId): self + { + $this->xUserId = $xUserId; + + return $this; + } + + public function setXScore(?float $xScore): self + { + $this->xScore = $xScore; + + return $this; + } + + public function setXPreview(bool $xPreview): self + { + $this->xPreview = $xPreview; + + return $this; + } + + public function setXPriority(int $xPriority): self + { + $this->xPriority = $xPriority; + + return $this; + } + + public function setXUserType(string $xUserType): self + { + $this->xUserType = $xUserType; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/messages'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Account-Id' => (string) $this->xAccountId, 'X-User-Id' => $this->xUserId === null ? null : (string) $this->xUserId, 'X-Score' => $this->xScore === null ? null : (string) $this->xScore, 'X-Debug' => $this->xDebug ? '1' : '0', 'X-Preview' => $this->xPreview === null ? null : ($this->xPreview ? '1' : '0'), 'X-Priority' => $this->xPriority === null ? null : (string) $this->xPriority, 'X-User-Type' => $this->xUserType, 'X-Trace-Id' => $this->xTraceId], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetMessagesRequest74.php b/test/suite/functional/Generator/Request/GetMessagesRequest74.php new file mode 100644 index 0000000..794cabf --- /dev/null +++ b/test/suite/functional/Generator/Request/GetMessagesRequest74.php @@ -0,0 +1,124 @@ +xAccountId = $xAccountId; + $this->xDebug = $xDebug; + $this->xTraceId = $xTraceId; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function setXUserId(int $xUserId): self + { + $this->xUserId = $xUserId; + + return $this; + } + + public function setXScore(?float $xScore): self + { + $this->xScore = $xScore; + + return $this; + } + + public function setXPreview(bool $xPreview): self + { + $this->xPreview = $xPreview; + + return $this; + } + + public function setXPriority(int $xPriority): self + { + $this->xPriority = $xPriority; + + return $this; + } + + public function setXUserType(string $xUserType): self + { + $this->xUserType = $xUserType; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/messages'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Account-Id' => (string) $this->xAccountId, 'X-User-Id' => $this->xUserId === null ? null : (string) $this->xUserId, 'X-Score' => $this->xScore === null ? null : (string) $this->xScore, 'X-Debug' => $this->xDebug ? '1' : '0', 'X-Preview' => $this->xPreview === null ? null : ($this->xPreview ? '1' : '0'), 'X-Priority' => $this->xPriority === null ? null : (string) $this->xPriority, 'X-User-Type' => $this->xUserType, 'X-Trace-Id' => $this->xTraceId], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetMessagesRequest81.php b/test/suite/functional/Generator/Request/GetMessagesRequest81.php new file mode 100644 index 0000000..75fb4e1 --- /dev/null +++ b/test/suite/functional/Generator/Request/GetMessagesRequest81.php @@ -0,0 +1,113 @@ +contentType; + } + + public function setXUserId(int $xUserId): self + { + $this->xUserId = $xUserId; + + return $this; + } + + public function setXScore(?float $xScore): self + { + $this->xScore = $xScore; + + return $this; + } + + public function setXPreview(bool $xPreview): self + { + $this->xPreview = $xPreview; + + return $this; + } + + public function setXPriority(XPriorityEnum $xPriority): self + { + $this->xPriority = $xPriority; + + return $this; + } + + public function setXUserType(XUserTypeEnum $xUserType): self + { + $this->xUserType = $xUserType; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/messages'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Account-Id' => (string) $this->xAccountId, 'X-User-Id' => $this->xUserId === null ? null : (string) $this->xUserId, 'X-Score' => $this->xScore === null ? null : (string) $this->xScore, 'X-Debug' => $this->xDebug ? '1' : '0', 'X-Preview' => $this->xPreview === null ? null : ($this->xPreview ? '1' : '0'), 'X-Priority' => $this->xPriority === null ? null : (string) $this->xPriority->value, 'X-User-Type' => $this->xUserType?->value, 'X-Trace-Id' => $this->xTraceId], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetMessagesRequest83.php b/test/suite/functional/Generator/Request/GetMessagesRequest83.php new file mode 100644 index 0000000..75fb4e1 --- /dev/null +++ b/test/suite/functional/Generator/Request/GetMessagesRequest83.php @@ -0,0 +1,113 @@ +contentType; + } + + public function setXUserId(int $xUserId): self + { + $this->xUserId = $xUserId; + + return $this; + } + + public function setXScore(?float $xScore): self + { + $this->xScore = $xScore; + + return $this; + } + + public function setXPreview(bool $xPreview): self + { + $this->xPreview = $xPreview; + + return $this; + } + + public function setXPriority(XPriorityEnum $xPriority): self + { + $this->xPriority = $xPriority; + + return $this; + } + + public function setXUserType(XUserTypeEnum $xUserType): self + { + $this->xUserType = $xUserType; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/messages'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return array_merge([], array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['X-Account-Id' => (string) $this->xAccountId, 'X-User-Id' => $this->xUserId === null ? null : (string) $this->xUserId, 'X-Score' => $this->xScore === null ? null : (string) $this->xScore, 'X-Debug' => $this->xDebug ? '1' : '0', 'X-Preview' => $this->xPreview === null ? null : ($this->xPreview ? '1' : '0'), 'X-Priority' => $this->xPriority === null ? null : (string) $this->xPriority->value, 'X-User-Type' => $this->xUserType?->value, 'X-Trace-Id' => $this->xTraceId], static function ($value) { + return null !== $value; + }))); + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest74.php b/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest74.php new file mode 100644 index 0000000..501556c --- /dev/null +++ b/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest74.php @@ -0,0 +1,89 @@ +operator = $operator; + } + + public function getContentType(): string + { + return $this->contentType; + } + + /** + * @param string[] $embed + */ + public function setEmbed(array $embed): self + { + $this->embed = $embed; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/resources-by-symbols'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['operator' => $this->operator, 'embed' => $this->embed], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['operator' => $this->operator, 'embed' => $this->embed]; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return []; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest81.php b/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest81.php new file mode 100644 index 0000000..c5d2c59 --- /dev/null +++ b/test/suite/functional/Generator/Request/GetResourcesBySymbolsRequest81.php @@ -0,0 +1,80 @@ +contentType; + } + + /** + * @param GetResourcesBySymbolsEmbedItemEnum[] $embed + */ + public function setEmbed(array $embed): self + { + $this->embed = $embed; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/resources-by-symbols'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['operator' => $this->operator->value, 'embed' => $this->embed === null ? null : array_map(static fn (GetResourcesBySymbolsEmbedItemEnum $enum): string => $enum->value, $this->embed)], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['operator' => $this->operator->value, 'embed' => $this->embed === null ? null : array_map(static fn (GetResourcesBySymbolsEmbedItemEnum $enum): string => $enum->value, $this->embed)]; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return []; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest74.php b/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest74.php new file mode 100644 index 0000000..696cb6c --- /dev/null +++ b/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest74.php @@ -0,0 +1,71 @@ +contentType; + } + + public function setFilter($filter): self + { + $this->filter = $filter; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/resources-by-untyped-filter'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['filter' => $this->filter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['filter' => $this->filter]; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return []; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest80.php b/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest80.php new file mode 100644 index 0000000..4bfa5f7 --- /dev/null +++ b/test/suite/functional/Generator/Request/GetResourcesByUntypedFilterRequest80.php @@ -0,0 +1,71 @@ +contentType; + } + + public function setFilter(mixed $filter): self + { + $this->filter = $filter; + + return $this; + } + + public function getMethod(): string + { + return 'GET'; + } + + public function getRoute(): string + { + return 'v1/resources-by-untyped-filter'; + } + + public function getQueryParameters(): array + { + return array_map(static function ($value) { + return $value instanceof SerializableInterface ? $value->toArray() : $value; + }, array_filter(['filter' => $this->filter], static function ($value) { + return null !== $value; + })); + } + + public function getRawQueryParameters(): array + { + return ['filter' => $this->filter]; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return []; + } + + public function getBody() + { + return null; + } +} diff --git a/test/suite/functional/Generator/Request/SetExpiryRequest74.php b/test/suite/functional/Generator/Request/SetExpiryRequest74.php new file mode 100644 index 0000000..ab0fbd4 --- /dev/null +++ b/test/suite/functional/Generator/Request/SetExpiryRequest74.php @@ -0,0 +1,68 @@ +setExpiryRequestBody = $setExpiryRequestBody; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/expiry'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setExpiryRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/SetExpiryRequest83.php b/test/suite/functional/Generator/Request/SetExpiryRequest83.php new file mode 100644 index 0000000..cfc9144 --- /dev/null +++ b/test/suite/functional/Generator/Request/SetExpiryRequest83.php @@ -0,0 +1,65 @@ +contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/expiry'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setExpiryRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/SetNicknameRequest74.php b/test/suite/functional/Generator/Request/SetNicknameRequest74.php new file mode 100644 index 0000000..359e6d9 --- /dev/null +++ b/test/suite/functional/Generator/Request/SetNicknameRequest74.php @@ -0,0 +1,66 @@ +setNicknameRequestBody = $setNicknameRequestBody; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/nickname'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setNicknameRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/SetNicknameRequest83.php b/test/suite/functional/Generator/Request/SetNicknameRequest83.php new file mode 100644 index 0000000..1e62b68 --- /dev/null +++ b/test/suite/functional/Generator/Request/SetNicknameRequest83.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/nickname'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setNicknameRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/SetTagsRequest74.php b/test/suite/functional/Generator/Request/SetTagsRequest74.php new file mode 100644 index 0000000..d2ba732 --- /dev/null +++ b/test/suite/functional/Generator/Request/SetTagsRequest74.php @@ -0,0 +1,66 @@ +setTagsRequestBody = $setTagsRequestBody; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/tags'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setTagsRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/SetTagsRequest83.php b/test/suite/functional/Generator/Request/SetTagsRequest83.php new file mode 100644 index 0000000..8462bee --- /dev/null +++ b/test/suite/functional/Generator/Request/SetTagsRequest83.php @@ -0,0 +1,63 @@ +contentType; + } + + public function getMethod(): string + { + return 'PUT'; + } + + public function getRoute(): string + { + return 'v1/tags'; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getRawQueryParameters(): array + { + return []; + } + + public function getCookies(): array + { + return []; + } + + public function getHeaders(): array + { + return ['Content-Type' => $this->contentType]; + } + + /** + * @return LiteralRequestBody + */ + public function getBody() + { + return new LiteralRequestBody($this->setTagsRequestBody); + } +} diff --git a/test/suite/functional/Generator/Request/addMember.yaml b/test/suite/functional/Generator/Request/addMember.yaml new file mode 100644 index 0000000..19a1109 --- /dev/null +++ b/test/suite/functional/Generator/Request/addMember.yaml @@ -0,0 +1,64 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/campaigns/{campaignId}/members': + post: + description: Literal integer request body + operationId: addMember + parameters: + - name: campaignId + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + type: integer + description: The member id. + responses: + '204': + description: Member added. + '/v1/nickname': + put: + description: Literal nullable string request body + operationId: setNickname + requestBody: + content: + application/json: + schema: + type: string + nullable: true + responses: + '204': + description: Nickname set. + '/v1/tags': + put: + description: Array of strings request body + operationId: setTags + requestBody: + content: + application/json: + schema: + type: array + items: + type: string + responses: + '204': + description: Tags set. + '/v1/expiry': + put: + description: Literal date-time request body + operationId: setExpiry + requestBody: + content: + application/json: + schema: + type: string + format: date-time + responses: + '204': + description: Expiry set. diff --git a/test/suite/functional/Generator/Request/checkEmailStatus.yaml b/test/suite/functional/Generator/Request/checkEmailStatus.yaml new file mode 100644 index 0000000..cb685ed --- /dev/null +++ b/test/suite/functional/Generator/Request/checkEmailStatus.yaml @@ -0,0 +1,38 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/email-status': + post: + description: Media type without schema, the request has no body field + operationId: checkEmailStatus + requestBody: + content: + application/x-www-form-urlencoded: {} + responses: + '204': + description: Email status checked. + '/v1/notes': + post: + description: Media type without schema next to one with a schema + operationId: createNote + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Note' + application/x-www-form-urlencoded: {} + responses: + '201': + description: Note created. +components: + schemas: + Note: + type: object + properties: + text: + type: string + required: + - text diff --git a/test/suite/functional/Generator/Request/findItems.yaml b/test/suite/functional/Generator/Request/findItems.yaml new file mode 100644 index 0000000..f5e97c8 --- /dev/null +++ b/test/suite/functional/Generator/Request/findItems.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/items': + get: + description: Find items with nullable optional parameters + operationId: findItems + parameters: + - $ref: '#/components/parameters/Limit' + - name: name + in: query + schema: + type: string + nullable: true + minLength: 1 + - name: status + in: query + schema: + type: string + nullable: true + enum: + - active + - inactive + - name: createdAfter + in: query + schema: + type: string + format: date-time + nullable: true + - name: X-Request-Source + in: header + schema: + type: string + nullable: true + - name: session + in: cookie + schema: + type: string + nullable: true + responses: + '204': + description: Items found. +components: + schemas: + Limit: + type: integer + nullable: true + minimum: 1 + parameters: + Limit: + name: limit + in: query + schema: + $ref: '#/components/schemas/Limit' diff --git a/test/suite/functional/Generator/Request/getMessages.yaml b/test/suite/functional/Generator/Request/getMessages.yaml new file mode 100644 index 0000000..336b9b6 --- /dev/null +++ b/test/suite/functional/Generator/Request/getMessages.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/messages': + get: + description: Get messages, with non-string header parameters + operationId: getMessages + parameters: + - name: X-Account-Id + in: header + required: true + schema: + type: integer + - name: X-User-Id + in: header + schema: + type: integer + - name: X-Score + in: header + schema: + type: number + format: float + nullable: true + - name: X-Debug + in: header + required: true + schema: + type: boolean + - name: X-Preview + in: header + schema: + type: boolean + - name: X-Priority + in: header + schema: + type: integer + enum: + - 1 + - 2 + - name: X-User-Type + in: header + schema: + type: string + enum: + - member + - performer + - name: X-Trace-Id + in: header + required: true + schema: + type: string + responses: + '204': + description: Messages found. diff --git a/test/suite/functional/Generator/Request/getResourcesBySymbols.yaml b/test/suite/functional/Generator/Request/getResourcesBySymbols.yaml new file mode 100644 index 0000000..fee5b84 --- /dev/null +++ b/test/suite/functional/Generator/Request/getResourcesBySymbols.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/resources-by-symbols': + get: + description: Get resources filtered by enum values that consist of symbols only + operationId: getResourcesBySymbols + parameters: + - name: operator + in: query + required: true + schema: + type: string + default: '=' + enum: + - '=' + - BETWEEN + - IN + - name: embed + in: query + required: false + schema: + type: array + items: + type: string + enum: + - '*' + responses: + '200': + description: list of resources + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer diff --git a/test/suite/functional/Generator/Request/getResourcesByUntypedFilter.yaml b/test/suite/functional/Generator/Request/getResourcesByUntypedFilter.yaml new file mode 100644 index 0000000..9c2a370 --- /dev/null +++ b/test/suite/functional/Generator/Request/getResourcesByUntypedFilter.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example API +paths: + '/v1/resources-by-untyped-filter': + get: + description: Get resources filtered by an untyped (mixed) query parameter + operationId: getResourcesByUntypedFilter + parameters: + - name: filter + in: query + required: false + schema: {} + responses: + '200': + description: list of resources + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer diff --git a/test/suite/functional/Generator/RequestGeneratorTest.php b/test/suite/functional/Generator/RequestGeneratorTest.php index 9c867a7..b4a5a7e 100644 --- a/test/suite/functional/Generator/RequestGeneratorTest.php +++ b/test/suite/functional/Generator/RequestGeneratorTest.php @@ -250,6 +250,192 @@ public function exampleProvider(): array self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetResourcesByStatusesRequest', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), ], + 'Request with nullable optional parameters with php 7.2' => [ + '/Request/findItems.yaml', + '/Request/FindItemsRequest72.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\FindItemsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP72)->build(), + ], + 'Request with nullable optional parameters with php 7.4' => [ + '/Request/findItems.yaml', + '/Request/FindItemsRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\FindItemsRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with nullable optional parameters with php 8.1' => [ + '/Request/findItems.yaml', + '/Request/FindItemsRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\FindItemsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with nullable optional parameters with php 8.3' => [ + '/Request/findItems.yaml', + '/Request/FindItemsRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\FindItemsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with a body media type without schema with php 7.2' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CheckEmailStatusRequest72.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CheckEmailStatusRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP72)->build(), + ], + 'Request with a body media type without schema with php 7.4' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CheckEmailStatusRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CheckEmailStatusRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with a body media type without schema with php 8.1' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CheckEmailStatusRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CheckEmailStatusRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with a body media type without schema with php 8.3' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CheckEmailStatusRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CheckEmailStatusRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with a body media type without schema next to one with schema with php 7.2' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CreateNoteRequest72.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CreateNoteRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP72)->build(), + ], + 'Request with a body media type without schema next to one with schema with php 7.4' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CreateNoteRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CreateNoteRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with a body media type without schema next to one with schema with php 8.1' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CreateNoteRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CreateNoteRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with a body media type without schema next to one with schema with php 8.3' => [ + '/Request/checkEmailStatus.yaml', + '/Request/CreateNoteRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\CreateNoteRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with integer, number and boolean header parameters with php 7.2' => [ + '/Request/getMessages.yaml', + '/Request/GetMessagesRequest72.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetMessagesRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP72)->build(), + ], + 'Request with integer, number and boolean header parameters with php 7.4' => [ + '/Request/getMessages.yaml', + '/Request/GetMessagesRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetMessagesRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with integer, number and boolean header parameters with php 8.1' => [ + '/Request/getMessages.yaml', + '/Request/GetMessagesRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetMessagesRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with integer, number and boolean header parameters with php 8.3' => [ + '/Request/getMessages.yaml', + '/Request/GetMessagesRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetMessagesRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with literal integer body with php 7.2' => [ + '/Request/addMember.yaml', + '/Request/AddMemberRequest72.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\AddMemberRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP72)->build(), + ], + 'Request with literal integer body with php 7.4' => [ + '/Request/addMember.yaml', + '/Request/AddMemberRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\AddMemberRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with literal integer body with php 8.1' => [ + '/Request/addMember.yaml', + '/Request/AddMemberRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\AddMemberRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with literal integer body with php 8.3' => [ + '/Request/addMember.yaml', + '/Request/AddMemberRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\AddMemberRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with literal nullable string body with php 7.4' => [ + '/Request/addMember.yaml', + '/Request/SetNicknameRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetNicknameRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with literal nullable string body with php 8.3' => [ + '/Request/addMember.yaml', + '/Request/SetNicknameRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetNicknameRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with array of strings body with php 7.4' => [ + '/Request/addMember.yaml', + '/Request/SetTagsRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetTagsRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with array of strings body with php 8.3' => [ + '/Request/addMember.yaml', + '/Request/SetTagsRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetTagsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with literal date-time body with php 7.4' => [ + '/Request/addMember.yaml', + '/Request/SetExpiryRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetExpiryRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with literal date-time body with php 8.3' => [ + '/Request/addMember.yaml', + '/Request/SetExpiryRequest83.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\SetExpiryRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP83)->build(), + ], + 'Request with optional untyped (mixed) query parameter with php 7.4' => [ + '/Request/getResourcesByUntypedFilter.yaml', + '/Request/GetResourcesByUntypedFilterRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetResourcesByUntypedFilterRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with optional untyped (mixed) query parameter with php 8.0' => [ + '/Request/getResourcesByUntypedFilter.yaml', + '/Request/GetResourcesByUntypedFilterRequest80.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetResourcesByUntypedFilterRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP80)->build(), + ], + 'Request with enum values consisting of symbols only with php 7.4' => [ + '/Request/getResourcesBySymbols.yaml', + '/Request/GetResourcesBySymbolsRequest74.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetResourcesBySymbolsRequest', + ConfigurationBuilder::fake()->build(), + ], + 'Request with enum values consisting of symbols only with php 8.1' => [ + '/Request/getResourcesBySymbols.yaml', + '/Request/GetResourcesBySymbolsRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\GetResourcesBySymbolsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Request with enum query parameters whose class names collide with php 8.1' => [ + '/Schema/classNameCollisions.yaml', + '/Request/FindHostsRequest81.php', + self::BASE_NAMESPACE . RequestGenerator::NAMESPACE_SUBPATH . '\\FindHostsRequest', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], ]; } diff --git a/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects70.php b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects70.php new file mode 100644 index 0000000..54204dc --- /dev/null +++ b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects70.php @@ -0,0 +1,119 @@ + false, 'groups' => false]; + + /** + * @param ShowSubscriberCollection[] $data + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * @param ShowSubscriberCollection[]|null $previousData + */ + public function setPreviousData($previousData): self + { + $this->previousData = $previousData; + $this->optionalPropertyChanged['previousData'] = true; + + return $this; + } + + /** + * @param ShowSubscriberCollection[][] $groups + */ + public function setGroups(array $groups): self + { + $this->groups = $groups; + $this->optionalPropertyChanged['groups'] = true; + + return $this; + } + + public function hasPreviousData(): bool + { + return $this->optionalPropertyChanged['previousData']; + } + + public function hasGroups(): bool + { + return $this->optionalPropertyChanged['groups']; + } + + /** + * @return ShowSubscriberCollection[] + */ + public function getData(): array + { + return $this->data; + } + + /** + * @return ShowSubscriberCollection[]|null + */ + public function getPreviousData() + { + return $this->previousData; + } + + /** + * @return ShowSubscriberCollection[][]|null + */ + public function getGroups() + { + return $this->groups; + } + + public function toArray(): array + { + $fields = []; + $fields['data'] = array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->data); + if ($this->hasPreviousData()) { + $fields['previousData'] = $this->previousData !== null ? array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->previousData) : null; + } + if ($this->hasGroups()) { + $fields['groups'] = array_map(static function (array $item): array { + return array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $item); + }, $this->groups); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects74.php b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects74.php new file mode 100644 index 0000000..7cdfdd3 --- /dev/null +++ b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects74.php @@ -0,0 +1,115 @@ + false, 'groups' => false]; + + /** + * @param ShowSubscriberCollection[] $data + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * @param ShowSubscriberCollection[]|null $previousData + */ + public function setPreviousData(?array $previousData): self + { + $this->previousData = $previousData; + $this->optionalPropertyChanged['previousData'] = true; + + return $this; + } + + /** + * @param ShowSubscriberCollection[][] $groups + */ + public function setGroups(array $groups): self + { + $this->groups = $groups; + $this->optionalPropertyChanged['groups'] = true; + + return $this; + } + + public function hasPreviousData(): bool + { + return $this->optionalPropertyChanged['previousData']; + } + + public function hasGroups(): bool + { + return $this->optionalPropertyChanged['groups']; + } + + /** + * @return ShowSubscriberCollection[] + */ + public function getData(): array + { + return $this->data; + } + + /** + * @return ShowSubscriberCollection[]|null + */ + public function getPreviousData(): ?array + { + return $this->previousData; + } + + /** + * @return ShowSubscriberCollection[][]|null + */ + public function getGroups(): ?array + { + return $this->groups; + } + + public function toArray(): array + { + $fields = []; + $fields['data'] = array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->data); + if ($this->hasPreviousData()) { + $fields['previousData'] = $this->previousData !== null ? array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->previousData) : null; + } + if ($this->hasGroups()) { + $fields['groups'] = array_map(static function (array $item): array { + return array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $item); + }, $this->groups); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects81.php b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects81.php new file mode 100644 index 0000000..ac4de1e --- /dev/null +++ b/test/suite/functional/Generator/Schema/ArrayOfArraysOfObjects81.php @@ -0,0 +1,112 @@ + false, 'groups' => false]; + + /** + * @param ShowSubscriberCollection[] $data + */ + public function __construct(private readonly array $data) + { + } + + /** + * @param ShowSubscriberCollection[]|null $previousData + */ + public function setPreviousData(?array $previousData): self + { + $this->previousData = $previousData; + $this->optionalPropertyChanged['previousData'] = true; + + return $this; + } + + /** + * @param ShowSubscriberCollection[][] $groups + */ + public function setGroups(array $groups): self + { + $this->groups = $groups; + $this->optionalPropertyChanged['groups'] = true; + + return $this; + } + + public function hasPreviousData(): bool + { + return $this->optionalPropertyChanged['previousData']; + } + + public function hasGroups(): bool + { + return $this->optionalPropertyChanged['groups']; + } + + /** + * @return ShowSubscriberCollection[] + */ + public function getData(): array + { + return $this->data; + } + + /** + * @return ShowSubscriberCollection[]|null + */ + public function getPreviousData(): ?array + { + return $this->previousData; + } + + /** + * @return ShowSubscriberCollection[][]|null + */ + public function getGroups(): ?array + { + return $this->groups; + } + + public function toArray(): array + { + $fields = []; + $fields['data'] = array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->data); + if ($this->hasPreviousData()) { + $fields['previousData'] = $this->previousData !== null ? array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $this->previousData) : null; + } + if ($this->hasGroups()) { + $fields['groups'] = array_map(static function (array $item): array { + return array_map(static function (ShowSubscriberCollection $item): array { + return $item->toArray(); + }, $item); + }, $this->groups); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/CallStatus2Enum.php b/test/suite/functional/Generator/Schema/CallStatus2Enum.php new file mode 100644 index 0000000..9d9df15 --- /dev/null +++ b/test/suite/functional/Generator/Schema/CallStatus2Enum.php @@ -0,0 +1,18 @@ +callId; + } + + public function getStatus(): CallStatus2Enum + { + return $this->status; + } + + public function toArray(): array + { + $fields = []; + $fields['callId'] = $this->callId; + $fields['status'] = $this->status->value; + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ClassSchema.php b/test/suite/functional/Generator/Schema/ClassSchema.php new file mode 100644 index 0000000..b34d1c5 --- /dev/null +++ b/test/suite/functional/Generator/Schema/ClassSchema.php @@ -0,0 +1,53 @@ + false]; + + public function setName(string $name): self + { + $this->name = $name; + $this->optionalPropertyChanged['name'] = true; + + return $this; + } + + public function hasName(): bool + { + return $this->optionalPropertyChanged['name']; + } + + public function getName(): ?string + { + return $this->name; + } + + public function toArray(): array + { + $fields = []; + if ($this->hasName()) { + $fields['name'] = $this->name; + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/CriterionStatusEq2Enum.php b/test/suite/functional/Generator/Schema/CriterionStatusEq2Enum.php new file mode 100644 index 0000000..cb09b87 --- /dev/null +++ b/test/suite/functional/Generator/Schema/CriterionStatusEq2Enum.php @@ -0,0 +1,17 @@ + false]; + + public function __construct(bool $isActive) + { + $this->isActive = $isActive; + } + + public function setDiscountedPrice(?float $discountedPrice): self + { + $this->discountedPrice = $discountedPrice; + $this->optionalPropertyChanged['discountedPrice'] = true; + + return $this; + } + + public function hasDiscountedPrice(): bool + { + return $this->optionalPropertyChanged['discountedPrice']; + } + + public function getIsActive(): bool + { + return $this->isActive; + } + + public function getDiscountedPrice(): ?float + { + return $this->discountedPrice; + } + + public function toArray(): array + { + $fields = []; + $fields['isActive'] = $this->isActive; + if ($this->hasDiscountedPrice()) { + $fields['discountedPrice'] = $this->discountedPrice; + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/HotDeal2.php b/test/suite/functional/Generator/Schema/HotDeal2.php new file mode 100644 index 0000000..6412651 --- /dev/null +++ b/test/suite/functional/Generator/Schema/HotDeal2.php @@ -0,0 +1,41 @@ +discountedPrice = $discountedPrice; + } + + public function getDiscountedPrice(): float + { + return $this->discountedPrice; + } + + public function toArray(): array + { + $fields = []; + $fields['discountedPrice'] = $this->discountedPrice; + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ItemWithMixedPhp74.php b/test/suite/functional/Generator/Schema/ItemWithMixedPhp74.php new file mode 100644 index 0000000..7fbcf9d --- /dev/null +++ b/test/suite/functional/Generator/Schema/ItemWithMixedPhp74.php @@ -0,0 +1,98 @@ + false, 'optionalNullableMixed' => false]; + + public function __construct($mandatoryMixed, $mandatoryNullableMixed) + { + $this->mandatoryMixed = $mandatoryMixed; + $this->mandatoryNullableMixed = $mandatoryNullableMixed; + } + + public function setOptionalMixed($optionalMixed): self + { + $this->optionalMixed = $optionalMixed; + $this->optionalPropertyChanged['optionalMixed'] = true; + + return $this; + } + + public function setOptionalNullableMixed($optionalNullableMixed): self + { + $this->optionalNullableMixed = $optionalNullableMixed; + $this->optionalPropertyChanged['optionalNullableMixed'] = true; + + return $this; + } + + public function hasOptionalMixed(): bool + { + return $this->optionalPropertyChanged['optionalMixed']; + } + + public function hasOptionalNullableMixed(): bool + { + return $this->optionalPropertyChanged['optionalNullableMixed']; + } + + public function getMandatoryMixed() + { + return $this->mandatoryMixed; + } + + public function getOptionalMixed() + { + return $this->optionalMixed; + } + + public function getOptionalNullableMixed() + { + return $this->optionalNullableMixed; + } + + public function getMandatoryNullableMixed() + { + return $this->mandatoryNullableMixed; + } + + public function toArray(): array + { + $fields = []; + $fields['mandatoryMixed'] = $this->mandatoryMixed; + if ($this->hasOptionalMixed()) { + $fields['optionalMixed'] = $this->optionalMixed; + } + if ($this->hasOptionalNullableMixed()) { + $fields['optionalNullableMixed'] = $this->optionalNullableMixed; + } + $fields['mandatoryNullableMixed'] = $this->mandatoryNullableMixed; + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ItemWithMixedPhp80.php b/test/suite/functional/Generator/Schema/ItemWithMixedPhp80.php new file mode 100644 index 0000000..49891d5 --- /dev/null +++ b/test/suite/functional/Generator/Schema/ItemWithMixedPhp80.php @@ -0,0 +1,107 @@ + false, 'optionalNullableMixed' => false]; + + /** + * @param mixed|null $mandatoryNullableMixed + */ + public function __construct(private mixed $mandatoryMixed, private mixed $mandatoryNullableMixed) + { + } + + public function setOptionalMixed(mixed $optionalMixed): self + { + $this->optionalMixed = $optionalMixed; + $this->optionalPropertyChanged['optionalMixed'] = true; + + return $this; + } + + /** + * @param mixed|null $optionalNullableMixed + */ + public function setOptionalNullableMixed(mixed $optionalNullableMixed): self + { + $this->optionalNullableMixed = $optionalNullableMixed; + $this->optionalPropertyChanged['optionalNullableMixed'] = true; + + return $this; + } + + public function hasOptionalMixed(): bool + { + return $this->optionalPropertyChanged['optionalMixed']; + } + + public function hasOptionalNullableMixed(): bool + { + return $this->optionalPropertyChanged['optionalNullableMixed']; + } + + public function getMandatoryMixed(): mixed + { + return $this->mandatoryMixed; + } + + /** + * @return mixed|null + */ + public function getOptionalMixed() + { + return $this->optionalMixed; + } + + /** + * @return mixed|null + */ + public function getOptionalNullableMixed() + { + return $this->optionalNullableMixed; + } + + /** + * @return mixed|null + */ + public function getMandatoryNullableMixed() + { + return $this->mandatoryNullableMixed; + } + + public function toArray(): array + { + $fields = []; + $fields['mandatoryMixed'] = $this->mandatoryMixed; + if ($this->hasOptionalMixed()) { + $fields['optionalMixed'] = $this->optionalMixed; + } + if ($this->hasOptionalNullableMixed()) { + $fields['optionalNullableMixed'] = $this->optionalNullableMixed; + } + $fields['mandatoryNullableMixed'] = $this->mandatoryNullableMixed; + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ItemWithMixedPhp81.php b/test/suite/functional/Generator/Schema/ItemWithMixedPhp81.php new file mode 100644 index 0000000..1572e4e --- /dev/null +++ b/test/suite/functional/Generator/Schema/ItemWithMixedPhp81.php @@ -0,0 +1,107 @@ + false, 'optionalNullableMixed' => false]; + + /** + * @param mixed|null $mandatoryNullableMixed + */ + public function __construct(private readonly mixed $mandatoryMixed, private readonly mixed $mandatoryNullableMixed) + { + } + + public function setOptionalMixed(mixed $optionalMixed): self + { + $this->optionalMixed = $optionalMixed; + $this->optionalPropertyChanged['optionalMixed'] = true; + + return $this; + } + + /** + * @param mixed|null $optionalNullableMixed + */ + public function setOptionalNullableMixed(mixed $optionalNullableMixed): self + { + $this->optionalNullableMixed = $optionalNullableMixed; + $this->optionalPropertyChanged['optionalNullableMixed'] = true; + + return $this; + } + + public function hasOptionalMixed(): bool + { + return $this->optionalPropertyChanged['optionalMixed']; + } + + public function hasOptionalNullableMixed(): bool + { + return $this->optionalPropertyChanged['optionalNullableMixed']; + } + + public function getMandatoryMixed(): mixed + { + return $this->mandatoryMixed; + } + + /** + * @return mixed|null + */ + public function getOptionalMixed() + { + return $this->optionalMixed; + } + + /** + * @return mixed|null + */ + public function getOptionalNullableMixed() + { + return $this->optionalNullableMixed; + } + + /** + * @return mixed|null + */ + public function getMandatoryNullableMixed() + { + return $this->mandatoryNullableMixed; + } + + public function toArray(): array + { + $fields = []; + $fields['mandatoryMixed'] = $this->mandatoryMixed; + if ($this->hasOptionalMixed()) { + $fields['optionalMixed'] = $this->optionalMixed; + } + if ($this->hasOptionalNullableMixed()) { + $fields['optionalNullableMixed'] = $this->optionalNullableMixed; + } + $fields['mandatoryNullableMixed'] = $this->mandatoryNullableMixed; + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/ListEnum.php b/test/suite/functional/Generator/Schema/ListEnum.php new file mode 100644 index 0000000..5636ae6 --- /dev/null +++ b/test/suite/functional/Generator/Schema/ListEnum.php @@ -0,0 +1,18 @@ + false]; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function setList(string $list): self + { + $this->list = $list; + $this->optionalPropertyChanged['list'] = true; + + return $this; + } + + public function hasList(): bool + { + return $this->optionalPropertyChanged['list']; + } + + public function getId(): int + { + return $this->id; + } + + public function getList(): ?string + { + return $this->list; + } + + public function toArray(): array + { + $fields = []; + $fields['id'] = $this->id; + if ($this->hasList()) { + $fields['list'] = $this->list; + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/MatchSchemaPhp81.php b/test/suite/functional/Generator/Schema/MatchSchemaPhp81.php new file mode 100644 index 0000000..635a595 --- /dev/null +++ b/test/suite/functional/Generator/Schema/MatchSchemaPhp81.php @@ -0,0 +1,63 @@ + false]; + + public function __construct(private readonly int $id) + { + } + + public function setList(ListEnum $list): self + { + $this->list = $list; + $this->optionalPropertyChanged['list'] = true; + + return $this; + } + + public function hasList(): bool + { + return $this->optionalPropertyChanged['list']; + } + + public function getId(): int + { + return $this->id; + } + + public function getList(): ?ListEnum + { + return $this->list; + } + + public function toArray(): array + { + $fields = []; + $fields['id'] = $this->id; + if ($this->hasList()) { + $fields['list'] = $this->list->value; + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/NullableAllOfReference74.php b/test/suite/functional/Generator/Schema/NullableAllOfReference74.php new file mode 100644 index 0000000..c571d31 --- /dev/null +++ b/test/suite/functional/Generator/Schema/NullableAllOfReference74.php @@ -0,0 +1,75 @@ + false]; + + public function __construct(?Membership $membership, Subscription $subscription) + { + $this->membership = $membership; + $this->subscription = $subscription; + } + + public function setFailedCondition(?FailedCondition $failedCondition): self + { + $this->failedCondition = $failedCondition; + $this->optionalPropertyChanged['failedCondition'] = true; + + return $this; + } + + public function hasFailedCondition(): bool + { + return $this->optionalPropertyChanged['failedCondition']; + } + + public function getMembership(): ?Membership + { + return $this->membership; + } + + public function getSubscription(): Subscription + { + return $this->subscription; + } + + public function getFailedCondition(): ?FailedCondition + { + return $this->failedCondition; + } + + public function toArray(): array + { + $fields = []; + $fields['membership'] = $this->membership !== null ? $this->membership->toArray() : null; + $fields['subscription'] = $this->subscription->toArray(); + if ($this->hasFailedCondition()) { + $fields['failedCondition'] = $this->failedCondition !== null ? $this->failedCondition->toArray() : null; + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/NullableAllOfReference81.php b/test/suite/functional/Generator/Schema/NullableAllOfReference81.php new file mode 100644 index 0000000..5ac7441 --- /dev/null +++ b/test/suite/functional/Generator/Schema/NullableAllOfReference81.php @@ -0,0 +1,69 @@ + false]; + + public function __construct(private readonly ?Membership $membership, private readonly Subscription $subscription) + { + } + + public function setFailedCondition(?FailedCondition $failedCondition): self + { + $this->failedCondition = $failedCondition; + $this->optionalPropertyChanged['failedCondition'] = true; + + return $this; + } + + public function hasFailedCondition(): bool + { + return $this->optionalPropertyChanged['failedCondition']; + } + + public function getMembership(): ?Membership + { + return $this->membership; + } + + public function getSubscription(): Subscription + { + return $this->subscription; + } + + public function getFailedCondition(): ?FailedCondition + { + return $this->failedCondition; + } + + public function toArray(): array + { + $fields = []; + $fields['membership'] = $this->membership?->toArray(); + $fields['subscription'] = $this->subscription->toArray(); + if ($this->hasFailedCondition()) { + $fields['failedCondition'] = $this->failedCondition?->toArray(); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/OperatorEnum.php b/test/suite/functional/Generator/Schema/OperatorEnum.php new file mode 100644 index 0000000..68b430a --- /dev/null +++ b/test/suite/functional/Generator/Schema/OperatorEnum.php @@ -0,0 +1,18 @@ + false]; + + public function setHotDeal(HotDeal2 $hotDeal): self + { + $this->hotDeal = $hotDeal; + $this->optionalPropertyChanged['hotDeal'] = true; + + return $this; + } + + public function hasHotDeal(): bool + { + return $this->optionalPropertyChanged['hotDeal']; + } + + public function getHotDeal(): ?HotDeal2 + { + return $this->hotDeal; + } + + public function toArray(): array + { + $fields = []; + if ($this->hasHotDeal()) { + $fields['hotDeal'] = $this->hotDeal->toArray(); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/Tournament.php b/test/suite/functional/Generator/Schema/Tournament.php new file mode 100644 index 0000000..65bb84e --- /dev/null +++ b/test/suite/functional/Generator/Schema/Tournament.php @@ -0,0 +1,63 @@ + false]; + + public function __construct(private readonly MatchCollection $matches) + { + } + + public function setClass(ClassSchema $class): self + { + $this->class = $class; + $this->optionalPropertyChanged['class'] = true; + + return $this; + } + + public function hasClass(): bool + { + return $this->optionalPropertyChanged['class']; + } + + public function getMatches(): MatchCollection + { + return $this->matches; + } + + public function getClass(): ?ClassSchema + { + return $this->class; + } + + public function toArray(): array + { + $fields = []; + $fields['matches'] = $this->matches->toArray(); + if ($this->hasClass()) { + $fields['class'] = $this->class->toArray(); + } + + return $fields; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/test/suite/functional/Generator/Schema/classNameCollisions.yaml b/test/suite/functional/Generator/Schema/classNameCollisions.yaml new file mode 100644 index 0000000..81733a9 --- /dev/null +++ b/test/suite/functional/Generator/Schema/classNameCollisions.yaml @@ -0,0 +1,144 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Class name collisions Test API +paths: + '/v1/hosts': + get: + operationId: findHosts + parameters: + - name: callStatus + in: query + schema: + type: string + enum: + - in_progress + - none + - name: status + in: query + schema: + type: string + enum: + - online + - offline + - name: criterion_status_eq + in: query + schema: + type: string + enum: + - pending + - activated + responses: + '204': + description: No content + '/v1/calls/{callId}': + get: + operationId: getCall + parameters: + - name: callId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Call' + '/v1/host-stats': + get: + operationId: findHostStats + parameters: + - name: status + in: query + schema: + type: string + enum: + - online + - offline + - name: criterion_status_eq + in: query + schema: + type: string + enum: + - expiring + - expired + responses: + '204': + description: No content + '/v1/sections/{sectionId}': + get: + operationId: getSection + parameters: + - name: sectionId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Section' + '/v1/pages/{pageId}': + get: + operationId: getPage + parameters: + - name: pageId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PageItem' +components: + schemas: + Call: + type: object + required: + - callId + - status + properties: + callId: + type: integer + status: + type: string + enum: + - created + - started + - finished + HotDeal: + type: object + required: + - isActive + properties: + isActive: + type: boolean + discountedPrice: + type: number + nullable: true + Section: + type: object + properties: + hotDeal: + $ref: '#/components/schemas/HotDeal' + PageItem: + allOf: + - type: object + properties: + hotDeal: + type: object + required: + - discountedPrice + properties: + discountedPrice: + type: number diff --git a/test/suite/functional/Generator/Schema/itemWithMixed.yaml b/test/suite/functional/Generator/Schema/itemWithMixed.yaml new file mode 100644 index 0000000..d2d7707 --- /dev/null +++ b/test/suite/functional/Generator/Schema/itemWithMixed.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Untyped (mixed) properties Test API +paths: + '/v1/items': + get: + operationId: getItems + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ItemWithMixed' +components: + schemas: + ItemWithMixed: + type: object + required: + - mandatoryMixed + - mandatoryNullableMixed + properties: + mandatoryMixed: {} + optionalMixed: {} + optionalNullableMixed: + nullable: true + mandatoryNullableMixed: + nullable: true diff --git a/test/suite/functional/Generator/Schema/reservedWords.yaml b/test/suite/functional/Generator/Schema/reservedWords.yaml new file mode 100644 index 0000000..34af400 --- /dev/null +++ b/test/suite/functional/Generator/Schema/reservedWords.yaml @@ -0,0 +1,106 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Reserved word schema names Test API +paths: + '/v1/matches': + get: + operationId: findMatches + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Match' + '/v1/matches/{matchId}': + get: + operationId: getMatch + parameters: + - name: matchId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Match' + '/v1/tournaments/{tournamentId}': + get: + operationId: getTournament + parameters: + - name: tournamentId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Tournament' + '/v1/games/{gameId}': + get: + operationId: getGame + parameters: + - name: gameId + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Game' +components: + schemas: + Game: + oneOf: + - $ref: '#/components/schemas/Match' + - $ref: '#/components/schemas/Tournament' + discriminator: + propertyName: type + mapping: + match: '#/components/schemas/Match' + tournament: '#/components/schemas/Tournament' + List: + type: string + enum: + - group + - knockout + - class + Match: + type: object + required: + - id + properties: + id: + type: integer + list: + $ref: '#/components/schemas/List' + Tournament: + allOf: + - type: object + required: + - matches + properties: + matches: + type: array + items: + $ref: '#/components/schemas/Match' + class: + type: object + properties: + name: + type: string diff --git a/test/suite/functional/Generator/SchemaCollection/MatchCollection.php b/test/suite/functional/Generator/SchemaCollection/MatchCollection.php new file mode 100644 index 0000000..e460d8a --- /dev/null +++ b/test/suite/functional/Generator/SchemaCollection/MatchCollection.php @@ -0,0 +1,67 @@ +items = $items; + } + + public function toArray(): array + { + $return = []; + foreach ($this->items as $item) { + $return[] = $item->toArray(); + } + + return $return; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * @return MatchSchema[] + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + public function count(): int + { + return count($this->items); + } + + public function first(): ?MatchSchema + { + $first = reset($this->items); + if ($first === false) { + return null; + } + + return $first; + } +} diff --git a/test/suite/functional/Generator/SchemaCollection/ShowSubscriberCollection74.php b/test/suite/functional/Generator/SchemaCollection/ShowSubscriberCollection74.php new file mode 100644 index 0000000..f6e84c7 --- /dev/null +++ b/test/suite/functional/Generator/SchemaCollection/ShowSubscriberCollection74.php @@ -0,0 +1,67 @@ +items = $items; + } + + public function toArray(): array + { + $return = []; + foreach ($this->items as $item) { + $return[] = $item->toArray(); + } + + return $return; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * @return ShowSubscriber[] + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + public function count(): int + { + return count($this->items); + } + + public function first(): ?ShowSubscriber + { + $first = reset($this->items); + if ($first === false) { + return null; + } + + return $first; + } +} diff --git a/test/suite/functional/Generator/SchemaCollectionGeneratorTest.php b/test/suite/functional/Generator/SchemaCollectionGeneratorTest.php index 419bf8e..b1b7041 100644 --- a/test/suite/functional/Generator/SchemaCollectionGeneratorTest.php +++ b/test/suite/functional/Generator/SchemaCollectionGeneratorTest.php @@ -47,6 +47,18 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ItemCollection', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'Collection nested in an array of arrays of objects with PHP 7.4' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/SchemaCollection/ShowSubscriberCollection74.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ShowSubscriberCollection', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Collection of a schema named after a reserved word' => [ + '/Schema/reservedWords.yaml', + '/SchemaCollection/MatchCollection.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\MatchCollection', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], ]; } diff --git a/test/suite/functional/Generator/SchemaGeneratorTest.php b/test/suite/functional/Generator/SchemaGeneratorTest.php index 8d2b217..a28d637 100644 --- a/test/suite/functional/Generator/SchemaGeneratorTest.php +++ b/test/suite/functional/Generator/SchemaGeneratorTest.php @@ -82,6 +82,102 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ItemWithArraysOfEnumProperties', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), ], + 'Nullable allOf reference with PHP 7.4' => [ + '/SchemaMapper/nullableAllOfReference.yaml', + '/Schema/NullableAllOfReference74.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\Profile', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Nullable allOf reference with PHP 8.1' => [ + '/SchemaMapper/nullableAllOfReference.yaml', + '/Schema/NullableAllOfReference81.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\Profile', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Array of arrays of objects with PHP 7.0' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/Schema/ArrayOfArraysOfObjects70.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribers', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP70)->build(), + ], + 'Array of arrays of objects with PHP 7.4' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/Schema/ArrayOfArraysOfObjects74.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribers', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Array of arrays of objects with PHP 8.1' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/Schema/ArrayOfArraysOfObjects81.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribers', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Untyped (mixed) properties with PHP 7.4' => [ + '/Schema/itemWithMixed.yaml', + '/Schema/ItemWithMixedPhp74.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ItemWithMixed', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Untyped (mixed) properties with PHP 8.0' => [ + '/Schema/itemWithMixed.yaml', + '/Schema/ItemWithMixedPhp80.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ItemWithMixed', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP80)->build(), + ], + 'Untyped (mixed) properties with PHP 8.1' => [ + '/Schema/itemWithMixed.yaml', + '/Schema/ItemWithMixedPhp81.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ItemWithMixed', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Schema named after a reserved word with PHP 7.4' => [ + '/Schema/reservedWords.yaml', + '/Schema/MatchSchemaPhp74.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\MatchSchema', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Schema named after a reserved word with PHP 8.1' => [ + '/Schema/reservedWords.yaml', + '/Schema/MatchSchemaPhp81.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\MatchSchema', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Schema referencing schemas named after reserved words' => [ + '/Schema/reservedWords.yaml', + '/Schema/Tournament.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\Tournament', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Inline schema with a reserved word fallback name' => [ + '/Schema/reservedWords.yaml', + '/Schema/ClassSchema.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\ClassSchema', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Schema with an enum property whose class name collides' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/CallWithCollidingEnum.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\Call', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Component schema keeps its name when an inline schema collides with it' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/HotDeal.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\HotDeal', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Inline schema colliding with a component schema gets a numeric suffix' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/HotDeal2.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\HotDeal2', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Schema with an inline property whose class name collides' => [ + '/Schema/classNameCollisions.yaml', + '/Schema/PageItem.php', + self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\PageItem', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], ]; } diff --git a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper74.php b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper74.php index e326539..86103fb 100644 --- a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper74.php +++ b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper74.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -24,6 +25,9 @@ public function __construct(AnimalResponseMapper $animalResponseMapper, MachineR $this->machineResponseMapper = $machineResponseMapper; } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -38,11 +42,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper80.php b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper80.php index 5a480e6..dcea7ae 100644 --- a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper80.php +++ b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper80.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -18,6 +19,9 @@ public function __construct(private AnimalResponseMapper $animalResponseMapper, { } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -32,11 +36,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper81.php b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper81.php index 200536d..5fd57ba 100644 --- a/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper81.php +++ b/test/suite/functional/Generator/SchemaMapper/AnyOfResponseBodyMapper81.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -18,6 +19,9 @@ public function __construct(private readonly AnimalResponseMapper $animalRespons { } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -32,11 +36,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper70.php b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper70.php new file mode 100644 index 0000000..9e2954a --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper70.php @@ -0,0 +1,57 @@ +showSubscriberCollectionMapper = $showSubscriberCollectionMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): ShowSubscribers + { + $missingFields = implode(', ', array_diff(['data'], array_keys($payload))); + if (!empty($missingFields)) { + throw new UnexpectedResponseBodyException('Required attributes for `ShowSubscribers` missing in the response body: ' . $missingFields); + } + $schema = new ShowSubscribers(array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $payload['data'])); + if (array_key_exists('previousData', $payload)) { + $schema->setPreviousData($payload['previousData'] !== null ? array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $payload['previousData']) : null); + } + if (isset($payload['groups'])) { + $schema->setGroups(array_map(function (array $item): array { + return array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $item); + }, $payload['groups'])); + } + if (empty($schema->toArray())) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper74.php b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper74.php new file mode 100644 index 0000000..ef4969e --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper74.php @@ -0,0 +1,56 @@ +showSubscriberCollectionMapper = $showSubscriberCollectionMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): ShowSubscribers + { + $missingFields = implode(', ', array_diff(['data'], array_keys($payload))); + if (!empty($missingFields)) { + throw new UnexpectedResponseBodyException('Required attributes for `ShowSubscribers` missing in the response body: ' . $missingFields); + } + $schema = new ShowSubscribers(array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $payload['data'])); + if (array_key_exists('previousData', $payload)) { + $schema->setPreviousData($payload['previousData'] !== null ? array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $payload['previousData']) : null); + } + if (isset($payload['groups'])) { + $schema->setGroups(array_map(function (array $item): array { + return array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $item); + }, $payload['groups'])); + } + if (empty($schema->toArray())) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper81.php b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper81.php new file mode 100644 index 0000000..b070cfd --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/ArrayOfArraysOfObjectsMapper81.php @@ -0,0 +1,53 @@ +showSubscriberCollectionMapper->toSchema($item); + }, $payload['data'])); + if (array_key_exists('previousData', $payload)) { + $schema->setPreviousData($payload['previousData'] !== null ? array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $payload['previousData']) : null); + } + if (isset($payload['groups'])) { + $schema->setGroups(array_map(function (array $item): array { + return array_map(function (array $item): ShowSubscriberCollection { + return $this->showSubscriberCollectionMapper->toSchema($item); + }, $item); + }, $payload['groups'])); + } + if (empty($schema->toArray())) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/GameMapper81.php b/test/suite/functional/Generator/SchemaMapper/GameMapper81.php new file mode 100644 index 0000000..7dea734 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/GameMapper81.php @@ -0,0 +1,45 @@ +setMatch($this->matchSchemaMapper->toSchema($payload)); + + break; + case 'tournament': + $schema->setTournament($this->tournamentMapper->toSchema($payload)); + + break; + default: + throw new UnexpectedResponseBodyException('Unknown `type` discriminator value for `Game` in the response body'); + } + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper74.php b/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper74.php new file mode 100644 index 0000000..d2f9781 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper74.php @@ -0,0 +1,47 @@ +membershipMapper = $membershipMapper; + $this->subscriptionMapper = $subscriptionMapper; + $this->failedConditionMapper = $failedConditionMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): Profile + { + $missingFields = implode(', ', array_diff(['membership', 'subscription'], array_keys($payload))); + if (!empty($missingFields)) { + throw new UnexpectedResponseBodyException('Required attributes for `Profile` missing in the response body: ' . $missingFields); + } + $schema = new Profile($payload['membership'] !== null ? $this->membershipMapper->toSchema($payload['membership']) : null, $this->subscriptionMapper->toSchema($payload['subscription'])); + if (array_key_exists('failedCondition', $payload)) { + $schema->setFailedCondition($payload['failedCondition'] !== null ? $this->failedConditionMapper->toSchema($payload['failedCondition']) : null); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper81.php b/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper81.php new file mode 100644 index 0000000..cefc536 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/NullableAllOfReferenceMapper81.php @@ -0,0 +1,38 @@ +membershipMapper->toSchema($payload['membership']) : null, $this->subscriptionMapper->toSchema($payload['subscription'])); + if (array_key_exists('failedCondition', $payload)) { + $schema->setFailedCondition($payload['failedCondition'] !== null ? $this->failedConditionMapper->toSchema($payload['failedCondition']) : null); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper74.php b/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper74.php new file mode 100644 index 0000000..516b438 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper74.php @@ -0,0 +1,43 @@ +setOptionalType($payload['optionalType']); + } + if (array_key_exists('optionalNullableType', $payload)) { + $schema->setOptionalNullableType($payload['optionalNullableType']); + } + if (array_key_exists('category', $payload)) { + $schema->setCategory($payload['category']); + } + if (empty($schema->toArray())) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper81.php b/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper81.php new file mode 100644 index 0000000..0212fc6 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/NullableEnumMapper81.php @@ -0,0 +1,47 @@ +setOptionalType(CookieOptionalTypeEnum::from($payload['optionalType'])); + } + if (array_key_exists('optionalNullableType', $payload)) { + $schema->setOptionalNullableType($payload['optionalNullableType'] !== null ? CookieOptionalNullableTypeEnum::from($payload['optionalNullableType']) : null); + } + if (array_key_exists('category', $payload)) { + $schema->setCategory($payload['category'] !== null ? CookieCategoryEnum::from($payload['category']) : null); + } + if (empty($schema->toArray())) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper74.php b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper74.php new file mode 100644 index 0000000..376ed33 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper74.php @@ -0,0 +1,63 @@ +catMapper = $catMapper; + $this->dogMapper = $dogMapper; + $this->lizardMapper = $lizardMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): Pet + { + $schema = new Pet(); + if (array_key_exists('petType', $payload)) { + switch ($payload['petType']) { + case 'cat': + $schema->setCat($this->catMapper->toSchema($payload)); + + break; + case 'kitten': + $schema->setCat($this->catMapper->toSchema($payload)); + + break; + case 'dog': + $schema->setDog($this->dogMapper->toSchema($payload)); + + break; + case 'Lizard': + case 'lizard': + $schema->setLizard($this->lizardMapper->toSchema($payload)); + + break; + default: + throw new UnexpectedResponseBodyException('Unknown `petType` discriminator value for `Pet` in the response body'); + } + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper81.php b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper81.php new file mode 100644 index 0000000..b258c11 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorPartialMappingMapper81.php @@ -0,0 +1,54 @@ +setCat($this->catMapper->toSchema($payload)); + + break; + case 'kitten': + $schema->setCat($this->catMapper->toSchema($payload)); + + break; + case 'dog': + $schema->setDog($this->dogMapper->toSchema($payload)); + + break; + case 'Lizard': + case 'lizard': + $schema->setLizard($this->lizardMapper->toSchema($payload)); + + break; + default: + throw new UnexpectedResponseBodyException('Unknown `petType` discriminator value for `Pet` in the response body'); + } + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper74.php b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper74.php new file mode 100644 index 0000000..94516a8 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper74.php @@ -0,0 +1,53 @@ +animalMapper = $animalMapper; + $this->machineMapper = $machineMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): GetExampleResponseBody + { + $schema = new GetExampleResponseBody(); + if (array_key_exists('objectType', $payload)) { + switch ($payload['objectType']) { + case 'Animal': + case 'animal': + $schema->setAnimal($this->animalMapper->toSchema($payload)); + + break; + case 'Machine': + case 'machine': + $schema->setMachine($this->machineMapper->toSchema($payload)); + + break; + default: + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); + } + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper81.php b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper81.php index 156beb8..622e5a1 100644 --- a/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper81.php +++ b/test/suite/functional/Generator/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper81.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -18,13 +19,27 @@ public function __construct(private readonly AnimalMapper $animalMapper, private { } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); if (array_key_exists('objectType', $payload)) { - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); + switch ($payload['objectType']) { + case 'Animal': + case 'animal': + $schema->setAnimal($this->animalMapper->toSchema($payload)); + + break; + case 'Machine': + case 'machine': + $schema->setMachine($this->machineMapper->toSchema($payload)); + + break; + default: + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); + } } return $schema; diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper74.php b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper74.php index e326539..86103fb 100644 --- a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper74.php +++ b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper74.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -24,6 +25,9 @@ public function __construct(AnimalResponseMapper $animalResponseMapper, MachineR $this->machineResponseMapper = $machineResponseMapper; } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -38,11 +42,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper80.php b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper80.php index 5a480e6..dcea7ae 100644 --- a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper80.php +++ b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper80.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -18,6 +19,9 @@ public function __construct(private AnimalResponseMapper $animalResponseMapper, { } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -32,11 +36,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper81.php b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper81.php index 200536d..5fd57ba 100644 --- a/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper81.php +++ b/test/suite/functional/Generator/SchemaMapper/OneOfResponseBodyMapper81.php @@ -10,6 +10,7 @@ namespace Test\Schema\Mapper; +use DoclerLabs\ApiClientException\UnexpectedResponseBodyException; use Test\Schema\GetExampleResponseBody; class GetExampleResponseBodyMapper implements SchemaMapperInterface @@ -18,6 +19,9 @@ public function __construct(private readonly AnimalResponseMapper $animalRespons { } + /** + * @throws UnexpectedResponseBodyException + */ public function toSchema(array $payload): GetExampleResponseBody { $schema = new GetExampleResponseBody(); @@ -32,11 +36,7 @@ public function toSchema(array $payload): GetExampleResponseBody break; default: - $methodName = 'set' . ucfirst($payload['objectType']); - $mapperName = $payload['objectType'] . 'Mapper'; - $schema->$methodName($this->$mapperName->toSchema($payload)); - - break; + throw new UnexpectedResponseBodyException('Unknown `objectType` discriminator value for `GetExampleResponseBody` in the response body'); } } diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper74.php b/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper74.php new file mode 100644 index 0000000..b930b44 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper74.php @@ -0,0 +1,71 @@ +performerUploaderMapper = $performerUploaderMapper; + $this->memberUploaderMapper = $memberUploaderMapper; + $this->systemUploaderMapper = $systemUploaderMapper; + $this->botUploaderMapper = $botUploaderMapper; + } + + /** + * @throws UnexpectedResponseBodyException + */ + public function toSchema(array $payload): Uploader + { + $schema = new Uploader(); + $matches = 0; + + try { + $schema->setPerformerUploader($this->performerUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException $exception) { + } + + try { + $schema->setMemberUploader($this->memberUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException $exception) { + } + + try { + $schema->setSystemUploader($this->systemUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException $exception) { + } + + try { + $schema->setBotUploader($this->botUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException $exception) { + } + if ($matches !== 1) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper81.php b/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper81.php new file mode 100644 index 0000000..6b14575 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper81.php @@ -0,0 +1,60 @@ +setPerformerUploader($this->performerUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException|ValueError $exception) { + } + + try { + $schema->setMemberUploader($this->memberUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException|ValueError $exception) { + } + + try { + $schema->setSystemUploader($this->systemUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException $exception) { + } + + try { + $schema->setBotUploader($this->botUploaderMapper->toSchema($payload)); + $matches = $matches + 1; + } catch (UnexpectedResponseBodyException|ValueError $exception) { + } + if ($matches !== 1) { + throw new UnexpectedResponseBodyException(); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/PageItemMapper74.php b/test/suite/functional/Generator/SchemaMapper/PageItemMapper74.php new file mode 100644 index 0000000..b29c5b1 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/PageItemMapper74.php @@ -0,0 +1,33 @@ +hotDeal2Mapper = $hotDeal2Mapper; + } + + public function toSchema(array $payload): PageItem + { + $schema = new PageItem(); + if (isset($payload['hotDeal'])) { + $schema->setHotDeal($this->hotDeal2Mapper->toSchema($payload['hotDeal'])); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/ShowSubscriberCollectionMapper74.php b/test/suite/functional/Generator/SchemaMapper/ShowSubscriberCollectionMapper74.php new file mode 100644 index 0000000..0ea51e4 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/ShowSubscriberCollectionMapper74.php @@ -0,0 +1,33 @@ +showSubscriberMapper = $showSubscriberMapper; + } + + public function toSchema(array $payload): ShowSubscriberCollection + { + $items = []; + foreach ($payload as $payloadItem) { + $items[] = $this->showSubscriberMapper->toSchema($payloadItem); + } + + return new ShowSubscriberCollection(...$items); + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/TournamentMapper81.php b/test/suite/functional/Generator/SchemaMapper/TournamentMapper81.php new file mode 100644 index 0000000..cf3f53c --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/TournamentMapper81.php @@ -0,0 +1,38 @@ +matchCollectionMapper->toSchema($payload['matches'])); + if (isset($payload['class'])) { + $schema->setClass($this->classSchemaMapper->toSchema($payload['class'])); + } + + return $schema; + } +} diff --git a/test/suite/functional/Generator/SchemaMapper/arrayOfArraysOfObjects.yaml b/test/suite/functional/Generator/SchemaMapper/arrayOfArraysOfObjects.yaml new file mode 100644 index 0000000..ef65f37 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/arrayOfArraysOfObjects.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.0 +info: + title: Arrays of arrays of objects + version: 1.0.0 + +paths: + /shows/{showId}/subscribers: + get: + operationId: findShowSubscribers + parameters: + - in: path + name: showId + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ShowSubscribers' + +components: + schemas: + ShowSubscribers: + type: object + required: + - data + properties: + data: + type: array + items: + type: array + items: + $ref: '#/components/schemas/ShowSubscriber' + previousData: + type: array + nullable: true + items: + type: array + items: + $ref: '#/components/schemas/ShowSubscriber' + groups: + type: array + items: + type: array + items: + type: array + items: + $ref: '#/components/schemas/ShowSubscriber' + ShowSubscriber: + type: object + required: + - id + properties: + id: + type: integer diff --git a/test/suite/functional/Generator/SchemaMapper/nullableAllOfReference.yaml b/test/suite/functional/Generator/SchemaMapper/nullableAllOfReference.yaml new file mode 100644 index 0000000..5960085 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/nullableAllOfReference.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: Nullable references expressed with allOf + version: 1.0.0 + +paths: + /profile: + get: + operationId: getProfile + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Profile' + +components: + schemas: + Profile: + type: object + required: + - membership + - subscription + properties: + membership: + nullable: true + allOf: + - $ref: '#/components/schemas/Membership' + subscription: + allOf: + - $ref: '#/components/schemas/Membership' + failedCondition: + nullable: true + allOf: + - $ref: '#/components/schemas/Membership' + Membership: + type: object + required: + - id + properties: + id: + type: integer diff --git a/test/suite/functional/Generator/SchemaMapper/nullableEnum.yaml b/test/suite/functional/Generator/SchemaMapper/nullableEnum.yaml new file mode 100644 index 0000000..d988916 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/nullableEnum.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.0 +info: + title: Nullable enum properties + version: 1.0.0 + +paths: + /cookies/{cookieId}: + get: + operationId: getCookie + parameters: + - in: path + name: cookieId + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Cookie' + +components: + schemas: + Cookie: + type: object + required: + - name + - mandatoryNullableType + properties: + name: + type: string + mandatoryNullableType: + type: string + enum: + - session + - persistent + nullable: true + optionalType: + type: string + enum: + - session + - persistent + optionalNullableType: + type: string + enum: + - session + - persistent + nullable: true + category: + $ref: '#/components/schemas/CookieCategory' + CookieCategory: + type: string + enum: + - analytical + - essential + nullable: true diff --git a/test/suite/functional/Generator/SchemaMapper/oneOfDiscriminatorPartialMapping.yaml b/test/suite/functional/Generator/SchemaMapper/oneOfDiscriminatorPartialMapping.yaml new file mode 100644 index 0000000..a396033 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/oneOfDiscriminatorPartialMapping.yaml @@ -0,0 +1,66 @@ +openapi: 3.0.0 +info: + title: OneOf with a discriminator mapping that covers only some of the alternatives + version: 1.0.0 + +paths: + /pets/{petId}: + get: + operationId: getPet + parameters: + - in: path + name: petId + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + +components: + schemas: + Pet: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Lizard' + discriminator: + propertyName: petType + mapping: + cat: '#/components/schemas/Cat' + kitten: '#/components/schemas/Cat' + dog: Dog + Cat: + type: object + required: + - petType + - name + properties: + petType: + type: string + name: + type: string + Dog: + type: object + required: + - petType + - bark + properties: + petType: + type: string + bark: + type: string + Lizard: + type: object + required: + - petType + - lovesRocks + properties: + petType: + type: string + lovesRocks: + type: boolean diff --git a/test/suite/functional/Generator/SchemaMapper/oneOfWithoutDiscriminatorEnum.yaml b/test/suite/functional/Generator/SchemaMapper/oneOfWithoutDiscriminatorEnum.yaml new file mode 100644 index 0000000..b10a9c9 --- /dev/null +++ b/test/suite/functional/Generator/SchemaMapper/oneOfWithoutDiscriminatorEnum.yaml @@ -0,0 +1,86 @@ +openapi: 3.0.0 +info: + title: OneOf without discriminator, alternatives told apart by an enum + version: 1.0.0 + +paths: + /attachments/{attachmentId}: + get: + operationId: getAttachment + parameters: + - in: path + name: attachmentId + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attachment' + +components: + schemas: + Attachment: + type: object + required: + - id + - uploader + properties: + id: + type: integer + uploader: + oneOf: + - $ref: '#/components/schemas/PerformerUploader' + - $ref: '#/components/schemas/MemberUploader' + - $ref: '#/components/schemas/SystemUploader' + - $ref: '#/components/schemas/BotUploader' + PerformerUploader: + type: object + required: + - type + - id + properties: + type: + type: string + enum: + - performer + id: + type: integer + MemberUploader: + type: object + required: + - type + - id + properties: + type: + type: string + enum: + - member + id: + type: integer + SystemUploader: + type: object + required: + - system + properties: + system: + type: boolean + BotUploader: + type: object + required: + - bot + properties: + bot: + $ref: '#/components/schemas/Bot' + Bot: + type: object + required: + - kind + properties: + kind: + type: string + enum: + - crawler diff --git a/test/suite/functional/Generator/SchemaMapperGeneratorTest.php b/test/suite/functional/Generator/SchemaMapperGeneratorTest.php index 0caa9c5..ea3ec11 100644 --- a/test/suite/functional/Generator/SchemaMapperGeneratorTest.php +++ b/test/suite/functional/Generator/SchemaMapperGeneratorTest.php @@ -106,12 +106,30 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'OneOf response with a discriminator but without mapping with php 7.4' => [ + '/SchemaMapper/oneOfDiscriminatorWithoutMapping.yaml', + '/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], 'OneOf response with a discriminator but without mapping with php 8.1' => [ '/SchemaMapper/oneOfDiscriminatorWithoutMapping.yaml', '/SchemaMapper/OneOfDiscriminatorWithoutMappingMapper81.php', self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'OneOf response with a discriminator mapping covering some alternatives with php 7.4' => [ + '/SchemaMapper/oneOfDiscriminatorPartialMapping.yaml', + '/SchemaMapper/OneOfDiscriminatorPartialMappingMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\PetMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'OneOf response with a discriminator mapping covering some alternatives with php 8.1' => [ + '/SchemaMapper/oneOfDiscriminatorPartialMapping.yaml', + '/SchemaMapper/OneOfDiscriminatorPartialMappingMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\PetMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], 'OneOf response without discriminator with php 7.4' => [ '/SchemaMapper/oneOfWithoutDiscriminator.yaml', '/SchemaMapper/OneOfResponseBodyMapperWithoutDiscriminator74.php', @@ -148,19 +166,19 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], - 'AnyOf response with php 7.4' => [ + 'AnyOf response without discriminator with php 7.4' => [ '/SchemaMapper/anyOfWithoutDiscriminator.yaml', '/SchemaMapper/AnyOfResponseBodyMapperWithoutDiscriminator74.php', self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), ], - 'AnyOf response with php 8.0' => [ + 'AnyOf response without discriminator with php 8.0' => [ '/SchemaMapper/anyOfWithoutDiscriminator.yaml', '/SchemaMapper/AnyOfResponseBodyMapperWithoutDiscriminator80.php', self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP80)->build(), ], - 'AnyOf response with php 8.1' => [ + 'AnyOf response without discriminator with php 8.1' => [ '/SchemaMapper/anyOfWithoutDiscriminator.yaml', '/SchemaMapper/AnyOfResponseBodyMapperWithoutDiscriminator81.php', self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GetExampleResponseBodyMapper', @@ -178,9 +196,104 @@ public function exampleProvider(): array self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ItemWithArraysOfEnumPropertiesMapper', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), ], + 'Nullable allOf reference with php 7.4' => [ + '/SchemaMapper/nullableAllOfReference.yaml', + '/SchemaMapper/NullableAllOfReferenceMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ProfileMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Nullable allOf reference with php 8.1' => [ + '/SchemaMapper/nullableAllOfReference.yaml', + '/SchemaMapper/NullableAllOfReferenceMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ProfileMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Nullable enums with php 7.4' => [ + '/SchemaMapper/nullableEnum.yaml', + '/SchemaMapper/NullableEnumMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\CookieMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Nullable enums with php 8.1' => [ + '/SchemaMapper/nullableEnum.yaml', + '/SchemaMapper/NullableEnumMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\CookieMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'OneOf response without discriminator told apart by enums with php 7.4' => [ + '/SchemaMapper/oneOfWithoutDiscriminatorEnum.yaml', + '/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\UploaderMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'OneOf response without discriminator told apart by enums with php 8.1' => [ + '/SchemaMapper/oneOfWithoutDiscriminatorEnum.yaml', + '/SchemaMapper/OneOfWithoutDiscriminatorEnumMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\UploaderMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Array of arrays of objects with php 7.0' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/SchemaMapper/ArrayOfArraysOfObjectsMapper70.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribersMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP70)->build(), + ], + 'Array of arrays of objects with php 7.4' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/SchemaMapper/ArrayOfArraysOfObjectsMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribersMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Array of arrays of objects with php 8.1' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/SchemaMapper/ArrayOfArraysOfObjectsMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ShowSubscribersMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Collection nested in an array of arrays of objects with php 7.4' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/SchemaMapper/ShowSubscriberCollectionMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ShowSubscriberCollectionMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], + 'Mapper referencing schemas named after reserved words' => [ + '/Schema/reservedWords.yaml', + '/SchemaMapper/TournamentMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\TournamentMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Discriminator mapping to a schema named after a reserved word' => [ + '/Schema/reservedWords.yaml', + '/SchemaMapper/GameMapper81.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\GameMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], + 'Mapper of a schema with an inline property whose class name collides' => [ + '/Schema/classNameCollisions.yaml', + '/SchemaMapper/PageItemMapper74.php', + self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\PageItemMapper', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP74)->build(), + ], ]; } + public function testArrayOfEnumsItemsGetNoMapper(): void + { + $this->setUpContainer(ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build()); + $specificationPath = __DIR__ . '/Schema/arrayOfEnums.yaml'; + $specification = $this->specificationParser->parse( + $this->specificationReader->read($specificationPath), + $specificationPath + ); + + $this->sut->generate($specification, $this->fileRegistry); + + self::assertSame( + [self::BASE_NAMESPACE . SchemaMapperGenerator::NAMESPACE_SUBPATH . '\\ItemWithArraysOfEnumPropertiesMapper'], + array_keys(iterator_to_array($this->fileRegistry)) + ); + } + protected function generatorClassName(): string { return SchemaMapperGenerator::class; diff --git a/test/suite/functional/Generator/ServiceProvider/ArrayOfArraysOfObjectsServiceProvider74.php b/test/suite/functional/Generator/ServiceProvider/ArrayOfArraysOfObjectsServiceProvider74.php new file mode 100644 index 0000000..dabf55c --- /dev/null +++ b/test/suite/functional/Generator/ServiceProvider/ArrayOfArraysOfObjectsServiceProvider74.php @@ -0,0 +1,51 @@ +add(new JsonContentTypeSerializer()); + }; + $container[QuerySerializer::class] = static function (): QuerySerializer { + return new QuerySerializer(); + }; + $container[ResponseHandler::class] = static function () use ($container): ResponseHandler { + return new ResponseHandler($container[BodySerializer::class], new ResponseExceptionFactory()); + }; + $container[RequestMapperInterface::class] = static function () use ($container): RequestMapperInterface { + return new GuzzleRequestMapper($container[BodySerializer::class], $container[QuerySerializer::class]); + }; + $container[ShowSubscribersMapper::class] = static function () use ($container): ShowSubscribersMapper { + return new ShowSubscribersMapper($container[ShowSubscriberCollectionMapper::class]); + }; + $container[ShowSubscriberCollectionMapper::class] = static function () use ($container): ShowSubscriberCollectionMapper { + return new ShowSubscriberCollectionMapper($container[ShowSubscriberMapper::class]); + }; + $container[ShowSubscriberMapper::class] = static function () use ($container): ShowSubscriberMapper { + return new ShowSubscriberMapper(); + }; + } +} diff --git a/test/suite/functional/Generator/ServiceProvider/ServiceProviderArrayOfEnumsPhp81.php b/test/suite/functional/Generator/ServiceProvider/ServiceProviderArrayOfEnumsPhp81.php new file mode 100644 index 0000000..7816ee3 --- /dev/null +++ b/test/suite/functional/Generator/ServiceProvider/ServiceProviderArrayOfEnumsPhp81.php @@ -0,0 +1,43 @@ +add(new JsonContentTypeSerializer()); + }; + $container[QuerySerializer::class] = static function (): QuerySerializer { + return new QuerySerializer(); + }; + $container[ResponseHandler::class] = static function () use ($container): ResponseHandler { + return new ResponseHandler($container[BodySerializer::class], new ResponseExceptionFactory()); + }; + $container[RequestMapperInterface::class] = static function () use ($container): RequestMapperInterface { + return new GuzzleRequestMapper($container[BodySerializer::class], $container[QuerySerializer::class]); + }; + $container[ItemWithArraysOfEnumPropertiesMapper::class] = static function () use ($container): ItemWithArraysOfEnumPropertiesMapper { + return new ItemWithArraysOfEnumPropertiesMapper(); + }; + } +} diff --git a/test/suite/functional/Generator/ServiceProviderGeneratorTest.php b/test/suite/functional/Generator/ServiceProviderGeneratorTest.php index 4bbf8ce..8967670 100644 --- a/test/suite/functional/Generator/ServiceProviderGeneratorTest.php +++ b/test/suite/functional/Generator/ServiceProviderGeneratorTest.php @@ -4,6 +4,7 @@ namespace DoclerLabs\ApiClientGenerator\Test\Functional\Generator; +use DoclerLabs\ApiClientGenerator\Ast\PhpVersion; use DoclerLabs\ApiClientGenerator\Generator\ServiceProviderGenerator; use DoclerLabs\ApiClientGenerator\Test\Functional\ConfigurationBuilder; @@ -27,6 +28,18 @@ public function exampleProvider(): array self::BASE_NAMESPACE . '\\ServiceProvider', ConfigurationBuilder::fake()->build(), ], + 'Array of arrays of objects' => [ + '/SchemaMapper/arrayOfArraysOfObjects.yaml', + '/ServiceProvider/ArrayOfArraysOfObjectsServiceProvider74.php', + self::BASE_NAMESPACE . '\\ServiceProvider', + ConfigurationBuilder::fake()->build(), + ], + 'Array of enums with PHP 8.1 (no mapper for the enum items)' => [ + '/Schema/arrayOfEnums.yaml', + '/ServiceProvider/ServiceProviderArrayOfEnumsPhp81.php', + self::BASE_NAMESPACE . '\\ServiceProvider', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), + ], ]; } diff --git a/test/suite/functional/Input/ClassNameCollisionResolverTest.php b/test/suite/functional/Input/ClassNameCollisionResolverTest.php new file mode 100644 index 0000000..a6f53b6 --- /dev/null +++ b/test/suite/functional/Input/ClassNameCollisionResolverTest.php @@ -0,0 +1,103 @@ +getContainerWith(ConfigurationBuilder::fake()->withPhpVersion($phpVersion)->build()); + + $warnings = []; + set_error_handler( + static function (int $level, string $message) use (&$warnings): bool { + if (str_starts_with($message, 'Class name')) { + $warnings[] = $message; + } + + return true; + }, + E_USER_WARNING + ); + + try { + $specification = $container[Parser::class]->parse( + $container[FileReader::class]->read(self::SPECIFICATION), + self::SPECIFICATION + ); + } finally { + restore_error_handler(); + } + + $fileRegistry = new PhpFileCollection(); + $container[EnumGenerator::class]->generate($specification, $fileRegistry); + $container[SchemaGenerator::class]->generate($specification, $fileRegistry); + $classNames = array_map( + static fn (string $className): string => substr($className, strlen('Test\\Schema\\')), + array_keys(iterator_to_array($fileRegistry)) + ); + sort($classNames); + + self::assertSame($expectedClassNames, $classNames); + self::assertSame($expectedWarnings, $warnings); + } + + public function phpVersionProvider(): array + { + $hotDealWarning = 'Class name HotDeal is shared by different schemas, HotDeal2 is used for the one in getPage. Consider giving the schemas distinct names.'; + + return [ + 'Enums and schemas with PHP 8.1' => [ + PhpVersion::VERSION_PHP81, + [ + 'Call', + 'CallStatus2Enum', + 'CallStatusEnum', + 'CriterionStatusEq2Enum', + 'CriterionStatusEqEnum', + 'HotDeal', + 'HotDeal2', + 'PageItem', + 'Section', + // identical definitions share their class + 'StatusEnum', + ], + [ + 'Class name CallStatusEnum is shared by different schemas, CallStatus2Enum is used for the one in getCall. Consider giving the schemas distinct names.', + 'Class name CriterionStatusEqEnum is shared by different schemas, CriterionStatusEq2Enum is used for the one in findHosts. Consider giving the schemas distinct names.', + $hotDealWarning, + ], + ], + 'Schemas with PHP 7.4, enums are constants' => [ + PhpVersion::VERSION_PHP74, + ['Call', 'HotDeal', 'HotDeal2', 'PageItem', 'Section'], + [$hotDealWarning], + ], + ]; + } +} diff --git a/test/suite/functional/Input/SpecificationTest.php b/test/suite/functional/Input/SpecificationTest.php index 3c129a9..183bc2c 100644 --- a/test/suite/functional/Input/SpecificationTest.php +++ b/test/suite/functional/Input/SpecificationTest.php @@ -269,6 +269,59 @@ public function testGetFromInfo(): void static::assertSame('License name', $specificationWithLicense->getLicenseName()); } + /** + * @dataProvider literalRequestBodyProvider + */ + public function testHasLiteralRequestBody(?array $requestBodySchema, bool $expectedResult): void + { + $operation = [ + 'operationId' => 'updateUser', + 'responses' => [ + '204' => [ + 'description' => 'No content', + ], + ], + ]; + if ($requestBodySchema !== null) { + $operation['requestBody'] = [ + 'content' => [ + 'application/json' => [ + 'schema' => $requestBodySchema, + ], + ], + ]; + } + $data = [ + 'openapi' => '3.0.0', + 'info' => [ + 'title' => 'Sample API', + 'version' => '1.0.0', + ], + 'paths' => [ + '/users' => [ + 'put' => $operation, + ], + ], + ]; + + $specification = $this->sut->parse($data, '/openapi.yaml'); + + static::assertSame($expectedResult, $specification->hasLiteralRequestBody()); + } + + public function literalRequestBodyProvider(): array + { + return [ + 'No request body' => [null, false], + 'Object body' => [['type' => 'object', 'properties' => ['name' => ['type' => 'string']]], false], + 'Array of objects' => [['type' => 'array', 'items' => ['type' => 'object', 'properties' => ['name' => ['type' => 'string']]]], false], + 'Untyped body' => [['description' => 'Anything'], false], + 'Integer body' => [['type' => 'integer'], true], + 'Nullable string body' => [['type' => 'string', 'nullable' => true], true], + 'Array of integers' => [['type' => 'array', 'items' => ['type' => 'integer']], true], + ]; + } + protected function setUp(): void { $container = $this->getContainerWith(ConfigurationBuilder::fake()->build()); diff --git a/test/suite/unit/Ast/Builder/CodeBuilderTest.php b/test/suite/unit/Ast/Builder/CodeBuilderTest.php index dd0a214..dcd7441 100644 --- a/test/suite/unit/Ast/Builder/CodeBuilderTest.php +++ b/test/suite/unit/Ast/Builder/CodeBuilderTest.php @@ -287,6 +287,39 @@ public function testTernary(): void self::assertEquals('true ? $left : null', $this->printer->prettyPrint([$ternary])); } + public function testThrowExpression(): void + { + $throw = $this->sut->throwExpression('RuntimeException', $this->sut->val('message')); + self::assertEquals( + '$var ?? throw new RuntimeException(\'message\')', + $this->printer->prettyPrintExpr($this->sut->coalesce($this->sut->var('var'), $throw)) + ); + } + + public function testMatchWithDefaultArm(): void + { + $match = $this->sut->match( + $this->sut->var('code'), + $this->sut->matchArm([$this->sut->val(200)], $this->sut->val(null)), + $this->sut->defaultMatchArm($this->sut->throwExpression('RuntimeException')) + ); + self::assertEquals( + <<<'EOD' +match ($code) { + 200 => null, + default => throw new RuntimeException(), +} +EOD, + $this->printer->prettyPrintExpr($match) + ); + } + + public function testCastToString(): void + { + $cast = $this->sut->castToString($this->sut->var('value')); + self::assertEquals('(string) $value', $this->printer->prettyPrintExpr($cast)); + } + public function testNotEquals(): void { $left = $this->sut->var('left'); @@ -295,4 +328,48 @@ public function testNotEquals(): void $notEquals = $this->sut->notEquals($left, $right); self::assertEquals('$left !== $right', $this->printer->prettyPrintExpr($notEquals)); } + + /** + * @dataProvider localPropertyProvider + */ + public function testLocalProperty(string $type, bool $nullable, string $expected): void + { + $this->phpVersionResolver->method('isPropertyTypeHintSupported')->willReturn(true); + + $property = $this->sut->localProperty('some', $type, $type, $nullable); + + self::assertEquals($expected, $this->printer->prettyPrint([$property])); + } + + public function localPropertyProvider(): array + { + return [ + 'mandatory' => ['string', false, 'private string $some;'], + 'nullable' => ['string', true, 'private ?string $some = null;'], + 'mandatory mixed' => ['mixed', false, 'private mixed $some;'], + 'nullable mixed' => ['mixed', true, 'private mixed $some = null;'], + ]; + } + + /** + * @dataProvider paramTypeProvider + */ + public function testParamType(string $type, bool $nullable, string $expected): void + { + $this->phpVersionResolver->method('isNullableTypeHintSupported')->willReturn(true); + + $param = $this->sut->param('some')->setType($type, $nullable)->getNode(); + + self::assertEquals($expected, $this->printer->prettyPrint([$this->sut->method('test')->addParam($param)->getNode()])); + } + + public function paramTypeProvider(): array + { + return [ + 'mandatory' => ['string', false, 'function test(string $some)' . PHP_EOL . '{' . PHP_EOL . '}'], + 'nullable' => ['string', true, 'function test(?string $some)' . PHP_EOL . '{' . PHP_EOL . '}'], + 'mandatory mixed' => ['mixed', false, 'function test(mixed $some)' . PHP_EOL . '{' . PHP_EOL . '}'], + 'nullable mixed' => ['mixed', true, 'function test(mixed $some)' . PHP_EOL . '{' . PHP_EOL . '}'], + ]; + } } diff --git a/test/suite/unit/Generator/EnumGeneratorTest.php b/test/suite/unit/Generator/EnumGeneratorTest.php index b270deb..1b1351b 100644 --- a/test/suite/unit/Generator/EnumGeneratorTest.php +++ b/test/suite/unit/Generator/EnumGeneratorTest.php @@ -30,7 +30,39 @@ public function caseNameProvider(): array 'leading digit, no separator' => ['3d', 'V_3D'], 'decimal' => ['1.5', 'V_1_5'], 'negative number' => ['-1', '_1'], - 'invalid characters only' => ['+', ''], + 'symbol only' => ['+', 'PLUS'], + 'asterisk' => ['*', 'ASTERISK'], + 'equals sign' => ['=', 'EQUALS'], + 'several symbols' => ['>=', 'GREATER_THAN_EQUALS'], + 'non-ASCII character only' => ['€', 'U20AC'], + 'empty string' => ['', 'EMPTY'], + 'reserved constant name' => ['class', 'V_CLASS'], ]; } + + /** + * @dataProvider caseNamesProvider + */ + public function testGetCaseNames(array $values, array $expectedCaseNames): void + { + self::assertSame($expectedCaseNames, EnumGenerator::getCaseNames($values)); + } + + public function caseNamesProvider(): array + { + return [ + 'symbol among words' => [['=', 'BETWEEN', 'IN'], ['EQUALS', 'BETWEEN', 'IN']], + 'symbolic name taken by a later value' => [['*', 'asterisk'], ['ASTERISK_2', 'ASTERISK']], + 'symbolic name taken by an early value' => [['equals', '='], ['EQUALS', 'EQUALS_2']], + 'integers' => [[1, 2], ['V_1', 'V_2']], + ]; + } + + public function testGetCaseNameOfValue(): void + { + self::assertSame('ASTERISK_2', EnumGenerator::getCaseNameOfValue(['*', 'asterisk'], '*')); + self::assertSame('ASTERISK', EnumGenerator::getCaseNameOfValue(['*', 'asterisk'], 'asterisk')); + self::assertSame('V_2', EnumGenerator::getCaseNameOfValue([1, 2], 2)); + self::assertSame('UNKNOWN', EnumGenerator::getCaseNameOfValue(['*'], 'unknown')); + } } diff --git a/test/suite/unit/Naming/SchemaNamingTest.php b/test/suite/unit/Naming/SchemaNamingTest.php new file mode 100644 index 0000000..40fc2b1 --- /dev/null +++ b/test/suite/unit/Naming/SchemaNamingTest.php @@ -0,0 +1,44 @@ + ['Match', true, 'MatchSchema'], + 'keyword in capitals' => ['LIST', true, 'LISTSchema'], + 'reserved type name' => ['Mixed', true, 'MixedSchema'], + 'reserved class name' => ['Self', true, 'SelfSchema'], + 'soft reserved word' => ['Enum', false, 'Enum'], + 'contains a keyword' => ['MatchResult', false, 'MatchResult'], + 'regular name' => ['Player', false, 'Player'], + ]; + } + + public function testGetUniqueName(): void + { + self::assertSame('ASTERISK', SchemaNaming::getUniqueName('ASTERISK', ['EQUALS'])); + self::assertSame('ASTERISK_2', SchemaNaming::getUniqueName('ASTERISK', ['ASTERISK'])); + self::assertSame('ASTERISK_3', SchemaNaming::getUniqueName('ASTERISK', ['ASTERISK', 'ASTERISK_2'])); + self::assertSame('Foo2', SchemaNaming::getUniqueName('Foo', ['Foo'], '')); + } +} diff --git a/test/suite/unit/Output/Copy/Request/LiteralRequestBodyTest.php b/test/suite/unit/Output/Copy/Request/LiteralRequestBodyTest.php new file mode 100644 index 0000000..d86da11 --- /dev/null +++ b/test/suite/unit/Output/Copy/Request/LiteralRequestBodyTest.php @@ -0,0 +1,76 @@ + $expectedValue], $body->toArray()); + } + + public function literalValuesProvider(): array + { + $date = new DateTimeImmutable('2026-09-28T10:11:12+02:00'); + + return [ + 'integer' => [42, 42], + 'float' => [1.5, 1.5], + 'string' => ['text', 'text'], + 'boolean' => [false, false], + 'null' => [null, null], + 'date-time' => [$date, '2026-09-28T10:11:12+02:00'], + 'array of scalar' => [['a' => 1, 'b' => 2], ['a' => 1, 'b' => 2]], + 'array of dates' => [[$date], ['2026-09-28T10:11:12+02:00']], + ]; + } + + /** + * @dataProvider serializedBodiesProvider + */ + public function testBodySerializerEncodesTheLiteralValue(string $contentType, $value, string $expectedBody): void + { + $serializer = (new BodySerializer()) + ->add(new JsonContentTypeSerializer()) + ->add(new VdnApiJsonContentTypeSerializer()); + + $request = $this->createMock(RequestInterface::class); + $request->method('getContentType')->willReturn($contentType); + $request->method('getBody')->willReturn(new LiteralRequestBody($value)); + + self::assertSame($expectedBody, $serializer->serializeRequest($request)); + } + + public function serializedBodiesProvider(): array + { + return [ + 'integer as json' => ['application/json', 123, '123'], + 'string as json' => ['application/json', 'text', '"text"'], + 'boolean as json' => ['application/json', true, 'true'], + 'null as json' => ['application/json', null, 'null'], + 'list as json' => ['application/json', [1, 2], '[1,2]'], + 'integer as json api' => ['application/vnd.api+json', 123, '123'], + 'integer as +json' => ['application/problem+json', 123, '123'], + 'date-time as json' => ['application/json', new DateTimeImmutable('2026-09-28T10:11:12Z'), '"2026-09-28T10:11:12+00:00"'], + ]; + } +} diff --git a/test/suite/unit/Output/Copy/Serializer/ContentType/JsonContentTypeSerializerTest.php b/test/suite/unit/Output/Copy/Serializer/ContentType/JsonContentTypeSerializerTest.php index a81ce7e..bd491b9 100644 --- a/test/suite/unit/Output/Copy/Serializer/ContentType/JsonContentTypeSerializerTest.php +++ b/test/suite/unit/Output/Copy/Serializer/ContentType/JsonContentTypeSerializerTest.php @@ -23,6 +23,8 @@ protected function setUp(): void /** * @dataProvider validCasesProvider + * @dataProvider literalCasesProvider + * @dataProvider literalEncodeCasesProvider */ public function testEncode(array $input, string $expectedResult): void { @@ -89,6 +91,31 @@ public function validCasesProvider(): array ]; } + public function literalEncodeCasesProvider(): array + { + return [ + 'literal float keeps its zero fraction' => [ + [ + '__literalResponseValue' => 1.0, + ], + '1.0', + ], + 'literal list' => [ + [ + '__literalResponseValue' => ['a', 'b'], + ], + '["a","b"]', + ], + 'literal key next to other keys is a regular property' => [ + [ + '__literalResponseValue' => 1, + 'other' => 2, + ], + '{"__literalResponseValue":1,"other":2}', + ], + ]; + } + public function literalCasesProvider(): array { return [ diff --git a/test/suite/unit/Output/Copy/Serializer/ContentType/VdnApiJsonContentTypeSerializerTest.php b/test/suite/unit/Output/Copy/Serializer/ContentType/VdnApiJsonContentTypeSerializerTest.php index 79869b6..c231e07 100644 --- a/test/suite/unit/Output/Copy/Serializer/ContentType/VdnApiJsonContentTypeSerializerTest.php +++ b/test/suite/unit/Output/Copy/Serializer/ContentType/VdnApiJsonContentTypeSerializerTest.php @@ -23,6 +23,8 @@ protected function setUp(): void /** * @dataProvider validCasesProvider + * @dataProvider literalCasesProvider + * @dataProvider literalEncodeCasesProvider */ public function testEncode(array $input, string $expectedResult): void { @@ -89,6 +91,31 @@ public function validCasesProvider(): array ]; } + public function literalEncodeCasesProvider(): array + { + return [ + 'literal float keeps its zero fraction' => [ + [ + '__literalResponseValue' => 1.0, + ], + '1.0', + ], + 'literal list' => [ + [ + '__literalResponseValue' => ['a', 'b'], + ], + '["a","b"]', + ], + 'literal key next to other keys is a regular property' => [ + [ + '__literalResponseValue' => 1, + 'other' => 2, + ], + '{"__literalResponseValue":1,"other":2}', + ], + ]; + } + public function literalCasesProvider(): array { return [ diff --git a/test/suite/unit/Output/Php/PhpVersionTest.php b/test/suite/unit/Output/Php/PhpVersionTest.php index 74ac1f9..2f60211 100644 --- a/test/suite/unit/Output/Php/PhpVersionTest.php +++ b/test/suite/unit/Output/Php/PhpVersionTest.php @@ -44,6 +44,13 @@ public function testIsNullableTypeHintSupported() self::assertTrue((new PhpVersion(PhpVersion::VERSION_PHP74))->isNullableTypeHintSupported()); } + public function testIsThrowExpressionSupported() + { + self::assertFalse((new PhpVersion(PhpVersion::VERSION_PHP74))->isThrowExpressionSupported()); + self::assertTrue((new PhpVersion(PhpVersion::VERSION_PHP80))->isThrowExpressionSupported()); + self::assertTrue((new PhpVersion(PhpVersion::VERSION_PHP83))->isThrowExpressionSupported()); + } + public function testIsClassConstantVisibilitySupported() { self::assertFalse(