diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index 8d76c69c..6a47c421 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -199,7 +199,7 @@ public function checkRefresh() /** * Refresh the access token of current user - * @GET + * @POST * @LoggedIn * @throws ForbiddenRequestException */ diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index a095c3b9..203d135e 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -112,15 +112,27 @@ 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(); } // 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; @@ -140,7 +152,9 @@ public function toAnnotationParameterData() $this->nullable, $exampleValue, $nestedArraySwaggerType, + $arrayDepth, $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/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/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/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 48ce5609..57d127e9 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,22 @@ 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) { + // 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 = "{" . implode(",", $required) . "}"; + $body->addValue("required=" . $requiredString); } return $head . $body->toString() . "))"; @@ -85,8 +107,8 @@ 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")); + // make the 'a' in the action prefix uppercase to match the camel-case notation + $endpoint = ucfirst($this->methodName); return $className . $endpoint; } 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 3426960c..6ee06bd2 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -18,7 +18,9 @@ class AnnotationParameterData public bool $nullable; public ?string $example; public ?string $nestedArraySwaggerType; + public ?int $arrayDepth; public ?array $nestedObjectParameterData; + public ?ParameterConstraints $constraints; public function __construct( string $swaggerType, @@ -29,7 +31,9 @@ public function __construct( bool $nullable, ?string $example = null, ?string $nestedArraySwaggerType = null, + ?int $arrayDepth = null, ?array $nestedObjectParameterData = null, + ?ParameterConstraints $constraints = null, ) { $this->swaggerType = $swaggerType; $this->name = $name; @@ -39,7 +43,9 @@ 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) @@ -49,18 +55,42 @@ private function addArrayItemsIfArray(ParenthesesBuilder $container) } $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) @@ -85,6 +115,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(); @@ -128,6 +159,11 @@ public function toPropertyAnnotation(): string $body->addKeyValue("description", $this->description); } + // 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); diff --git a/app/helpers/Swagger/ParameterConstraints.php b/app/helpers/Swagger/ParameterConstraints.php new file mode 100644 index 00000000..9ddb07a1 --- /dev/null +++ b/app/helpers/Swagger/ParameterConstraints.php @@ -0,0 +1,44 @@ += 2) { + $pattern = substr($pattern, 1, -1); + } + + $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); + } + } +}