Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
18501e1
added support for nested formats
eceltov Mar 2, 2025
73752f0
added swagger generator support for format classes
eceltov Mar 2, 2025
68bd434
added readable operationIds
eceltov Mar 5, 2025
f620fb2
Merge branch 'meta-views' into nested-formats
eceltov Mar 6, 2025
50b4587
validators now have a common ancestor
eceltov Mar 12, 2025
87f280b
renamed VFormat to VObject
eceltov Mar 12, 2025
971f87f
removed endless recursion
eceltov Mar 13, 2025
3d17f25
getExampleValue can now return null
eceltov Mar 13, 2025
c270f1e
query and path parameters now use different validators
eceltov Mar 13, 2025
5764802
format and loose attributes can both be used on the same endpoint
eceltov Mar 14, 2025
36a2751
path params are now checked
eceltov Mar 14, 2025
38c9100
VBool text validation now supports strings as well
eceltov Mar 14, 2025
31ecebb
debug: display test values
eceltov Mar 14, 2025
8cf01b9
changed validation rule
eceltov Mar 14, 2025
2b41028
debug: reverted that error messages showed user values
eceltov Mar 14, 2025
a20f7f5
Merge remote-tracking branch 'origin/master' into nested-formats
eceltov Mar 15, 2025
ad4cf35
added comment
eceltov Mar 15, 2025
80101e9
removed 'Presenter' suffix from operation ids
eceltov Mar 17, 2025
f3d8eac
refactored strictness in validators
eceltov Mar 19, 2025
2bfe17d
changed string value to int
eceltov Mar 19, 2025
5bcdd30
Merge branch 'nested-formats' into client-generator-adaptation
eceltov Mar 20, 2025
f231185
string validator constraints are now propagated to the swagger document
eceltov Mar 20, 2025
97728d7
required and nullable flags are now generated in the schema section
eceltov Mar 22, 2025
7dc3bdd
bugfix: required properties are now listed correctly
eceltov Mar 22, 2025
864243e
operationId is now more verbose so that the class and method names ca…
eceltov Mar 23, 2025
782f280
bugfix: operationId is now camel-case
eceltov Mar 23, 2025
e455656
fixed bad endpoint annotation
eceltov Apr 19, 2025
1c82ccf
merged
eceltov Apr 30, 2025
48020e7
fixed bad swagger string patterns
eceltov May 7, 2025
c8c8fc5
improved requirement string generation algorithm
eceltov May 13, 2025
a0e58ac
added support for arrays of arbitrary depths
eceltov May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/V1Module/presenters/LoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public function checkRefresh()

/**
* Refresh the access token of current user
* @GET
* @POST
* @LoggedIn
* @throws ForbiddenRequestException
*/
Expand Down
16 changes: 15 additions & 1 deletion app/helpers/MetaFormats/RequestParamData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -140,7 +152,9 @@ public function toAnnotationParameterData()
$this->nullable,
$exampleValue,
$nestedArraySwaggerType,
$arrayDepth,
$nestedObjectParameterData,
$constraints,
);
}
}
12 changes: 12 additions & 0 deletions app/helpers/MetaFormats/Validators/BaseValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Helpers\MetaFormats\Validators;

use App\Helpers\Swagger\ParameterConstraints;

/**
* Base class for all validators.
*/
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 31 additions & 1 deletion app/helpers/MetaFormats/Validators/VArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Helpers\MetaFormats\Validators;

use App\Helpers\Swagger\ParameterConstraints;

/**
* Validates arrays and their nested elements.
*/
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions app/helpers/MetaFormats/Validators/VString.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Helpers\MetaFormats\Validators;

use App\Helpers\Swagger\ParameterConstraints;

/**
* Validates strings.
*/
Expand Down Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions app/helpers/Swagger/AnnotationData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() . "))";
Expand All @@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions app/helpers/Swagger/AnnotationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -172,6 +177,7 @@ private static function extractStandardAnnotationParams(array $annotations, stri
$isPathParam,
$nullable,
nestedArraySwaggerType: $nestedArraySwaggerType,
arrayDepth: $arrayDepth,
);
$params[] = $descriptor;
}
Expand Down
50 changes: 43 additions & 7 deletions app/helpers/Swagger/AnnotationParameterData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
44 changes: 44 additions & 0 deletions app/helpers/Swagger/ParameterConstraints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace App\Helpers\Swagger;

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)
{
// swagger patterns must not contain the bounding '/.../' slashes
if ($pattern != null && strlen($pattern) >= 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);
}
}
}