From 18501e13e328c96619f4afacbad689f138a6a3e9 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 2 Mar 2025 15:04:33 +0100 Subject: [PATCH 01/27] added support for nested formats --- .../presenters/base/BasePresenter.php | 46 +++++++++++++++---- app/helpers/MetaFormats/RequestParamData.php | 18 ++++++++ .../MetaFormats/Validators/VFormat.php | 39 ++++++++++++++++ 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 app/helpers/MetaFormats/Validators/VFormat.php diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index b50d3c1e..d5aa43fd 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -214,7 +214,7 @@ 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); + $this->requestFormatInstance = $this->processParamsFormat($format, null); return; } @@ -239,7 +239,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 +261,34 @@ 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) { + ///TODO: path parameters are not checked yet + if ($requestParamData->type == Type::Path) { + continue; + } + + $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,7 +299,7 @@ private function processParamsFormat(string $format) throw new BadRequestException("All request fields are valid but additional structural constraints failed."); } - $this->requestFormatInstance = $formatInstance; + return $formatInstance; } /** diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index ad1e4434..cc5a5c39 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -5,6 +5,7 @@ use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VFormat; use App\Helpers\Swagger\AnnotationParameterData; use Exception; @@ -76,6 +77,23 @@ public function conformsToDefinition(mixed $value) } } + /** + * 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 + { + // all format params have to have a VFormat validator + foreach ($this->validators as $validator) { + if ($validator instanceof VFormat) { + return $validator->format; + } + } + + // return null for primitive types + return null; + } + private function hasValidators(): bool { if (is_array($this->validators)) { diff --git a/app/helpers/MetaFormats/Validators/VFormat.php b/app/helpers/MetaFormats/Validators/VFormat.php new file mode 100644 index 00000000..7f94b3fe --- /dev/null +++ b/app/helpers/MetaFormats/Validators/VFormat.php @@ -0,0 +1,39 @@ +format = $format; + + // throw immediatelly if the format does not exist + if (!FormatCache::formatExists($format)) { + throw new InternalServerException("Format $format does not exist."); + } + } + + public function getExampleValue() + { + ///TODO + return "0"; + } + + public function validate(mixed $value) + { + // fine-grained checking is done in the properties + return $value instanceof MetaFormat; + } +} From 73752f096afcd761f35b2771a426dfa818e3d3c5 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 2 Mar 2025 18:49:06 +0100 Subject: [PATCH 02/27] added swagger generator support for format classes --- app/helpers/MetaFormats/RequestParamData.php | 11 ++++ .../MetaFormats/Validators/VFormat.php | 6 --- app/helpers/Swagger/AnnotationHelper.php | 13 ++++- .../Swagger/AnnotationParameterData.php | 50 +++++++++++++------ 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index cc5a5c39..8ddff80c 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -129,6 +129,16 @@ public function toAnnotationParameterData() $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( $swaggerType, $this->name, @@ -138,6 +148,7 @@ public function toAnnotationParameterData() $this->nullable, $exampleValue, $nestedArraySwaggerType, + $nestedObjectParameterData, ); } } diff --git a/app/helpers/MetaFormats/Validators/VFormat.php b/app/helpers/MetaFormats/Validators/VFormat.php index 7f94b3fe..0a426668 100644 --- a/app/helpers/MetaFormats/Validators/VFormat.php +++ b/app/helpers/MetaFormats/Validators/VFormat.php @@ -25,12 +25,6 @@ public function __construct(string $format) } } - public function getExampleValue() - { - ///TODO - return "0"; - } - public function validate(mixed $value) { // fine-grained checking is done in the properties diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index ebf7829c..0e4cc835 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; @@ -339,7 +340,17 @@ 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); diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index ddd9924f..81dd2427 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); } From 68bd434936a334e5697a80a9a4a85cda9326d7c8 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 5 Mar 2025 18:03:22 +0100 Subject: [PATCH 03/27] added readable operationIds --- app/helpers/Swagger/AnnotationData.php | 35 ++++++++++++++++++++---- app/helpers/Swagger/AnnotationHelper.php | 30 +++++++++++++++++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 59480423..48ce5609 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 0e4cc835..942cc4aa 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -279,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 { @@ -299,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 + ); } /** @@ -323,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 + ); } /** @@ -356,7 +372,13 @@ public static function extractAttributeData(string $className, string $methodNam }, $attributeData); $description = self::extractAnnotationDescription($methodAnnotations); - return self::annotationParameterDataToAnnotationData($httpMethod, $params, $description); + return self::annotationParameterDataToAnnotationData( + $className, + $methodName, + $httpMethod, + $params, + $description + ); } /** From 50b4587e101771f3c7d91477c3b2c6e9d0136082 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 12 Mar 2025 22:16:13 +0100 Subject: [PATCH 04/27] validators now have a common ancestor --- app/helpers/MetaFormats/RequestParamData.php | 21 ++---- .../MetaFormats/Validators/BaseValidator.php | 64 +++++++++++++++++++ app/helpers/MetaFormats/Validators/VArray.php | 11 +++- app/helpers/MetaFormats/Validators/VBool.php | 15 ++++- .../MetaFormats/Validators/VDouble.php | 14 +++- app/helpers/MetaFormats/Validators/VEmail.php | 7 +- .../MetaFormats/Validators/VFormat.php | 11 +++- app/helpers/MetaFormats/Validators/VInt.php | 11 +++- app/helpers/MetaFormats/Validators/VMixed.php | 8 +-- .../MetaFormats/Validators/VString.php | 9 ++- app/helpers/MetaFormats/Validators/VUuid.php | 7 +- 11 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 app/helpers/MetaFormats/Validators/BaseValidator.php diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 8ddff80c..07135713 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -4,6 +4,7 @@ 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\VFormat; use App\Helpers\Swagger\AnnotationParameterData; @@ -18,6 +19,9 @@ class RequestParamData public string $name; public string $description; public bool $required; + /** + * @var BaseValidator[] + */ public array $validators; public bool $nullable; @@ -94,14 +98,6 @@ public function getFormatName(): ?string return null; } - private function hasValidators(): bool - { - if (is_array($this->validators)) { - return count($this->validators) > 0; - } - return $this->validators !== null; - } - /** * Converts the metadata into metadata used for swagger generation. * @throws \App\Exceptions\InternalServerException Thrown when the parameter metadata is corrupted. @@ -109,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}." ); @@ -123,11 +119,8 @@ 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(); diff --git a/app/helpers/MetaFormats/Validators/BaseValidator.php b/app/helpers/MetaFormats/Validators/BaseValidator.php new file mode 100644 index 00000000..76d57e13 --- /dev/null +++ b/app/helpers/MetaFormats/Validators/BaseValidator.php @@ -0,0 +1,64 @@ +useJsonValidation) { + return $this->validateJson($value); + } + return $this->validateText($value); + } +} diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index 36690154..8cae1c9e 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -5,7 +5,7 @@ /** * Validates arrays and their nested elements. */ -class VArray +class VArray extends BaseValidator { public const SWAGGER_TYPE = "array"; @@ -24,7 +24,7 @@ public function __construct(mixed $nestedValidator = null) public function getExampleValue() { - if ($this->nestedValidator !== null && method_exists(get_class($this->nestedValidator), "getExampleValue")) { + if ($this->nestedValidator !== null) { return $this->nestedValidator->getExampleValue(); } @@ -43,7 +43,12 @@ public function getElementSwaggerType(): mixed return $this->nestedValidator::SWAGGER_TYPE; } - public function validate(mixed $value) + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(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 55064756..3db8996e 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -5,11 +5,22 @@ /** * 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 + { + return "true"; + } + + public function validateText(mixed $value): bool + { + // additionally allow 0 and 1 + return $value === 0 || $value === 1 || $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { ///TODO: remove 'false' once the testUpdateInstance test issue is fixed. return $value === true || $value === false || $value === 'false'; diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index a876e63f..cfa94770 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -5,11 +5,21 @@ /** * 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 + { + return "0.1"; + } + + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { // check if it is a double if (is_double($value)) { diff --git a/app/helpers/MetaFormats/Validators/VEmail.php b/app/helpers/MetaFormats/Validators/VEmail.php index e9577465..85c0caa1 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -18,7 +18,12 @@ public function getExampleValue() return "name@domain.tld"; } - public function validate(mixed $value): bool + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { if (!parent::validate($value)) { return false; diff --git a/app/helpers/MetaFormats/Validators/VFormat.php b/app/helpers/MetaFormats/Validators/VFormat.php index 0a426668..b530f70c 100644 --- a/app/helpers/MetaFormats/Validators/VFormat.php +++ b/app/helpers/MetaFormats/Validators/VFormat.php @@ -10,7 +10,7 @@ * Validates formats. Accepts any format derived of the base MetaFormat. * Format fields are validated by validators added to the fields. */ -class VFormat +class VFormat extends BaseValidator { public const SWAGGER_TYPE = "object"; public string $format; @@ -19,13 +19,18 @@ public function __construct(string $format) { $this->format = $format; - // throw immediatelly if the format does not exist + // 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) + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { // fine-grained checking is done in the properties return $value instanceof MetaFormat; diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index c3d78127..f6033fda 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -5,16 +5,21 @@ /** * 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 validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { // check if it is an integer (does not handle integer strings) if (is_int($value)) { diff --git a/app/helpers/MetaFormats/Validators/VMixed.php b/app/helpers/MetaFormats/Validators/VMixed.php index bf3fd175..b02f33c5 100644 --- a/app/helpers/MetaFormats/Validators/VMixed.php +++ b/app/helpers/MetaFormats/Validators/VMixed.php @@ -7,16 +7,16 @@ * 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() + public function validateText(mixed $value): bool { - return "value"; + return true; } - public function validate(mixed $value): bool + public function validateJson(mixed $value): bool { return true; } diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index a70effd3..218c581b 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; @@ -31,7 +31,12 @@ public function getExampleValue() return "text"; } - public function validate(mixed $value): bool + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { // do not allow other types if (!is_string($value)) { diff --git a/app/helpers/MetaFormats/Validators/VUuid.php b/app/helpers/MetaFormats/Validators/VUuid.php index ae5f6d0e..f076784c 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -17,7 +17,12 @@ public function getExampleValue() return "10000000-2000-4000-8000-160000000000"; } - public function validate(mixed $value): bool + public function validateText(mixed $value): bool + { + return $this->validateJson($value); + } + + public function validateJson(mixed $value): bool { return parent::validate($value); } From 87f280b954ef04f50703354a01b65cf45f065fd0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 12 Mar 2025 22:24:59 +0100 Subject: [PATCH 05/27] renamed VFormat to VObject --- app/helpers/MetaFormats/RequestParamData.php | 6 +++--- .../MetaFormats/Validators/{VFormat.php => VObject.php} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/helpers/MetaFormats/Validators/{VFormat.php => VObject.php} (96%) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 07135713..b06f8e0b 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -6,7 +6,7 @@ use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\Validators\BaseValidator; use App\Helpers\MetaFormats\Validators\VArray; -use App\Helpers\MetaFormats\Validators\VFormat; +use App\Helpers\MetaFormats\Validators\VObject; use App\Helpers\Swagger\AnnotationParameterData; use Exception; @@ -87,9 +87,9 @@ public function conformsToDefinition(mixed $value) */ public function getFormatName(): ?string { - // all format params have to have a VFormat validator + // all format params have to have a VObject validator foreach ($this->validators as $validator) { - if ($validator instanceof VFormat) { + if ($validator instanceof VObject) { return $validator->format; } } diff --git a/app/helpers/MetaFormats/Validators/VFormat.php b/app/helpers/MetaFormats/Validators/VObject.php similarity index 96% rename from app/helpers/MetaFormats/Validators/VFormat.php rename to app/helpers/MetaFormats/Validators/VObject.php index b530f70c..d607d54b 100644 --- a/app/helpers/MetaFormats/Validators/VFormat.php +++ b/app/helpers/MetaFormats/Validators/VObject.php @@ -10,7 +10,7 @@ * Validates formats. Accepts any format derived of the base MetaFormat. * Format fields are validated by validators added to the fields. */ -class VFormat extends BaseValidator +class VObject extends BaseValidator { public const SWAGGER_TYPE = "object"; public string $format; From 971f87fa14c8ddc70edd125787fc832c6f62ca2d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 13 Mar 2025 14:20:56 +0100 Subject: [PATCH 06/27] removed endless recursion --- app/helpers/MetaFormats/Validators/VEmail.php | 2 +- app/helpers/MetaFormats/Validators/VUuid.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VEmail.php b/app/helpers/MetaFormats/Validators/VEmail.php index 85c0caa1..13bd0caa 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -25,7 +25,7 @@ public function validateText(mixed $value): bool public function validateJson(mixed $value): bool { - if (!parent::validate($value)) { + if (!parent::validateJson($value)) { return false; } diff --git a/app/helpers/MetaFormats/Validators/VUuid.php b/app/helpers/MetaFormats/Validators/VUuid.php index f076784c..f56769b8 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -24,6 +24,6 @@ public function validateText(mixed $value): bool public function validateJson(mixed $value): bool { - return parent::validate($value); + return parent::validateJson($value); } } From 3d17f254665fa3114a836e3b6144949ff23626c1 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 13 Mar 2025 14:27:17 +0100 Subject: [PATCH 07/27] getExampleValue can now return null --- app/helpers/MetaFormats/Validators/BaseValidator.php | 5 +++-- app/helpers/MetaFormats/Validators/VArray.php | 2 +- app/helpers/MetaFormats/Validators/VEmail.php | 2 +- app/helpers/MetaFormats/Validators/VString.php | 2 +- app/helpers/MetaFormats/Validators/VUuid.php | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/BaseValidator.php b/app/helpers/MetaFormats/Validators/BaseValidator.php index 76d57e13..2be9a19e 100644 --- a/app/helpers/MetaFormats/Validators/BaseValidator.php +++ b/app/helpers/MetaFormats/Validators/BaseValidator.php @@ -21,10 +21,11 @@ class BaseValidator /** * @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() + public function getExampleValue(): string | null { - return "No example provided."; + return null; } /** diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index 8cae1c9e..f034f59c 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -22,7 +22,7 @@ public function __construct(mixed $nestedValidator = null) $this->nestedValidator = $nestedValidator; } - public function getExampleValue() + public function getExampleValue(): string | null { if ($this->nestedValidator !== null) { return $this->nestedValidator->getExampleValue(); diff --git a/app/helpers/MetaFormats/Validators/VEmail.php b/app/helpers/MetaFormats/Validators/VEmail.php index 13bd0caa..c3d34368 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -13,7 +13,7 @@ public function __construct() parent::__construct(1); } - public function getExampleValue() + public function getExampleValue(): string { return "name@domain.tld"; } diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index 218c581b..a337558f 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -26,7 +26,7 @@ public function __construct(int $minLength = 0, int $maxLength = -1, ?string $re $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 f56769b8..7b75e75d 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -12,7 +12,7 @@ public function __construct() parent::__construct(regex: "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/"); } - public function getExampleValue() + public function getExampleValue(): string { return "10000000-2000-4000-8000-160000000000"; } From c270f1e9eb68b9d05c4896030764269f53e4a088 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 13 Mar 2025 15:12:36 +0100 Subject: [PATCH 08/27] query and path parameters now use different validators --- app/helpers/MetaFormats/Attributes/FPath.php | 2 ++ app/helpers/MetaFormats/Attributes/FQuery.php | 2 ++ .../Attributes/FormatParameterAttribute.php | 15 ++++++++++++++- app/helpers/MetaFormats/Attributes/Path.php | 2 ++ app/helpers/MetaFormats/Attributes/Query.php | 2 ++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/Attributes/FPath.php b/app/helpers/MetaFormats/Attributes/FPath.php index e4a6c4b9..b376d870 100644 --- a/app/helpers/MetaFormats/Attributes/FPath.php +++ b/app/helpers/MetaFormats/Attributes/FPath.php @@ -24,5 +24,7 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Path, $validators, $description, $required, $nullable); + // do not use json validation for path params + $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/FQuery.php b/app/helpers/MetaFormats/Attributes/FQuery.php index 3cf21c02..47c4866c 100644 --- a/app/helpers/MetaFormats/Attributes/FQuery.php +++ b/app/helpers/MetaFormats/Attributes/FQuery.php @@ -24,5 +24,7 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Query, $validators, $description, $required, $nullable); + // do not use json validation for query params + $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 9f34d53c..8c2348d5 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; /** @@ -51,4 +54,14 @@ public function __construct( $this->validators = $validators; } } + + /** + * Disables JSON validation for all validators. + */ + protected function disableJsonValidation() + { + foreach ($this->validators as $validator) { + $validator->useJsonValidation = false; + } + } } diff --git a/app/helpers/MetaFormats/Attributes/Path.php b/app/helpers/MetaFormats/Attributes/Path.php index 7df7cca1..60d14a82 100644 --- a/app/helpers/MetaFormats/Attributes/Path.php +++ b/app/helpers/MetaFormats/Attributes/Path.php @@ -26,5 +26,7 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Path, $name, $validators, $description, $required, $nullable); + // do not use json validation for path params + $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/Query.php b/app/helpers/MetaFormats/Attributes/Query.php index dc6ea4b9..f4b7d172 100644 --- a/app/helpers/MetaFormats/Attributes/Query.php +++ b/app/helpers/MetaFormats/Attributes/Query.php @@ -26,5 +26,7 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Query, $name, $validators, $description, $required, $nullable); + // do not use json validation for query params + $this->disableJsonValidation(); } } From 576480259f2d317e54e679898b03c788008ca0e3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 15:10:00 +0100 Subject: [PATCH 09/27] format and loose attributes can both be used on the same endpoint --- app/V1Module/presenters/base/BasePresenter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index d5aa43fd..0499c90d 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -215,10 +215,9 @@ private function processParams(ReflectionMethod $reflection) $format = MetaFormatHelper::extractFormatFromAttribute($reflection); if ($format !== null) { $this->requestFormatInstance = $this->processParamsFormat($format, null); - return; } - // otherwise use a method for loose parameters + // handle loose parameters $paramData = MetaFormatHelper::extractRequestParamData($reflection); $this->processParamsLoose($paramData); } From 36a27517621d881c8f4ab275e5d62782b3062d08 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 15:35:17 +0100 Subject: [PATCH 10/27] path params are now checked --- .../presenters/base/BasePresenter.php | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 0499c90d..2ea417e1 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -226,11 +226,6 @@ 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 @@ -265,11 +260,6 @@ private function processParamsFormat(string $format, ?array $valueDictionary): M $value = null; // top-level format if ($valueDictionary === null) { - ///TODO: path parameters are not checked yet - if ($requestParamData->type == Type::Path) { - continue; - } - $value = $this->getValueFromParamData($requestParamData); // nested format } else { @@ -302,7 +292,7 @@ private function processParamsFormat(string $format, ?array $valueDictionary): M } /** - * 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. @@ -314,6 +304,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}"); } @@ -356,6 +348,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()) { From 38c910031441b0747efc8f43f84f746a1e5a785b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 15:51:56 +0100 Subject: [PATCH 11/27] VBool text validation now supports strings as well --- app/helpers/MetaFormats/Validators/VBool.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 3db8996e..dda15918 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -16,8 +16,16 @@ public function getExampleValue(): string public function validateText(mixed $value): bool { - // additionally allow 0 and 1 - return $value === 0 || $value === 1 || $this->validateJson($value); + // FILTER_VALIDATE_BOOL is not used because it additionally allows "on", "yes", "off", "no" and "" + + // urlencoded params are strings + return $value === "0" + || $value === "1" + || $value === "true" + || $value === "false" + || $value === 0 + || $value === 1 + || $this->validateJson($value); } public function validateJson(mixed $value): bool From 31ecebb0e5e33691a51b7b10aaf9c1fb5852e920 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 16:13:12 +0100 Subject: [PATCH 12/27] debug: display test values --- app/helpers/MetaFormats/RequestParamData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index b06f8e0b..4bb63ba6 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -75,7 +75,7 @@ public function conformsToDefinition(mixed $value) $type = $validator::SWAGGER_TYPE; throw new InvalidArgumentException( $this->name, - "The provided value did not pass the validation of type '{$type}'." + "The provided value {$value} did not pass the validation of type '{$type}'." ); } } From 8cf01b9e782132da4aa6304ab347a54c7a92003d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 17:21:17 +0100 Subject: [PATCH 13/27] changed validation rule --- app/V1Module/presenters/GroupsPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index 64aad84f..84e8d201 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); From 2b41028cd1761fb02cf9da152bd3c335b2dbb315 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Mar 2025 17:27:13 +0100 Subject: [PATCH 14/27] debug: reverted that error messages showed user values --- app/helpers/MetaFormats/RequestParamData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 4bb63ba6..b06f8e0b 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -75,7 +75,7 @@ public function conformsToDefinition(mixed $value) $type = $validator::SWAGGER_TYPE; throw new InvalidArgumentException( $this->name, - "The provided value {$value} did not pass the validation of type '{$type}'." + "The provided value did not pass the validation of type '{$type}'." ); } } From ad4cf355db63981942d04d963f66f181359b13d0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 15 Mar 2025 18:54:17 +0100 Subject: [PATCH 15/27] added comment --- app/V1Module/presenters/base/BasePresenter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 2ea417e1..bd3d2000 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -222,6 +222,10 @@ private function processParams(ReflectionMethod $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 From 80101e9fb8d2100630b4da6375106062c75e2b87 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 17 Mar 2025 18:33:17 +0100 Subject: [PATCH 16/27] removed 'Presenter' suffix from operation ids --- app/helpers/Swagger/AnnotationData.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 48ce5609..2d3214a1 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -85,6 +85,9 @@ private function constructOperationId() { // remove the namespace prefix of the class and make the first letter lowercase $className = lcfirst(Utils::shortenClass($this->className)); + // remove the 'Presenter' suffix + $className = substr($className, 0, strlen($className) - strlen("Presenter")); + // remove the 'action' prefix $endpoint = substr($this->methodName, strlen("action")); return $className . $endpoint; From f3d8eac0042c07513bf90d6c2041d7a98df4abb5 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 19 Mar 2025 12:57:07 +0100 Subject: [PATCH 17/27] refactored strictness in validators --- app/helpers/MetaFormats/Attributes/FPath.php | 2 - app/helpers/MetaFormats/Attributes/FQuery.php | 2 - .../Attributes/FormatParameterAttribute.php | 13 ++--- app/helpers/MetaFormats/Attributes/Path.php | 2 - app/helpers/MetaFormats/Attributes/Query.php | 2 - .../MetaFormats/Validators/BaseValidator.php | 49 ++++++++----------- app/helpers/MetaFormats/Validators/VArray.php | 20 +++++--- app/helpers/MetaFormats/Validators/VBool.php | 28 +++++------ .../MetaFormats/Validators/VDouble.php | 15 +++--- app/helpers/MetaFormats/Validators/VEmail.php | 13 ++--- app/helpers/MetaFormats/Validators/VInt.php | 11 ++--- app/helpers/MetaFormats/Validators/VMixed.php | 7 +-- .../MetaFormats/Validators/VObject.php | 10 ++-- .../MetaFormats/Validators/VString.php | 10 ++-- app/helpers/MetaFormats/Validators/VUuid.php | 14 +----- 15 files changed, 78 insertions(+), 120 deletions(-) diff --git a/app/helpers/MetaFormats/Attributes/FPath.php b/app/helpers/MetaFormats/Attributes/FPath.php index b376d870..e4a6c4b9 100644 --- a/app/helpers/MetaFormats/Attributes/FPath.php +++ b/app/helpers/MetaFormats/Attributes/FPath.php @@ -24,7 +24,5 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Path, $validators, $description, $required, $nullable); - // do not use json validation for path params - $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/FQuery.php b/app/helpers/MetaFormats/Attributes/FQuery.php index 47c4866c..3cf21c02 100644 --- a/app/helpers/MetaFormats/Attributes/FQuery.php +++ b/app/helpers/MetaFormats/Attributes/FQuery.php @@ -24,7 +24,5 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Query, $validators, $description, $required, $nullable); - // do not use json validation for query params - $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 8c2348d5..c7eb04f2 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -53,15 +53,12 @@ public function __construct( } $this->validators = $validators; } - } - /** - * Disables JSON validation for all validators. - */ - protected function disableJsonValidation() - { - foreach ($this->validators as $validator) { - $validator->useJsonValidation = false; + // 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/Attributes/Path.php b/app/helpers/MetaFormats/Attributes/Path.php index 60d14a82..7df7cca1 100644 --- a/app/helpers/MetaFormats/Attributes/Path.php +++ b/app/helpers/MetaFormats/Attributes/Path.php @@ -26,7 +26,5 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Path, $name, $validators, $description, $required, $nullable); - // do not use json validation for path params - $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Attributes/Query.php b/app/helpers/MetaFormats/Attributes/Query.php index f4b7d172..dc6ea4b9 100644 --- a/app/helpers/MetaFormats/Attributes/Query.php +++ b/app/helpers/MetaFormats/Attributes/Query.php @@ -26,7 +26,5 @@ public function __construct( bool $nullable = false, ) { parent::__construct(Type::Query, $name, $validators, $description, $required, $nullable); - // do not use json validation for query params - $this->disableJsonValidation(); } } diff --git a/app/helpers/MetaFormats/Validators/BaseValidator.php b/app/helpers/MetaFormats/Validators/BaseValidator.php index 2be9a19e..d3d7aa7a 100644 --- a/app/helpers/MetaFormats/Validators/BaseValidator.php +++ b/app/helpers/MetaFormats/Validators/BaseValidator.php @@ -7,59 +7,50 @@ */ class BaseValidator { + public function __construct(bool $strict = true) + { + $this->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 If true, the validateJson method will be used instead of the validateText one for validation. - * Intended to be changed by Attributes containing validators to change their behavior based on the Attribute type. + * @var bool Whether strict type checking is done in validation. */ - public bool $useJsonValidation = true; + protected bool $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. + * 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 getExampleValue(): string | null + public function setStrict(bool $strict) { - return null; + $this->strict = $strict; } /** - * Validates a value retrieved from unstructured data sources, such as query parameters. - * @param mixed $value The value to be validated. - * @return bool Whether the value passed the test. + * @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 validateText(mixed $value): bool + public function getExampleValue(): string | null { - // return false by default to enforce overriding in derived types - return false; + return null; } /** - * Validates a value retrieved from json files (usually request bodies). + * 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 validateJson(mixed $value): bool + public function validate(mixed $value): bool { // return false by default to enforce overriding in derived types return false; } - - /** - * Validates a value with the configured validator method. - * @param mixed $value The value to be validated. - * @return bool Whether the value passed the test. - */ - public function validate(mixed $value): bool - { - if ($this->useJsonValidation) { - return $this->validateJson($value); - } - return $this->validateText($value); - } } diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index f034f59c..7f205d7b 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -10,15 +10,16 @@ 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; } @@ -43,12 +44,19 @@ public function getElementSwaggerType(): mixed return $this->nestedValidator::SWAGGER_TYPE; } - public function validateText(mixed $value): bool + /** + * 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) { - return $this->validateJson($value); + parent::setStrict($strict); + $this->nestedValidator?->setStrict($strict); } - public function validateJson(mixed $value): bool + 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 dda15918..bf0defee 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -14,23 +14,23 @@ public function getExampleValue(): string return "true"; } - public function validateText(mixed $value): bool + public function validate(mixed $value): bool { - // FILTER_VALIDATE_BOOL is not used because it additionally allows "on", "yes", "off", "no" and "" + 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'; + } - // urlencoded params are strings - return $value === "0" + // 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 === "true" || $value === "false" - || $value === 0 - || $value === 1 - || $this->validateJson($value); - } - - public function validateJson(mixed $value): bool - { - ///TODO: remove 'false' once the testUpdateInstance test issue is fixed. - return $value === true || $value === false || $value === 'false'; + || $value === "true"; } } diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index cfa94770..fa7b3c36 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -14,18 +14,17 @@ public function getExampleValue(): string return "0.1"; } - public function validateText(mixed $value): bool + public function validate(mixed $value): bool { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool - { - // check if it is a double - if (is_double($value)) { + // check if it is a double or a whole number (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 c3d34368..250758a3 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -7,10 +7,10 @@ */ 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(): string @@ -18,14 +18,9 @@ public function getExampleValue(): string return "name@domain.tld"; } - public function validateText(mixed $value): bool + public function validate(mixed $value): bool { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool - { - if (!parent::validateJson($value)) { + if (!parent::validate($value)) { return false; } diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index f6033fda..570e9a5a 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -14,18 +14,17 @@ public function getExampleValue(): string return "0"; } - public function validateText(mixed $value): bool - { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool + 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 b02f33c5..ad856938 100644 --- a/app/helpers/MetaFormats/Validators/VMixed.php +++ b/app/helpers/MetaFormats/Validators/VMixed.php @@ -11,12 +11,7 @@ class VMixed extends BaseValidator { public const SWAGGER_TYPE = "string"; - public function validateText(mixed $value): bool - { - return true; - } - - public function validateJson(mixed $value): bool + public function validate(mixed $value): bool { return true; } diff --git a/app/helpers/MetaFormats/Validators/VObject.php b/app/helpers/MetaFormats/Validators/VObject.php index d607d54b..892233b9 100644 --- a/app/helpers/MetaFormats/Validators/VObject.php +++ b/app/helpers/MetaFormats/Validators/VObject.php @@ -15,8 +15,9 @@ class VObject extends BaseValidator public const SWAGGER_TYPE = "object"; public string $format; - public function __construct(string $format) + public function __construct(string $format, bool $strict = true) { + parent::__construct($strict); $this->format = $format; // throw immediately if the format does not exist @@ -25,12 +26,7 @@ public function __construct(string $format) } } - public function validateText(mixed $value): bool - { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool + 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 a337558f..19cc6368 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -19,8 +19,9 @@ class VString extends BaseValidator * @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; @@ -31,12 +32,7 @@ public function getExampleValue(): string return "text"; } - public function validateText(mixed $value): bool - { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool + public function validate(mixed $value): bool { // do not allow other types if (!is_string($value)) { diff --git a/app/helpers/MetaFormats/Validators/VUuid.php b/app/helpers/MetaFormats/Validators/VUuid.php index 7b75e75d..7becf8a4 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -7,23 +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(): string { return "10000000-2000-4000-8000-160000000000"; } - - public function validateText(mixed $value): bool - { - return $this->validateJson($value); - } - - public function validateJson(mixed $value): bool - { - return parent::validateJson($value); - } } From 2bfe17d9fda65394a778a6c9c63ea3fc87f60216 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 19 Mar 2025 13:01:00 +0100 Subject: [PATCH 18/27] changed string value to int --- tests/Presenters/LoginPresenter.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Presenters/LoginPresenter.phpt b/tests/Presenters/LoginPresenter.phpt index a622b4a8..30a735a4 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); From f2311851074e5639be173f31a3eea79a04216159 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Mar 2025 14:08:58 +0100 Subject: [PATCH 19/27] string validator constraints are now propagated to the swagger document --- app/helpers/MetaFormats/RequestParamData.php | 11 ++++++ .../MetaFormats/Validators/BaseValidator.php | 12 ++++++ .../MetaFormats/Validators/VString.php | 10 +++++ .../Swagger/AnnotationParameterData.php | 11 +++++- app/helpers/Swagger/ParameterConstraints.php | 39 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 app/helpers/Swagger/ParameterConstraints.php diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index b06f8e0b..42429736 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -122,6 +122,16 @@ public function toAnnotationParameterData() // get example value from the first validator $exampleValue = $this->validators[0]->getExampleValue(); + // get constraints from validators + $constraints = null; + foreach ($this->validators as $validator) { + $constraints = $validator->getConstraints(); + // it is assumed that at most one validator defines constraints + if ($constraints !== null) { + break; + } + } + // add nested parameter data if this is an object $format = $this->getFormatName(); $nestedObjectParameterData = null; @@ -142,6 +152,7 @@ public function toAnnotationParameterData() $exampleValue, $nestedArraySwaggerType, $nestedObjectParameterData, + $constraints, ); } } diff --git a/app/helpers/MetaFormats/Validators/BaseValidator.php b/app/helpers/MetaFormats/Validators/BaseValidator.php index d3d7aa7a..c1c09f9f 100644 --- a/app/helpers/MetaFormats/Validators/BaseValidator.php +++ b/app/helpers/MetaFormats/Validators/BaseValidator.php @@ -2,6 +2,8 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\Swagger\ParameterConstraints; + /** * Base class for all validators. */ @@ -43,6 +45,16 @@ public function getExampleValue(): string | null return null; } + /** + * @return ParameterConstraints Returns all parameter constrains that will be written into the generated + * swagger document. Returns null if there are no constraints. + */ + public function getConstraints(): ?ParameterConstraints + { + // there are no default constraints + return null; + } + /** * Validates a value with the configured validation strictness. * @param mixed $value The value to be validated. diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index 19cc6368..a5532460 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -2,6 +2,8 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\Swagger\ParameterConstraints; + /** * Validates strings. */ @@ -32,6 +34,14 @@ public function getExampleValue(): string return "text"; } + public function getConstraints(): ParameterConstraints + { + // do not pass redundant constraints + $minLength = ($this->minLength > 0 ? $this->minLength : null); + $maxLength = ($this->maxLength !== -1 ? $this->maxLength : null); + return new ParameterConstraints($this->regex, $minLength, $maxLength); + } + public function validate(mixed $value): bool { // do not allow other types diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index 81dd2427..1f5d980c 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -19,6 +19,7 @@ class AnnotationParameterData public ?string $example; public ?string $nestedArraySwaggerType; public ?array $nestedObjectParameterData; + public ?ParameterConstraints $constraints; public function __construct( string $swaggerType, @@ -27,9 +28,10 @@ public function __construct( string $location, bool $required, bool $nullable, - string $example = null, - string $nestedArraySwaggerType = null, + ?string $example = null, + ?string $nestedArraySwaggerType = null, ?array $nestedObjectParameterData = null, + ?ParameterConstraints $constraints = null, ) { $this->swaggerType = $swaggerType; $this->name = $name; @@ -40,10 +42,12 @@ public function __construct( $this->example = $example; $this->nestedArraySwaggerType = $nestedArraySwaggerType; $this->nestedObjectParameterData = $nestedObjectParameterData; + $this->constraints = $constraints; } private function addArrayItemsIfArray(ParenthesesBuilder $container) { + ///TODO: nested constraints should be added here if ($this->swaggerType !== "array") { return; } @@ -128,6 +132,9 @@ public function toPropertyAnnotation(): string $body->addKeyValue("description", $this->description); } + // handle param constraints + $this->constraints?->addConstraints($body); + // handle arrays $this->addArrayItemsIfArray($body); diff --git a/app/helpers/Swagger/ParameterConstraints.php b/app/helpers/Swagger/ParameterConstraints.php new file mode 100644 index 00000000..1503dc75 --- /dev/null +++ b/app/helpers/Swagger/ParameterConstraints.php @@ -0,0 +1,39 @@ +constraints["pattern"] = $pattern; + $this->constraints["minLength"] = $minLength; + $this->constraints["maxLength"] = $maxLength; + } + + /** + * Adds constraints to a ParenthesesBuilder for swagger doc construction. + * @param \App\Helpers\Swagger\ParenthesesBuilder $container The container for keywords and values. + */ + public function addConstraints(ParenthesesBuilder $container) + { + foreach ($this->constraints as $keyword=>$value) { + // skip null values + if ($value === null) { + continue; + } + + $container->addKeyValue($keyword, $value); + } + } +} From 97728d7b5428c02c76f0b7cfa3ecb64de318d181 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 22 Mar 2025 14:31:44 +0100 Subject: [PATCH 20/27] required and nullable flags are now generated in the schema section --- app/helpers/Swagger/AnnotationParameterData.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index 1f5d980c..fb33cd23 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -89,6 +89,7 @@ private function generateSchemaAnnotation(): string $body = new ParenthesesBuilder(); $body->addKeyValue("type", $this->swaggerType); + $body->addKeyValue("nullable", $this->nullable); $this->addArrayItemsIfArray($body); return $head . $body->toString(); @@ -127,6 +128,7 @@ public function toPropertyAnnotation(): string $body->addKeyValue("property", $this->name); $body->addKeyValue("type", $this->swaggerType); $body->addKeyValue("nullable", $this->nullable); + $body->addKeyValue("required", $this->required); if ($this->description !== null) { $body->addKeyValue("description", $this->description); From 7dc3bdd009b8fce5f488b5def0da5ecfa51a2efa Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 22 Mar 2025 15:05:19 +0100 Subject: [PATCH 21/27] bugfix: required properties are now listed correctly --- app/helpers/Swagger/AnnotationData.php | 26 +++++++++++++++++++ .../Swagger/AnnotationParameterData.php | 1 - 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 2d3214a1..8b3fc856 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -13,8 +13,17 @@ class AnnotationData public string $className; public string $methodName; + /** + * @var AnnotationParameterData[] + */ public array $pathParams; + /** + * @var AnnotationParameterData[] + */ public array $queryParams; + /** + * @var AnnotationParameterData[] + */ public array $bodyParams; public ?string $endpointDescription; @@ -68,9 +77,26 @@ private function getBodyAnnotation(): string | null // only json is supported due to the media type $head = '@OA\RequestBody(@OA\MediaType(mediaType="application/json",@OA\Schema'; $body = new ParenthesesBuilder(); + // list of all required properties + $required = []; foreach ($this->bodyParams as $bodyParam) { $body->addValue($bodyParam->toPropertyAnnotation()); + if ($bodyParam->required) { + $required[] = $bodyParam->name; + } + } + + // add a list of required properties + if (count($required) > 0) { + // stringify the list (it has to be in '{"name1","name1",...}' format) + $requiredString = "{"; + foreach ($required as $name) { + $requiredString .= "\"$name\","; + } + $requiredString .= "}"; + + $body->addValue("required=" . $requiredString); } return $head . $body->toString() . "))"; diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index fb33cd23..f3dc4207 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -128,7 +128,6 @@ public function toPropertyAnnotation(): string $body->addKeyValue("property", $this->name); $body->addKeyValue("type", $this->swaggerType); $body->addKeyValue("nullable", $this->nullable); - $body->addKeyValue("required", $this->required); if ($this->description !== null) { $body->addKeyValue("description", $this->description); From 864243ee16fa40aa37afabe18f283611d748abab Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 23 Mar 2025 18:17:11 +0100 Subject: [PATCH 22/27] operationId is now more verbose so that the class and method names can be discerned --- app/helpers/Swagger/AnnotationData.php | 7 +------ app/helpers/Swagger/AnnotationParameterData.php | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 8b3fc856..69d3e54c 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -111,12 +111,7 @@ private function constructOperationId() { // remove the namespace prefix of the class and make the first letter lowercase $className = lcfirst(Utils::shortenClass($this->className)); - // remove the 'Presenter' suffix - $className = substr($className, 0, strlen($className) - strlen("Presenter")); - - // remove the 'action' prefix - $endpoint = substr($this->methodName, strlen("action")); - return $className . $endpoint; + return $className . $this->methodName; } /** diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index f3dc4207..db17f1ed 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -95,9 +95,9 @@ private function generateSchemaAnnotation(): string return $head . $body->toString(); } - /** - * Converts the object to a @OA\Parameter(...) annotation string - */ + /** + * Converts the object to a @OA\Parameter(...) annotation string + */ public function toParameterAnnotation(): string { $head = "@OA\\Parameter"; From 782f2801c4930b8969fd824dd4efceedc6bea8b3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 23 Mar 2025 18:23:43 +0100 Subject: [PATCH 23/27] bugfix: operationId is now camel-case --- app/helpers/Swagger/AnnotationData.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 69d3e54c..85e6f2ee 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -111,7 +111,9 @@ private function constructOperationId() { // remove the namespace prefix of the class and make the first letter lowercase $className = lcfirst(Utils::shortenClass($this->className)); - return $className . $this->methodName; + // make the 'a' in the action prefix uppercase to match the camel-case notation + $endpoint = ucfirst($this->methodName); + return $className . $endpoint; } /** From e455656f5de3bbf3581697b634fb5246fd31ad52 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 19 Apr 2025 16:48:03 +0200 Subject: [PATCH 24/27] fixed bad endpoint annotation --- app/V1Module/presenters/LoginPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index 47e3c526..646e148e 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -206,7 +206,7 @@ public function checkRefresh() /** * Refresh the access token of current user - * @GET + * @POST * @LoggedIn * @throws ForbiddenRequestException */ From 48020e7e7bdc08a2f1a974e1de2d2f8c420e5f92 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 7 May 2025 11:01:16 +0200 Subject: [PATCH 25/27] fixed bad swagger string patterns --- app/helpers/Swagger/ParameterConstraints.php | 63 +++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/app/helpers/Swagger/ParameterConstraints.php b/app/helpers/Swagger/ParameterConstraints.php index 1503dc75..58e44232 100644 --- a/app/helpers/Swagger/ParameterConstraints.php +++ b/app/helpers/Swagger/ParameterConstraints.php @@ -4,36 +4,41 @@ class ParameterConstraints { - private array $constraints; - - /** - * Constructs a container for swagger constraints. - * Constructor parameter names match swagger keywords, see - * https://swagger.io/docs/specification/v3_0/data-models/keywords/. - * @param ?string $pattern String regex pattern. - * @param ?int $minLength String min length. - * @param ?int $maxLength String max length. - */ - public function __construct(?string $pattern = null, ?int $minLength = null, ?int $maxLength = null) - { - $this->constraints["pattern"] = $pattern; - $this->constraints["minLength"] = $minLength; - $this->constraints["maxLength"] = $maxLength; - } + private array $constraints; - /** - * Adds constraints to a ParenthesesBuilder for swagger doc construction. - * @param \App\Helpers\Swagger\ParenthesesBuilder $container The container for keywords and values. - */ - public function addConstraints(ParenthesesBuilder $container) - { - foreach ($this->constraints as $keyword=>$value) { - // skip null values - if ($value === null) { - continue; - } + /** + * Constructs a container for swagger constraints. + * Constructor parameter names match swagger keywords, see + * https://swagger.io/docs/specification/v3_0/data-models/keywords/. + * @param ?string $pattern String regex pattern. + * @param ?int $minLength String min length. + * @param ?int $maxLength String max length. + */ + public function __construct(?string $pattern = null, ?int $minLength = null, ?int $maxLength = null) + { + # swagger patterns must not contain the bounding '/.../' slashes + if ($pattern != null && strlen($pattern) >= 2) { + $pattern = substr($pattern, 1, -1); + } - $container->addKeyValue($keyword, $value); + $this->constraints["pattern"] = $pattern; + $this->constraints["minLength"] = $minLength; + $this->constraints["maxLength"] = $maxLength; + } + + /** + * Adds constraints to a ParenthesesBuilder for swagger doc construction. + * @param \App\Helpers\Swagger\ParenthesesBuilder $container The container for keywords and values. + */ + public function addConstraints(ParenthesesBuilder $container) + { + foreach ($this->constraints as $keyword => $value) { + // skip null values + if ($value === null) { + continue; + } + + $container->addKeyValue($keyword, $value); + } } - } } From c8c8fc574fd521dd3fbe045bf9e7803b68e32662 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 13 May 2025 15:45:54 +0200 Subject: [PATCH 26/27] improved requirement string generation algorithm --- app/helpers/Swagger/AnnotationData.php | 10 +++------- app/helpers/Swagger/ParameterConstraints.php | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index e8d14fc4..57d127e9 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -83,19 +83,15 @@ private function getBodyAnnotation(): string | null foreach ($this->bodyParams as $bodyParam) { $body->addValue($bodyParam->toPropertyAnnotation()); if ($bodyParam->required) { - $required[] = $bodyParam->name; + // add quotes around the names (required by the swagger generator) + $required[] = '"' . $bodyParam->name . '"'; } } // add a list of required properties if (count($required) > 0) { // stringify the list (it has to be in '{"name1","name1",...}' format) - $requiredString = "{"; - foreach ($required as $name) { - $requiredString .= "\"$name\","; - } - $requiredString .= "}"; - + $requiredString = "{" . implode(",", $required) . "}"; $body->addValue("required=" . $requiredString); } diff --git a/app/helpers/Swagger/ParameterConstraints.php b/app/helpers/Swagger/ParameterConstraints.php index 58e44232..9ddb07a1 100644 --- a/app/helpers/Swagger/ParameterConstraints.php +++ b/app/helpers/Swagger/ParameterConstraints.php @@ -16,7 +16,7 @@ class ParameterConstraints */ public function __construct(?string $pattern = null, ?int $minLength = null, ?int $maxLength = null) { - # swagger patterns must not contain the bounding '/.../' slashes + // swagger patterns must not contain the bounding '/.../' slashes if ($pattern != null && strlen($pattern) >= 2) { $pattern = substr($pattern, 1, -1); } From a0e58acee50f17f1204f9ff494f986205b887554 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 20 May 2025 10:49:31 +0200 Subject: [PATCH 27/27] added support for arrays of arbitrary depths --- app/helpers/MetaFormats/RequestParamData.php | 5 +- app/helpers/MetaFormats/Validators/VArray.php | 32 ++++++++++++- app/helpers/Swagger/AnnotationHelper.php | 6 +++ .../Swagger/AnnotationParameterData.php | 48 +++++++++++++++---- 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 97ede2ae..203d135e 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -112,9 +112,11 @@ public function toAnnotationParameterData() // determine swagger type $nestedArraySwaggerType = null; + $arrayDepth = null; $swaggerType = $this->validators[0]::SWAGGER_TYPE; - // extract array element type + // extract array depth and element type if ($this->validators[0] instanceof VArray) { + $arrayDepth = $this->validators[0]->getArrayDepth(); $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); } @@ -150,6 +152,7 @@ public function toAnnotationParameterData() $this->nullable, $exampleValue, $nestedArraySwaggerType, + $arrayDepth, $nestedObjectParameterData, $constraints, ); diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index 7f205d7b..70680064 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -2,6 +2,8 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\Swagger\ParameterConstraints; + /** * Validates arrays and their nested elements. */ @@ -33,17 +35,45 @@ public function getExampleValue(): string | null } /** - * @return string|null Returns the element swagger type. Can be null if the element validator is not set. + * @return string|null Returns the bottommost element swagger type. Can be null if the element validator is not set. */ public function getElementSwaggerType(): mixed { + // return null if the element type is unspecified if ($this->nestedValidator === null) { return null; } + // traverse the VArray chain to get the final element type + if ($this->nestedValidator instanceof VArray) { + return $this->nestedValidator->getElementSwaggerType(); + } + return $this->nestedValidator::SWAGGER_TYPE; } + /** + * @return int Returns the defined depth of the array. + * 1 for arrays containing the final elements, 2 for arrays of arrays etc. + */ + public function getArrayDepth(): int + { + if ($this->nestedValidator instanceof VArray) { + return $this->nestedValidator->getArrayDepth() + 1; + } + + return 1; + } + + /** + * @return ParameterConstraints Returns all parameter constrains of the bottommost element type that will be + * written into the generated swagger document. Returns null if there are no constraints. + */ + public function getConstraints(): ?ParameterConstraints + { + return $this->nestedValidator?->getConstraints(); + } + /** * 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. diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 77c3bce7..c28a12e0 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -163,6 +163,11 @@ private static function extractStandardAnnotationParams(array $annotations, stri // the array element type cannot be determined from standard @param annotations $nestedArraySwaggerType = null; + // the actual depth of the array cannot be determined as well + $arrayDepth = null; + if ($swaggerType == "array") { + $arrayDepth = 1; + } $descriptor = new AnnotationParameterData( $swaggerType, @@ -172,6 +177,7 @@ private static function extractStandardAnnotationParams(array $annotations, stri $isPathParam, $nullable, nestedArraySwaggerType: $nestedArraySwaggerType, + arrayDepth: $arrayDepth, ); $params[] = $descriptor; } diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index db17f1ed..6ee06bd2 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 ?int $arrayDepth; public ?array $nestedObjectParameterData; public ?ParameterConstraints $constraints; @@ -30,6 +31,7 @@ public function __construct( bool $nullable, ?string $example = null, ?string $nestedArraySwaggerType = null, + ?int $arrayDepth = null, ?array $nestedObjectParameterData = null, ?ParameterConstraints $constraints = null, ) { @@ -41,30 +43,54 @@ public function __construct( $this->nullable = $nullable; $this->example = $example; $this->nestedArraySwaggerType = $nestedArraySwaggerType; + $this->arrayDepth = $arrayDepth; $this->nestedObjectParameterData = $nestedObjectParameterData; $this->constraints = $constraints; } private function addArrayItemsIfArray(ParenthesesBuilder $container) { - ///TODO: nested constraints should be added here if ($this->swaggerType !== "array") { return; } $itemsHead = "@OA\\Items"; - $items = new ParenthesesBuilder(); + $layers = []; + for ($i = 0; $i < $this->arrayDepth; $i++) { + $items = new ParenthesesBuilder(); + + // add array time for all nested arrays + if ($i < $this->arrayDepth - 1) { + $items->addKeyValue("type", "array"); + // handle bottommost elements + } else { + // add element type if present + if ($this->nestedArraySwaggerType !== null) { + $items->addKeyValue("type", $this->nestedArraySwaggerType); + } + + // add example value + if ($this->example != null) { + $items->addKeyValue("example", $this->example); + } + + // add constraints + $this->constraints?->addConstraints($items); + } - if ($this->nestedArraySwaggerType !== null) { - $items->addKeyValue("type", $this->nestedArraySwaggerType); + $layers[] = $items; } - // add example value - if ($this->example != null) { - $items->addKeyValue("example", $this->example); + // serialize the layers from the bottom up + $layers = array_reverse($layers); + $serialized_layer = $itemsHead . $layers[0]->toString(); + for ($i = 1; $i < $this->arrayDepth; $i++) { + $layer = $layers[$i]; + $layer->addValue($serialized_layer); + $serialized_layer = $itemsHead . $layer->toString(); } - $container->addValue($itemsHead . $items->toString()); + $container->addValue($serialized_layer); } private function addObjectParamsIfObject(ParenthesesBuilder $container) @@ -133,8 +159,10 @@ public function toPropertyAnnotation(): string $body->addKeyValue("description", $this->description); } - // handle param constraints - $this->constraints?->addConstraints($body); + // handle param constraints (array constrains have to be added to the element, not the array) + if ($this->swaggerType !== "array") { + $this->constraints?->addConstraints($body); + } // handle arrays $this->addArrayItemsIfArray($body);