diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index 64aad84f6..84e8d2010 100644 --- a/app/V1Module/presenters/GroupsPresenter.php +++ b/app/V1Module/presenters/GroupsPresenter.php @@ -758,7 +758,7 @@ public function checkGetExamLocks(string $id, string $examId) * @GET */ #[Path("id", new VString(), "An identifier of the related group", required: true)] - #[Path("examId", new VString(), "An identifier of the exam", required: true)] + #[Path("examId", new VInt(), "An identifier of the exam", required: true)] public function actionGetExamLocks(string $id, string $examId) { $group = $this->groups->findOrThrow($id); diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index b50d3c1ec..bd3d2000d 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -214,24 +214,22 @@ private function processParams(ReflectionMethod $reflection) // use a method specialized for formats if there is a format available $format = MetaFormatHelper::extractFormatFromAttribute($reflection); if ($format !== null) { - $this->processParamsFormat($format); - return; + $this->requestFormatInstance = $this->processParamsFormat($format, null); } - // otherwise use a method for loose parameters + // handle loose parameters $paramData = MetaFormatHelper::extractRequestParamData($reflection); $this->processParamsLoose($paramData); } + /** + * Processes loose parameters. Request parameters are validated, no new data is created. + * @param array $paramData Parameter data to be validated. + */ private function processParamsLoose(array $paramData) { // validate each param foreach ($paramData as $param) { - ///TODO: path parameters are not checked yet - if ($param->type == Type::Path) { - continue; - } - $paramValue = $this->getValueFromParamData($param); // this throws when it does not conform @@ -239,7 +237,18 @@ private function processParamsLoose(array $paramData) } } - private function processParamsFormat(string $format) + /** + * Processes parameters defined by a format. Request parameters are validated and a format instance with + * parameter values created. + * @param string $format The format defining the parameters. + * @param ?array $valueDictionary If not null, a nested format instance will be created. The values will be taken + * from here instead of the request object. Format validation ignores parameter type (path, query or post). + * A top-level format will be created if null. + * @throws \App\Exceptions\InternalServerException Thrown when the format definition is corrupted/absent. + * @throws \App\Exceptions\BadRequestException Thrown when the request parameter values do not conform to the definition. + * @return MetaFormat Returns a format instance with values filled from the request object. + */ + private function processParamsFormat(string $format, ?array $valueDictionary): MetaFormat { // get the parsed attribute data from the format fields $formatToFieldDefinitionsMap = FormatCache::getFormatToFieldDefinitionsMap(); @@ -250,15 +259,29 @@ private function processParamsFormat(string $format) // maps field names to their attribute data $nameToFieldDefinitionsMap = $formatToFieldDefinitionsMap[$format]; - ///TODO: handle nested MetaFormat creation $formatInstance = MetaFormatHelper::createFormatInstance($format); foreach ($nameToFieldDefinitionsMap as $fieldName => $requestParamData) { - ///TODO: path parameters are not checked yet - if ($requestParamData->type == Type::Path) { - continue; + $value = null; + // top-level format + if ($valueDictionary === null) { + $value = $this->getValueFromParamData($requestParamData); + // nested format + } else { + // Instead of retrieving the values with the getRequest call, use the provided $valueDictionary. + // This makes the nested format ignore the parameter type (path, query, post) which is intended. + // The data for this nested format cannot be spread across multiple param types, but it could be + // if this was not a nested format but the top level format. + if (array_key_exists($requestParamData->name, $valueDictionary)) { + $value = $valueDictionary[$requestParamData->name]; + } } - $value = $this->getValueFromParamData($requestParamData); + // handle nested format creation + // replace the value dictionary stored in $value with a format instance + $nestedFormatName = $requestParamData->getFormatName(); + if ($nestedFormatName !== null) { + $value = $this->processParamsFormat($nestedFormatName, $value); + } // this throws if the value is invalid $formatInstance->checkedAssign($fieldName, $value); @@ -269,11 +292,11 @@ private function processParamsFormat(string $format) throw new BadRequestException("All request fields are valid but additional structural constraints failed."); } - $this->requestFormatInstance = $formatInstance; + return $formatInstance; } /** - * Calls either getPostField or getQueryField based on the provided metadata. + * Calls either getPostField, getQueryField or getPathField based on the provided metadata. * @param \App\Helpers\MetaFormats\RequestParamData $paramData Metadata of the request parameter. * @throws \App\Exceptions\InternalServerException Thrown when an unexpected parameter location was set. * @return mixed Returns the value from the request. @@ -285,6 +308,8 @@ private function getValueFromParamData(RequestParamData $paramData): mixed return $this->getPostField($paramData->name, required: $paramData->required); case Type::Query: return $this->getQueryField($paramData->name, required: $paramData->required); + case Type::Path: + return $this->getPathField($paramData->name); default: throw new InternalServerException("Unknown parameter type: {$paramData->type->name}"); } @@ -327,6 +352,15 @@ private function getQueryField($param, $required = true) return $value; } + private function getPathField($param) + { + $value = $this->getParameter($param); + if ($value === null) { + throw new BadRequestException("Missing required path field $param"); + } + return $value; + } + protected function logUserAction($code = IResponse::S200_OK) { if ($this->getUser()->isLoggedIn()) { diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 9f34d53c6..c7eb04f2f 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -4,6 +4,7 @@ use App\Exceptions\InternalServerException; use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\BaseValidator; use Attribute; /** @@ -13,10 +14,12 @@ class FormatParameterAttribute { public Type $type; + /** + * @var BaseValidator[] + */ public array $validators; public string $description; public bool $required; - // there is not an easy way to check whether a property has the nullability flag set public bool $nullable; /** @@ -50,5 +53,12 @@ public function __construct( } $this->validators = $validators; } + + // remove strict type checking for query and path parameters + if ($type === Type::Path || $type === Type::Query) { + foreach ($this->validators as $validator) { + $validator->setStrict(false); + } + } } } diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index ad1e44349..b06f8e0b3 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -4,7 +4,9 @@ use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; +use App\Helpers\MetaFormats\Validators\BaseValidator; use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VObject; use App\Helpers\Swagger\AnnotationParameterData; use Exception; @@ -17,6 +19,9 @@ class RequestParamData public string $name; public string $description; public bool $required; + /** + * @var BaseValidator[] + */ public array $validators; public bool $nullable; @@ -76,12 +81,21 @@ public function conformsToDefinition(mixed $value) } } - private function hasValidators(): bool + /** + * Returns the format name if the parameter should be interpreted as a format and not as a primitive type. + * @return ?string Returns the format name or null if the param represents a primitive type. + */ + public function getFormatName(): ?string { - if (is_array($this->validators)) { - return count($this->validators) > 0; + // all format params have to have a VObject validator + foreach ($this->validators as $validator) { + if ($validator instanceof VObject) { + return $validator->format; + } } - return $this->validators !== null; + + // return null for primitive types + return null; } /** @@ -91,7 +105,7 @@ private function hasValidators(): bool */ public function toAnnotationParameterData() { - if (!$this->hasValidators()) { + if (count($this->validators) === 0) { throw new InternalServerException( "No validator found for parameter {$this->name}, description: {$this->description}." ); @@ -105,10 +119,17 @@ public function toAnnotationParameterData() $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); } - // retrieve the example value from the getExampleValue method if present - $exampleValue = null; - if (method_exists(get_class($this->validators[0]), "getExampleValue")) { - $exampleValue = $this->validators[0]->getExampleValue(); + // get example value from the first validator + $exampleValue = $this->validators[0]->getExampleValue(); + + // add nested parameter data if this is an object + $format = $this->getFormatName(); + $nestedObjectParameterData = null; + if ($format !== null) { + $nestedRequestParmData = FormatCache::getFieldDefinitions($format); + $nestedObjectParameterData = array_map(function (RequestParamData $data) { + return $data->toAnnotationParameterData(); + }, $nestedRequestParmData); } return new AnnotationParameterData( @@ -120,6 +141,7 @@ public function toAnnotationParameterData() $this->nullable, $exampleValue, $nestedArraySwaggerType, + $nestedObjectParameterData, ); } } diff --git a/app/helpers/MetaFormats/Validators/BaseValidator.php b/app/helpers/MetaFormats/Validators/BaseValidator.php new file mode 100644 index 000000000..d3d7aa7af --- /dev/null +++ b/app/helpers/MetaFormats/Validators/BaseValidator.php @@ -0,0 +1,56 @@ +strict = $strict; + } + + /** + * @var string One of the valid swagger types (https://swagger.io/docs/specification/v3_0/data-models/data-types/). + */ + public const SWAGGER_TYPE = "invalid"; + + /** + * @var bool Whether strict type checking is done in validation. + */ + protected bool $strict; + + /** + * Sets the strict flag. + * Expected to be changed by Attributes containing validators to change their behavior based on the Attribute type. + * @param bool $strict Whether validation type checking should be done. + * When false, the validation step will no longer enforce the correct type of the value. + */ + public function setStrict(bool $strict) + { + $this->strict = $strict; + } + + /** + * @return string Returns a sample expected value to be validated by the validator. + * This value will be used in generated swagger documents. + * Can return null, signalling to the swagger generator to omit the example field. + */ + public function getExampleValue(): string | null + { + return null; + } + + /** + * Validates a value with the configured validation strictness. + * @param mixed $value The value to be validated. + * @return bool Whether the value passed the test. + */ + public function validate(mixed $value): bool + { + // return false by default to enforce overriding in derived types + return false; + } +} diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index 36690154a..7f205d7b8 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -5,26 +5,27 @@ /** * Validates arrays and their nested elements. */ -class VArray +class VArray extends BaseValidator { public const SWAGGER_TYPE = "array"; // validator used for elements - private mixed $nestedValidator; + private ?BaseValidator $nestedValidator; /** * Creates an array validator. - * @param mixed $nestedValidator A validator that will be applied on all elements + * @param ?BaseValidator $nestedValidator A validator that will be applied on all elements * (validator arrays are not supported). */ - public function __construct(mixed $nestedValidator = null) + public function __construct(?BaseValidator $nestedValidator = null, bool $strict = true) { + parent::__construct($strict); $this->nestedValidator = $nestedValidator; } - public function getExampleValue() + public function getExampleValue(): string | null { - if ($this->nestedValidator !== null && method_exists(get_class($this->nestedValidator), "getExampleValue")) { + if ($this->nestedValidator !== null) { return $this->nestedValidator->getExampleValue(); } @@ -43,7 +44,19 @@ public function getElementSwaggerType(): mixed return $this->nestedValidator::SWAGGER_TYPE; } - public function validate(mixed $value) + /** + * Sets the strict flag for this validator and the element validator if present. + * Expected to be changed by Attributes containing validators to change their behavior based on the Attribute type. + * @param bool $strict Whether validation type checking should be done. + * When false, the validation step will no longer enforce the correct type of the value. + */ + public function setStrict(bool $strict) + { + parent::setStrict($strict); + $this->nestedValidator?->setStrict($strict); + } + + public function validate(mixed $value): bool { if (!is_array($value)) { return false; diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 55064756e..bf0defee6 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -5,13 +5,32 @@ /** * Validates boolean values. Accepts only boolean true and false. */ -class VBool +class VBool extends BaseValidator { public const SWAGGER_TYPE = "boolean"; - public function validate(mixed $value) + public function getExampleValue(): string { - ///TODO: remove 'false' once the testUpdateInstance test issue is fixed. - return $value === true || $value === false || $value === 'false'; + return "true"; + } + + public function validate(mixed $value): bool + { + if (is_bool($value)) { + return true; + } + + if ($this->strict) { + ///TODO: replace this with 'return false;' once the testUpdateInstance test issue is fixed. + return $value === 'false'; + } + + // FILTER_VALIDATE_BOOL is not used because it additionally allows "on", "yes", "off", "no" and "" + return $value === 0 + || $value === 1 + || $value === "0" + || $value === "1" + || $value === "false" + || $value === "true"; } } diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index a876e63f3..799b1fdd9 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -5,17 +5,26 @@ /** * Validates doubles. Accepts doubles as well as their stringified versions. */ -class VDouble +class VDouble extends BaseValidator { public const SWAGGER_TYPE = "number"; - public function validate(mixed $value) + public function getExampleValue(): string { - // check if it is a double - if (is_double($value)) { + return "0.1"; + } + + public function validate(mixed $value): bool + { + // check if it is a double or an integer (is_double(0) returns false) + if (is_double($value) || is_int($value)) { return true; } + if ($this->strict) { + return false; + } + // the value may be a string containing the number, or an integer return is_numeric($value); } diff --git a/app/helpers/MetaFormats/Validators/VEmail.php b/app/helpers/MetaFormats/Validators/VEmail.php index e9577465b..250758a33 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -7,13 +7,13 @@ */ class VEmail extends VString { - public function __construct() + public function __construct(bool $strict = true) { // the email should not be empty - parent::__construct(1); + parent::__construct(1, strict: $strict); } - public function getExampleValue() + public function getExampleValue(): string { return "name@domain.tld"; } diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index c3d78127c..570e9a5af 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -5,22 +5,26 @@ /** * Validates integers. Accepts ints as well as their stringified versions. */ -class VInt +class VInt extends BaseValidator { public const SWAGGER_TYPE = "integer"; - public function getExampleValue(): int|string + public function getExampleValue(): string { return "0"; } - public function validate(mixed $value) + public function validate(mixed $value): bool { // check if it is an integer (does not handle integer strings) if (is_int($value)) { return true; } + if ($this->strict) { + return false; + } + // the value may be a string containing the integer if (!is_numeric($value)) { return false; diff --git a/app/helpers/MetaFormats/Validators/VMixed.php b/app/helpers/MetaFormats/Validators/VMixed.php index bf3fd1754..ad856938f 100644 --- a/app/helpers/MetaFormats/Validators/VMixed.php +++ b/app/helpers/MetaFormats/Validators/VMixed.php @@ -7,15 +7,10 @@ * Placeholder validator used for endpoints with no existing validation rules. * New endpoints should never use this validator, instead use a more restrictive one. */ -class VMixed +class VMixed extends BaseValidator { public const SWAGGER_TYPE = "string"; - public function getExampleValue() - { - return "value"; - } - public function validate(mixed $value): bool { return true; diff --git a/app/helpers/MetaFormats/Validators/VObject.php b/app/helpers/MetaFormats/Validators/VObject.php new file mode 100644 index 000000000..892233b9c --- /dev/null +++ b/app/helpers/MetaFormats/Validators/VObject.php @@ -0,0 +1,34 @@ +format = $format; + + // throw immediately if the format does not exist + if (!FormatCache::formatExists($format)) { + throw new InternalServerException("Format $format does not exist."); + } + } + + public function validate(mixed $value): bool + { + // fine-grained checking is done in the properties + return $value instanceof MetaFormat; + } +} diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index a70effd35..19cc6368f 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -5,7 +5,7 @@ /** * Validates strings. */ -class VString +class VString extends BaseValidator { public const SWAGGER_TYPE = "string"; private int $minLength; @@ -19,14 +19,15 @@ class VString * @param ?string $regex Regex pattern used for validation. * Evaluated with the preg_match function with this argument as the pattern. */ - public function __construct(int $minLength = 0, int $maxLength = -1, ?string $regex = null) + public function __construct(int $minLength = 0, int $maxLength = -1, ?string $regex = null, bool $strict = true) { + parent::__construct($strict); $this->minLength = $minLength; $this->maxLength = $maxLength; $this->regex = $regex; } - public function getExampleValue() + public function getExampleValue(): string { return "text"; } diff --git a/app/helpers/MetaFormats/Validators/VUuid.php b/app/helpers/MetaFormats/Validators/VUuid.php index ae5f6d0ee..7becf8a4d 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -7,18 +7,13 @@ */ class VUuid extends VString { - public function __construct() + public function __construct(bool $strict = true) { - parent::__construct(regex: "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/"); + parent::__construct(regex: "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/", strict: $strict); } - public function getExampleValue() + public function getExampleValue(): string { return "10000000-2000-4000-8000-160000000000"; } - - public function validate(mixed $value): bool - { - return parent::validate($value); - } } diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 594804233..48ce56097 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -2,6 +2,8 @@ namespace App\Helpers\Swagger; +use App\Helpers\MetaFormats\AnnotationConversion\Utils; + /** * A data structure for endpoint signatures that can produce annotations parsable by a swagger generator. */ @@ -9,18 +11,24 @@ class AnnotationData { public HttpMethods $httpMethod; + public string $className; + public string $methodName; public array $pathParams; public array $queryParams; public array $bodyParams; public ?string $endpointDescription; public function __construct( + string $className, + string $methodName, HttpMethods $httpMethod, array $pathParams, array $queryParams, array $bodyParams, - string $endpointDescription = null, + ?string $endpointDescription = null, ) { + $this->className = $className; + $this->methodName = $methodName; $this->httpMethod = $httpMethod; $this->pathParams = $pathParams; $this->queryParams = $queryParams; @@ -68,16 +76,31 @@ private function getBodyAnnotation(): string | null return $head . $body->toString() . "))"; } - /** - * Converts the extracted annotation data to a string parsable by the Swagger-PHP library. - * @param string $route The route of the handler this set of data represents. - * @return string Returns the transpiled annotations on a single line. - */ + /** + * Constructs an operation ID used to identify the endpoint. + * The operation ID is composed of the presenter class name and the endpoint method name with the 'action' prefix. + * @return string Returns the operation ID. + */ + private function constructOperationId() + { + // remove the namespace prefix of the class and make the first letter lowercase + $className = lcfirst(Utils::shortenClass($this->className)); + // remove the 'action' prefix + $endpoint = substr($this->methodName, strlen("action")); + return $className . $endpoint; + } + + /** + * Converts the extracted annotation data to a string parsable by the Swagger-PHP library. + * @param string $route The route of the handler this set of data represents. + * @return string Returns the transpiled annotations on a single line. + */ public function toSwaggerAnnotations(string $route) { $httpMethodAnnotation = $this->getHttpMethodAnnotation(); $body = new ParenthesesBuilder(); $body->addKeyValue("path", $route); + $body->addKeyValue("operationId", $this->constructOperationId()); // add the endpoint description when provided if ($this->endpointDescription !== null) { diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index ebf7829c5..942cc4aab 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -3,6 +3,7 @@ namespace App\Helpers\Swagger; use App\Exceptions\InvalidArgumentException; +use App\Helpers\MetaFormats\FormatCache; use App\Helpers\MetaFormats\MetaFormatHelper; use App\V1Module\Router\MethodRoute; use App\V1Module\RouterFactory; @@ -278,7 +279,9 @@ private static function extractAnnotationDescription(array $annotations): ?strin } private static function annotationParameterDataToAnnotationData( - HttpMethods $method, + string $className, + string $methodName, + HttpMethods $httpMethod, array $params, ?string $description ): AnnotationData { @@ -298,7 +301,15 @@ private static function annotationParameterDataToAnnotationData( } } - return new AnnotationData($method, $pathParams, $queryParams, $bodyParams, $description); + return new AnnotationData( + $className, + $methodName, + $httpMethod, + $pathParams, + $queryParams, + $bodyParams, + $description + ); } /** @@ -322,7 +333,13 @@ public static function extractStandardAnnotationData( $params = self::extractStandardAnnotationParams($methodAnnotations, $route); $description = self::extractAnnotationDescription($methodAnnotations); - return self::annotationParameterDataToAnnotationData($httpMethod, $params, $description); + return self::annotationParameterDataToAnnotationData( + $className, + $methodName, + $httpMethod, + $params, + $description + ); } /** @@ -339,13 +356,29 @@ public static function extractAttributeData(string $className, string $methodNam $methodAnnotations = self::getMethodAnnotations($className, $methodName); $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); - $attributeData = MetaFormatHelper::extractRequestParamData(self::getMethod($className, $methodName)); + $reflectionMethod = self::getMethod($className, $methodName); + + $format = MetaFormatHelper::extractFormatFromAttribute($reflectionMethod); + // if the endpoint is linked to a format, use the format class + if ($format !== null) { + $attributeData = FormatCache::getFieldDefinitions($format); + // otherwise use loose param attributes + } else { + $attributeData = MetaFormatHelper::extractRequestParamData($reflectionMethod); + } + $params = array_map(function ($data) { return $data->toAnnotationParameterData(); }, $attributeData); $description = self::extractAnnotationDescription($methodAnnotations); - return self::annotationParameterDataToAnnotationData($httpMethod, $params, $description); + return self::annotationParameterDataToAnnotationData( + $className, + $methodName, + $httpMethod, + $params, + $description + ); } /** diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index ddd9924f6..81dd24271 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -18,6 +18,7 @@ class AnnotationParameterData public bool $nullable; public ?string $example; public ?string $nestedArraySwaggerType; + public ?array $nestedObjectParameterData; public function __construct( string $swaggerType, @@ -28,6 +29,7 @@ public function __construct( bool $nullable, string $example = null, string $nestedArraySwaggerType = null, + ?array $nestedObjectParameterData = null, ) { $this->swaggerType = $swaggerType; $this->name = $name; @@ -37,24 +39,39 @@ public function __construct( $this->nullable = $nullable; $this->example = $example; $this->nestedArraySwaggerType = $nestedArraySwaggerType; + $this->nestedObjectParameterData = $nestedObjectParameterData; } - private function addArrayItemsIfArray(string $swaggerType, ParenthesesBuilder $container) + private function addArrayItemsIfArray(ParenthesesBuilder $container) { - if ($swaggerType === "array") { - $itemsHead = "@OA\\Items"; - $items = new ParenthesesBuilder(); + if ($this->swaggerType !== "array") { + return; + } - if ($this->nestedArraySwaggerType !== null) { - $items->addKeyValue("type", $this->nestedArraySwaggerType); - } + $itemsHead = "@OA\\Items"; + $items = new ParenthesesBuilder(); - // add example value - if ($this->example != null) { - $items->addKeyValue("example", $this->example); - } + if ($this->nestedArraySwaggerType !== null) { + $items->addKeyValue("type", $this->nestedArraySwaggerType); + } + + // add example value + if ($this->example != null) { + $items->addKeyValue("example", $this->example); + } + + $container->addValue($itemsHead . $items->toString()); + } + + private function addObjectParamsIfObject(ParenthesesBuilder $container) + { + if ($this->nestedObjectParameterData === null) { + return; + } - $container->addValue($itemsHead . $items->toString()); + foreach ($this->nestedObjectParameterData as $paramData) { + $annotation = $paramData->toPropertyAnnotation(); + $container->addValue($annotation); } } @@ -68,7 +85,7 @@ private function generateSchemaAnnotation(): string $body = new ParenthesesBuilder(); $body->addKeyValue("type", $this->swaggerType); - $this->addArrayItemsIfArray($this->swaggerType, $body); + $this->addArrayItemsIfArray($body); return $head . $body->toString(); } @@ -112,10 +129,13 @@ public function toPropertyAnnotation(): string } // handle arrays - $this->addArrayItemsIfArray($this->swaggerType, $body); + $this->addArrayItemsIfArray($body); + + // handle objects + $this->addObjectParamsIfObject($body); // add example value - if ($this->swaggerType !== "array") { + if ($this->swaggerType !== "array" && $this->swaggerType !== "object") { if ($this->example != null) { $body->addKeyValue("example", $this->example); } diff --git a/tests/Presenters/LoginPresenter.phpt b/tests/Presenters/LoginPresenter.phpt index a622b4a89..30a735a47 100644 --- a/tests/Presenters/LoginPresenter.phpt +++ b/tests/Presenters/LoginPresenter.phpt @@ -296,7 +296,7 @@ class TestLoginPresenter extends Tester\TestCase "V1:Login", "POST", ["action" => "issueRestrictedToken"], - ["scopes" => [TokenScope::REFRESH, "read-all"], "expiration" => "3000"] + ["scopes" => [TokenScope::REFRESH, "read-all"], "expiration" => 3000] ); $response = $this->presenter->run($request); Assert::type(JsonResponse::class, $response);